From 4717f17b7b624b46150ba6bf7bd95d9b247d77ea Mon Sep 17 00:00:00 2001 From: Cosmas Ken Date: Sun, 19 Oct 2025 11:33:47 +0300 Subject: [PATCH 1/4] Fix Issue #2 - Problems in README.MD (executable name and path references) --- .gitignore | 2 +- .idea/modules.xml | 2 +- Cargo.lock | 24 ++++++++++++------------ Cargo.toml | 2 +- README.MD | 2 +- src/commands/contacts.rs | 6 +++--- src/config/config.rs | 4 ++-- src/config/mod.rs | 2 +- src/config/setup.rs | 2 +- src/utils/api.rs | 2 +- src/utils/constants.rs | 4 ++-- src/utils/helper.rs | 2 +- 12 files changed, 27 insertions(+), 27 deletions(-) diff --git a/.gitignore b/.gitignore index 63e9227..d6ca1f4 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,7 @@ wallet*.png *copied* *Copied* -rootstock-wallet.json +rsk-rust-cli.json mined.json here exported.key diff --git a/.idea/modules.xml b/.idea/modules.xml index 2f440b0..7504a61 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 230fef2..5812f82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4085,7 +4085,18 @@ dependencies = [ ] [[package]] -name = "rootstock-wallet" +name = "rpassword" +version = "7.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d4c8b64f049c6721ec8ccec37ddfc3d641c4a7fca57e8f2a89de509c73df39" +dependencies = [ + "libc", + "rtoolbox", + "windows-sys 0.59.0", +] + +[[package]] +name = "rsk-rust-cli" version = "0.1.0" dependencies = [ "aes", @@ -4138,17 +4149,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rpassword" -version = "7.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d4c8b64f049c6721ec8ccec37ddfc3d641c4a7fca57e8f2a89de509c73df39" -dependencies = [ - "libc", - "rtoolbox", - "windows-sys 0.59.0", -] - [[package]] name = "rtoolbox" version = "0.0.3" diff --git a/Cargo.toml b/Cargo.toml index 54ebd24..5092f5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "rootstock-wallet" +name = "rsk-rust-cli" version = "0.1.0" edition = "2024" diff --git a/README.MD b/README.MD index e67e4bd..2304679 100644 --- a/README.MD +++ b/README.MD @@ -180,7 +180,7 @@ Add, list, and delete contacts from your address book. #### Wallet Issues -- Confirm wallet file exists at `~/.local/share/rootstock-wallet/` +- Confirm wallet file exists at `~/.local/share/rsk-rust-cli/` - Check file permissions if access is denied - Ensure you're using the correct network (mainnet/testnet) diff --git a/src/commands/contacts.rs b/src/commands/contacts.rs index a7cef3d..0506b79 100644 --- a/src/commands/contacts.rs +++ b/src/commands/contacts.rs @@ -279,7 +279,7 @@ impl ContactsCommand { pub fn load_contacts(&self) -> Result> { let contacts_path = dirs::data_local_dir() .ok_or_else(|| anyhow::anyhow!("Failed to get data directory"))? - .join("rootstock-wallet") + .join("rsk-rust-cli") .join("contacts.json"); if !contacts_path.exists() { @@ -294,7 +294,7 @@ impl ContactsCommand { // pub fn save_contacts(&self, contacts: &[Contact]) -> Result<()> { // let contacts_path = dirs::data_local_dir() // .ok_or_else(|| anyhow::anyhow!("Failed to get data directory"))? - // .join("rootstock-wallet") + // .join("rsk-rust-cli") // .join("contacts.json"); // std::fs::create_dir_all(contacts_path.parent().unwrap())?; @@ -305,7 +305,7 @@ impl ContactsCommand { pub fn save_contacts(&self, contacts: &[Contact]) -> Result<()> { let contacts_dir = dirs::data_local_dir() .ok_or_else(|| anyhow::anyhow!("Failed to get data directory"))? - .join("rootstock-wallet"); + .join("rsk-rust-cli"); std::fs::create_dir_all(&contacts_dir)?; diff --git a/src/config/config.rs b/src/config/config.rs index c934a8e..7856f6b 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -120,7 +120,7 @@ impl ConfigManager { pub fn new() -> Result { let config_dir = dirs::config_dir() .context("Could not find config directory")? - .join("rootstock-wallet"); + .join("rsk-rust-cli"); std::fs::create_dir_all(&config_dir)?; @@ -200,7 +200,7 @@ impl ConfigManager { // Clear wallet data directory if let Some(data_dir) = dirs::data_local_dir() { - let wallet_data_dir = data_dir.join("rootstock-wallet"); + let wallet_data_dir = data_dir.join("rsk-rust-cli"); if wallet_data_dir.exists() { // Remove all files in the wallet data directory for entry in fs::read_dir(&wallet_data_dir)? { diff --git a/src/config/mod.rs b/src/config/mod.rs index b963e0c..07068bc 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -17,4 +17,4 @@ pub const RSK_RPC_DOCS_URL: &str = "https://dev.rootstock.io/developers/rpc-api/ pub const ALCH_MAINNET_URL: &str = "https://dashboard.alchemy.com/apps/create?referrer=/apps"; pub const ALCH_TESTNET_URL: &str = "https://dashboard.alchemy.com/apps/create?referrer=/apps&chain=rsk-testnet"; -pub const DOCS_URL: &str = "https://github.com/cosmasken/rootstock-wallet/wiki"; +pub const DOCS_URL: &str = "https://github.com/rsksmart/rsk-rust-cli/wiki"; diff --git a/src/config/setup.rs b/src/config/setup.rs index 6b5e571..9fabd46 100644 --- a/src/config/setup.rs +++ b/src/config/setup.rs @@ -55,7 +55,7 @@ pub fn run_setup_wizard() -> Result<()> { println!("\n{}", style("✅ Setup complete!").bold().green()); println!("\nYou can now use the wallet. For more information, visit:"); println!("{}", style(DOCS_URL).blue().underlined()); - println!("\nRun `rootstock-wallet --help` to see available commands."); + println!("\nRun `rsk-rust-cli --help` to see available commands."); Ok(()) } diff --git a/src/utils/api.rs b/src/utils/api.rs index 5b5eedf..2b7b23d 100644 --- a/src/utils/api.rs +++ b/src/utils/api.rs @@ -67,7 +67,7 @@ impl ApiKeys { fn get_config_path() -> Result { let mut path = dirs::config_dir() .context("Could not find config directory")? - .join("rootstock-wallet"); + .join("rsk-rust-cli"); std::fs::create_dir_all(&path)?; path.push(API_KEYS_FILE); diff --git a/src/utils/constants.rs b/src/utils/constants.rs index 26a523c..3b0bfbc 100644 --- a/src/utils/constants.rs +++ b/src/utils/constants.rs @@ -3,12 +3,12 @@ use std::path::PathBuf; pub fn wallet_file_path() -> PathBuf { let dir = dirs::data_local_dir() .expect("Failed to get data directory") - .join("rootstock-wallet"); + .join("rsk-rust-cli"); // Ensure the directory exists std::fs::create_dir_all(&dir).expect("Failed to create wallet directory"); - dir.join("rootstock-wallet.json") + dir.join("rsk-rust-cli.json") } pub const METHOD_TYPES: &str = "read"; diff --git a/src/utils/helper.rs b/src/utils/helper.rs index a3f871b..673fc3d 100644 --- a/src/utils/helper.rs +++ b/src/utils/helper.rs @@ -69,7 +69,7 @@ impl Helper { }; println!( - "[rootstock-wallet] Connected to {} at {} ({})", + "[rsk-rust-cli] Connected to {} at {} ({})", config.network.name, config.network.rpc_url, rpc_type.dimmed() From 8bea9830b42d915e9a6f04472dbf3bea59ae961c Mon Sep 17 00:00:00 2001 From: cosmasken Date: Mon, 27 Oct 2025 08:58:59 +0300 Subject: [PATCH 2/4] Security audit remediation: Fix all 10 identified vulnerabilities --- Cargo.lock | 797 ++++++++++++++-------------- Cargo.toml | 23 +- README.MD | 69 ++- src/api/mod.rs | 5 +- src/commands/api.rs | 76 ++- src/commands/balance.rs | 95 +++- src/commands/history.rs | 23 +- src/commands/transfer.rs | 19 +- src/commands/tx.rs | 179 +++++-- src/commands/wallet.rs | 39 +- src/config/setup.rs | 10 +- src/interactive/balance.rs | 81 ++- src/interactive/bulk_transfer.rs | 67 ++- src/interactive/config.rs | 6 +- src/interactive/contract.rs | 154 ------ src/interactive/history.rs | 107 +++- src/interactive/mod.rs | 100 +++- src/interactive/system.rs | 1 - src/interactive/transfer.rs | 78 ++- src/interactive/transfer_preview.rs | 32 +- src/interactive/tx.rs | 78 ++- src/interactive/wallet.rs | 142 ++++- src/main.rs | 2 +- src/types/contacts.rs | 15 +- src/types/transaction.rs | 1 - src/types/wallet.rs | 88 ++- src/utils/api_validator.rs | 129 +++++ src/utils/constants.rs | 5 +- src/utils/eth.rs | 34 +- src/utils/helper.rs | 3 +- src/utils/mod.rs | 4 + src/utils/network.rs | 51 ++ src/utils/secure_fs.rs | 43 ++ src/utils/terminal.rs | 15 +- 34 files changed, 1659 insertions(+), 912 deletions(-) delete mode 100644 src/interactive/contract.rs create mode 100644 src/utils/api_validator.rs create mode 100644 src/utils/network.rs create mode 100644 src/utils/secure_fs.rs diff --git a/Cargo.lock b/Cargo.lock index 5812f82..a70917d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,21 +2,22 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - [[package]] name = "adler2" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array 0.14.9", +] + [[package]] name = "aes" version = "0.8.4" @@ -28,6 +29,20 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.8.12" @@ -35,7 +50,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", - "getrandom 0.3.3", + "getrandom 0.3.4", "once_cell", "version_check", "zerocopy", @@ -96,7 +111,7 @@ version = "0.1.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28e2652684758b0d9b389d248b209ed9fd9989ef489a550265fe4bb8454fe7eb" dependencies = [ - "alloy-primitives 0.8.25", + "alloy-primitives 0.8.26", "num_enum", "strum 0.27.2", ] @@ -108,7 +123,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae09ffd7c29062431dd86061deefe4e3c6f07fa0d674930095f8dcedb0baf02c" dependencies = [ "alloy-eips", - "alloy-primitives 0.8.25", + "alloy-primitives 0.8.26", "alloy-rlp", "alloy-serde", "auto_impl", @@ -128,7 +143,7 @@ dependencies = [ "alloy-json-abi", "alloy-network", "alloy-network-primitives", - "alloy-primitives 0.8.25", + "alloy-primitives 0.8.26", "alloy-provider", "alloy-pubsub", "alloy-rpc-types-eth", @@ -141,25 +156,25 @@ dependencies = [ [[package]] name = "alloy-core" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d8bcce99ad10fe02640cfaec1c6bc809b837c783c1d52906aa5af66e2a196f6" +checksum = "05f1ab91967646311bb7dd32db4fee380c69fe624319dcd176b89fb2a420c6b5" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", - "alloy-primitives 0.8.25", + "alloy-primitives 0.8.26", "alloy-rlp", "alloy-sol-types", ] [[package]] name = "alloy-dyn-abi" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb8e762aefd39a397ff485bc86df673465c4ad3ec8819cc60833a8a3ba5cdc87" +checksum = "cf69d3061e2e908a4370bda5d8d6529d5080232776975489eec0b49ce971027e" dependencies = [ "alloy-json-abi", - "alloy-primitives 0.8.25", + "alloy-primitives 0.8.26", "alloy-sol-type-parser", "alloy-sol-types", "const-hex", @@ -175,7 +190,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0069cf0642457f87a01a014f6dc29d5d893cd4fd8fddf0c3cdfad1bb3ebafc41" dependencies = [ - "alloy-primitives 0.8.25", + "alloy-primitives 0.8.26", "alloy-rlp", "serde", ] @@ -186,7 +201,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c986539255fb839d1533c128e190e557e52ff652c9ef62939e233a81dd93f7e" dependencies = [ - "alloy-primitives 0.8.25", + "alloy-primitives 0.8.26", "alloy-rlp", "derive_more 1.0.0", "k256", @@ -201,7 +216,7 @@ checksum = "5b6aa3961694b30ba53d41006131a2fca3bdab22e4c344e46db2c639e7c2dfdd" dependencies = [ "alloy-eip2930", "alloy-eip7702", - "alloy-primitives 0.8.25", + "alloy-primitives 0.8.26", "alloy-rlp", "alloy-serde", "c-kzg", @@ -217,18 +232,18 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e53f7877ded3921d18a0a9556d55bedf84535567198c9edab2aa23106da91855" dependencies = [ - "alloy-primitives 0.8.25", + "alloy-primitives 0.8.26", "alloy-serde", "serde", ] [[package]] name = "alloy-json-abi" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe6beff64ad0aa6ad1019a3db26fef565aefeb011736150ab73ed3366c3cfd1b" +checksum = "4584e3641181ff073e9d5bec5b3b8f78f9749d9fb108a1cfbc4399a4a139c72a" dependencies = [ - "alloy-primitives 0.8.25", + "alloy-primitives 0.8.26", "alloy-sol-type-parser", "serde", "serde_json", @@ -240,7 +255,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3694b7e480728c0b3e228384f223937f14c10caef5a4c766021190fc8f283d35" dependencies = [ - "alloy-primitives 0.8.25", + "alloy-primitives 0.8.26", "alloy-sol-types", "serde", "serde_json", @@ -258,7 +273,7 @@ dependencies = [ "alloy-eips", "alloy-json-rpc", "alloy-network-primitives", - "alloy-primitives 0.8.25", + "alloy-primitives 0.8.26", "alloy-rpc-types-eth", "alloy-serde", "alloy-signer", @@ -279,7 +294,7 @@ checksum = "df9f3e281005943944d15ee8491534a1c7b3cbf7a7de26f8c433b842b93eb5f9" dependencies = [ "alloy-consensus", "alloy-eips", - "alloy-primitives 0.8.25", + "alloy-primitives 0.8.26", "alloy-serde", "serde", ] @@ -308,9 +323,9 @@ dependencies = [ [[package]] name = "alloy-primitives" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c77490fe91a0ce933a1f219029521f20fc28c2c0ca95d53fa4da9c00b8d9d4e" +checksum = "777d58b30eb9a4db0e5f59bc30e8c2caef877fee7dc8734cf242a51a60f22e05" dependencies = [ "alloy-rlp", "bytes", @@ -345,7 +360,7 @@ dependencies = [ "alloy-json-rpc", "alloy-network", "alloy-network-primitives", - "alloy-primitives 0.8.25", + "alloy-primitives 0.8.26", "alloy-pubsub", "alloy-rpc-client", "alloy-rpc-types-eth", @@ -380,7 +395,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90f1f34232f77341076541c405482e4ae12f0ee7153d8f9969fc1691201b2247" dependencies = [ "alloy-json-rpc", - "alloy-primitives 0.8.25", + "alloy-primitives 0.8.26", "alloy-transport", "bimap", "futures", @@ -411,7 +426,7 @@ checksum = "64b728d511962dda67c1bc7ea7c03736ec275ed2cf4c35d9585298ac9ccf3b73" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -421,7 +436,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "374dbe0dc3abdc2c964f36b3d3edf9cdb3db29d16bda34aa123f03d810bec1dd" dependencies = [ "alloy-json-rpc", - "alloy-primitives 0.8.25", + "alloy-primitives 0.8.26", "alloy-pubsub", "alloy-transport", "alloy-transport-http", @@ -446,7 +461,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c74832aa474b670309c20fffc2a869fa141edab7c79ff7963fad0a08de60bae1" dependencies = [ - "alloy-primitives 0.8.25", + "alloy-primitives 0.8.26", "alloy-rpc-types-engine", "alloy-rpc-types-eth", "alloy-serde", @@ -461,7 +476,7 @@ checksum = "3f56294dce86af23ad6ee8df46cf8b0d292eb5d1ff67dc88a0886051e32b1faf" dependencies = [ "alloy-consensus", "alloy-eips", - "alloy-primitives 0.8.25", + "alloy-primitives 0.8.26", "alloy-rlp", "alloy-serde", "derive_more 1.0.0", @@ -480,7 +495,7 @@ dependencies = [ "alloy-consensus", "alloy-eips", "alloy-network-primitives", - "alloy-primitives 0.8.25", + "alloy-primitives 0.8.26", "alloy-rlp", "alloy-serde", "alloy-sol-types", @@ -496,7 +511,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dfa4a7ccf15b2492bb68088692481fd6b2604ccbee1d0d6c44c21427ae4df83" dependencies = [ - "alloy-primitives 0.8.25", + "alloy-primitives 0.8.26", "serde", "serde_json", ] @@ -507,7 +522,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e10aec39d60dc27edcac447302c7803d2371946fb737245320a05b78eb2fafd" dependencies = [ - "alloy-primitives 0.8.25", + "alloy-primitives 0.8.26", "async-trait", "auto_impl", "elliptic-curve", @@ -523,7 +538,7 @@ checksum = "d8396f6dff60700bc1d215ee03d86ff56de268af96e2bf833a14d0bafcab9882" dependencies = [ "alloy-consensus", "alloy-network", - "alloy-primitives 0.8.25", + "alloy-primitives 0.8.26", "alloy-signer", "async-trait", "k256", @@ -533,23 +548,23 @@ dependencies = [ [[package]] name = "alloy-sol-macro" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10ae8e9a91d328ae954c22542415303919aabe976fe7a92eb06db1b68fd59f2" +checksum = "e68b32b6fa0d09bb74b4cefe35ccc8269d711c26629bc7cd98a47eeb12fe353f" dependencies = [ "alloy-sol-macro-expander", "alloy-sol-macro-input", "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] name = "alloy-sol-macro-expander" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83ad5da86c127751bc607c174d6c9fe9b85ef0889a9ca0c641735d77d4f98f26" +checksum = "2afe6879ac373e58fd53581636f2cce843998ae0b058ebe1e4f649195e2bd23c" dependencies = [ "alloy-json-abi", "alloy-sol-macro-input", @@ -559,16 +574,16 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", "syn-solidity", "tiny-keccak", ] [[package]] name = "alloy-sol-macro-input" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3d30f0d3f9ba3b7686f3ff1de9ee312647aac705604417a2f40c604f409a9e" +checksum = "c3ba01aee235a8c699d07e5be97ba215607564e71be72f433665329bec307d28" dependencies = [ "alloy-json-abi", "const-hex", @@ -578,15 +593,15 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn 2.0.106", + "syn 2.0.107", "syn-solidity", ] [[package]] name = "alloy-sol-type-parser" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d162f8524adfdfb0e4bd0505c734c985f3e2474eb022af32eef0d52a4f3935c" +checksum = "4c13fc168b97411e04465f03e632f31ef94cad1c7c8951bf799237fd7870d535" dependencies = [ "serde", "winnow", @@ -594,12 +609,12 @@ dependencies = [ [[package]] name = "alloy-sol-types" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d43d5e60466a440230c07761aa67671d4719d46f43be8ea6e7ed334d8db4a9ab" +checksum = "6e960c4b52508ef2ae1e37cae5058e905e9ae099b107900067a503f8c454036f" dependencies = [ "alloy-json-abi", - "alloy-primitives 0.8.25", + "alloy-primitives 0.8.26", "alloy-sol-macro", "const-hex", "serde", @@ -688,9 +703,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.20" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -703,9 +718,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" @@ -756,7 +771,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -844,7 +859,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" dependencies = [ "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -882,7 +897,7 @@ dependencies = [ "num-traits", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -973,7 +988,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -984,7 +999,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -1012,7 +1027,7 @@ checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -1044,21 +1059,6 @@ dependencies = [ "arrayvec", ] -[[package]] -name = "backtrace" -version = "0.3.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", -] - [[package]] name = "base16ct" version = "0.2.0" @@ -1106,9 +1106,9 @@ checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "bitstream-io" @@ -1134,7 +1134,7 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array", + "generic-array 0.14.9", ] [[package]] @@ -1143,7 +1143,7 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" dependencies = [ - "generic-array", + "generic-array 0.14.9", ] [[package]] @@ -1178,9 +1178,9 @@ checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" [[package]] name = "bytemuck" -version = "1.23.2" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" [[package]] name = "byteorder" @@ -1229,9 +1229,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.38" +version = "1.2.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80f41ae168f955c12fb8960b057d70d0ca153fb83182b57d86380443527be7e9" +checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" dependencies = [ "find-msvc-tools", "jobserver", @@ -1251,9 +1251,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -1272,7 +1272,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -1287,9 +1287,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.48" +version = "4.5.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" +checksum = "0c2cfd7bf8a6017ddaa4e32ffe7403d547790db06bd171c1c53926faab501623" dependencies = [ "clap_builder", "clap_derive", @@ -1297,9 +1297,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.48" +version = "4.5.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" +checksum = "0a4c05b9e80c5ccd3a7ef080ad7b6ba7d6fc00a985b8b157197075677c82c7a0" dependencies = [ "anstream", "anstyle", @@ -1309,21 +1309,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.47" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] name = "clap_lex" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "clearscreen" @@ -1333,7 +1333,7 @@ checksum = "85a8ab73a1c02b0c15597b22e09c7dc36e63b2f601f9d1e83ac0c3decd38b1ae" dependencies = [ "nix", "terminfo", - "thiserror 2.0.16", + "thiserror 2.0.17", "which", "windows-sys 0.59.0", ] @@ -1368,15 +1368,15 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "unicode-width 0.2.1", + "unicode-width 0.2.2", "windows-sys 0.59.0", ] [[package]] name = "const-hex" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6407bff74dea37e0fa3dc1c1c974e5d46405f0c987bf9997a0762adce71eda6" +checksum = "3bb320cac8a0750d7f25280aa97b09c26edfe161164238ecbbb31092b079e735" dependencies = [ "cfg-if", "cpufeatures", @@ -1392,9 +1392,9 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "const_format" -version = "0.2.34" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" dependencies = [ "const_format_proc_macros", ] @@ -1523,7 +1523,7 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ - "generic-array", + "generic-array 0.14.9", "rand_core 0.6.4", "subtle", "zeroize", @@ -1535,27 +1535,28 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ - "generic-array", + "generic-array 0.14.9", + "rand_core 0.6.4", "typenum", ] [[package]] name = "csv" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" dependencies = [ "csv-core", "itoa", "ryu", - "serde", + "serde_core", ] [[package]] name = "csv-core" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" dependencies = [ "memchr", ] @@ -1629,7 +1630,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version 0.4.1", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -1658,7 +1659,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", "unicode-xid", ] @@ -1671,7 +1672,7 @@ dependencies = [ "convert_case 0.7.1", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", "unicode-xid", ] @@ -1695,7 +1696,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array", + "generic-array 0.14.9", ] [[package]] @@ -1738,7 +1739,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] @@ -1760,7 +1761,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -1779,10 +1780,10 @@ dependencies = [ ] [[package]] -name = "dotenv" -version = "0.15.0" +name = "dotenvy" +version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] name = "dunce" @@ -1819,7 +1820,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -1838,7 +1839,7 @@ dependencies = [ "crypto-bigint", "digest 0.10.7", "ff", - "generic-array", + "generic-array 0.14.9", "group", "pkcs8", "rand_core 0.6.4", @@ -1879,14 +1880,14 @@ checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] name = "env_filter" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" dependencies = [ "log", "regex", @@ -1928,7 +1929,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -1944,7 +1945,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] @@ -2029,7 +2030,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -2053,9 +2054,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" [[package]] name = "fixed-hash" @@ -2071,9 +2072,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" dependencies = [ "crc32fast", "miniz_oxide", @@ -2177,7 +2178,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -2227,15 +2228,25 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.7" +version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" dependencies = [ "typenum", "version_check", "zeroize", ] +[[package]] +name = "generic-array" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985a5578ebdb02351d484a77fb27e7cb79272f1ba9bc24692d8243c3cfe40660" +dependencies = [ + "rustversion", + "typenum", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -2245,24 +2256,34 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "js-sys", "libc", "r-efi", - "wasi 0.14.7+wasi-0.2.4", + "wasip2", "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gif" version = "0.13.3" @@ -2273,12 +2294,6 @@ dependencies = [ "weezl", ] -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - [[package]] name = "glob" version = "0.3.3" @@ -2317,12 +2332,13 @@ dependencies = [ [[package]] name = "half" -version = "2.6.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", + "zerocopy", ] [[package]] @@ -2464,7 +2480,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.2", + "webpki-roots 1.0.3", ] [[package]] @@ -2697,14 +2713,14 @@ checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] name = "indexmap" -version = "2.11.4" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", "hashbrown 0.16.0", @@ -2719,7 +2735,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ "block-padding", - "generic-array", + "generic-array 0.14.9", ] [[package]] @@ -2733,7 +2749,7 @@ dependencies = [ "dyn-clone", "fuzzy-matcher", "unicode-segmentation", - "unicode-width 0.2.1", + "unicode-width 0.2.2", ] [[package]] @@ -2744,7 +2760,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -2762,17 +2778,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "io-uring" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" -dependencies = [ - "bitflags", - "cfg-if", - "libc", -] - [[package]] name = "ipnet" version = "2.11.0" @@ -2802,9 +2807,9 @@ dependencies = [ [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" @@ -2860,7 +2865,7 @@ checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -2869,7 +2874,7 @@ version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "libc", ] @@ -2945,9 +2950,9 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "libc" -version = "0.2.176" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libfuzzer-sys" @@ -2995,11 +3000,10 @@ checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] @@ -3041,7 +3045,7 @@ checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -3056,9 +3060,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.5" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "mime" @@ -3090,15 +3094,15 @@ checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "log", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "windows-sys 0.59.0", ] [[package]] name = "moxcms" -version = "0.7.5" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd32fa8935aeadb8a8a6b6b351e40225570a37c43de67690383d87ef170cd08" +checksum = "c588e11a3082784af229e23e8e4ecf5bcc6fbe4f69101e0421ce8d79da7f0b40" dependencies = [ "num-traits", "pxfm", @@ -3179,7 +3183,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -3224,9 +3228,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" dependencies = [ "num_enum_derive", "rustversion", @@ -3234,22 +3238,13 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", -] - -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", + "syn 2.0.107", ] [[package]] @@ -3260,15 +3255,21 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.73" +version = "0.10.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654" dependencies = [ "bitflags", "cfg-if", @@ -3287,7 +3288,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -3298,9 +3299,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.109" +version = "0.9.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2" dependencies = [ "cc", "libc", @@ -3339,14 +3340,14 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -3354,15 +3355,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] [[package]] @@ -3403,12 +3404,12 @@ dependencies = [ [[package]] name = "pem" -version = "3.0.5" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ "base64", - "serde", + "serde_core", ] [[package]] @@ -3419,12 +3420,11 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8" +checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4" dependencies = [ "memchr", - "thiserror 2.0.16", "ucd-trie", ] @@ -3493,7 +3493,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -3537,6 +3537,18 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "portable-atomic" version = "1.11.1" @@ -3607,7 +3619,7 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit 0.23.6", + "toml_edit 0.23.7", ] [[package]] @@ -3629,7 +3641,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -3657,7 +3669,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" dependencies = [ "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -3682,9 +3694,9 @@ dependencies = [ [[package]] name = "pxfm" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83f9b339b02259ada5c0f4a389b7fb472f933aa17ce176fd2ad98f28bb401fde" +checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84" dependencies = [ "num-traits", ] @@ -3733,7 +3745,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tracing", "web-time", @@ -3746,7 +3758,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ "bytes", - "getrandom 0.3.3", + "getrandom 0.3.4", "lru-slab", "rand 0.9.2", "ring", @@ -3754,7 +3766,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.16", + "thiserror 2.0.17", "tinyvec", "tracing", "web-time", @@ -3776,9 +3788,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.40" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] @@ -3852,7 +3864,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", ] [[package]] @@ -3942,9 +3954,9 @@ checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags", ] @@ -3968,14 +3980,14 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] name = "regex" -version = "1.11.2" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", @@ -3985,9 +3997,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.10" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", @@ -3996,15 +4008,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" -version = "0.12.23" +version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ "base64", "bytes", @@ -4041,7 +4053,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.2", + "webpki-roots 1.0.3", ] [[package]] @@ -4100,6 +4112,7 @@ name = "rsk-rust-cli" version = "0.1.0" dependencies = [ "aes", + "aes-gcm", "alloy", "alloy-consensus", "alloy-contract", @@ -4122,10 +4135,10 @@ dependencies = [ "csv", "dialoguer", "dirs", - "dotenv", + "dotenvy", "env_logger", "eth-keystore", - "generic-array", + "generic-array 1.3.4", "hex", "image", "inquire", @@ -4141,9 +4154,9 @@ dependencies = [ "serde_json", "sha3", "tempfile", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", - "toml 0.9.7", + "toml 0.9.8", "typenum", "url", "zeroize", @@ -4193,12 +4206,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" -[[package]] -name = "rustc-demangle" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -4239,14 +4246,14 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.32" +version = "0.23.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" +checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7" dependencies = [ "once_cell", "ring", @@ -4268,9 +4275,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.6" +version = "0.103.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8572f3c2cb9934231157b45499fc41e1f58c589fdfb81a844ba873265e80f8eb" +checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" dependencies = [ "ring", "rustls-pki-types", @@ -4285,9 +4292,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rusty-fork" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" dependencies = [ "fnv", "quick-error 1.2.3", @@ -4316,7 +4323,7 @@ version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] @@ -4368,7 +4375,7 @@ checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ "base16ct", "der", - "generic-array", + "generic-array 0.14.9", "pkcs8", "subtle", "zeroize", @@ -4429,9 +4436,9 @@ checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" [[package]] name = "serde" -version = "1.0.226" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", @@ -4439,22 +4446,22 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.226" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.226" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -4481,9 +4488,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee" +checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" dependencies = [ "serde_core", ] @@ -4617,7 +4624,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.16", + "thiserror 2.0.17", "time", ] @@ -4641,12 +4648,12 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -4661,9 +4668,9 @@ dependencies = [ [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "static_assertions" @@ -4705,7 +4712,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -4717,7 +4724,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -4739,9 +4746,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "2a26dbd934e5451d21ef060c018dae56fc073894c5a7896f882928a76e6d081b" dependencies = [ "proc-macro2", "quote", @@ -4750,14 +4757,14 @@ dependencies = [ [[package]] name = "syn-solidity" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4560533fbd6914b94a8fb5cc803ed6801c3455668db3b810702c57612bac9412" +checksum = "ab4e6eed052a117409a1a744c8bda9c3ea6934597cf7419f791cb7d590871c4c" dependencies = [ "paste", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -4777,7 +4784,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -4833,10 +4840,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] @@ -4873,11 +4880,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.16", + "thiserror-impl 2.0.17", ] [[package]] @@ -4888,18 +4895,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] name = "thiserror-impl" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -5001,33 +5008,30 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.47.1" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "slab", "socket2", "tokio-macros", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -5042,9 +5046,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.3" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f63835928ca123f1bef57abbcd23bb2ba0ac9ae1235f1e65bda0d06e7786bd" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", @@ -5105,14 +5109,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" dependencies = [ "indexmap", "serde_core", - "serde_spanned 1.0.2", - "toml_datetime 0.7.2", + "serde_spanned 1.0.3", + "toml_datetime 0.7.3", "toml_parser", "toml_writer", "winnow", @@ -5129,9 +5133,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" dependencies = [ "serde_core", ] @@ -5151,30 +5155,30 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.6" +version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ "indexmap", - "toml_datetime 0.7.2", + "toml_datetime 0.7.3", "toml_parser", "winnow", ] [[package]] name = "toml_parser" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109" +checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" [[package]] name = "tower" @@ -5240,7 +5244,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -5280,9 +5284,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "ucd-trie" @@ -5310,9 +5314,9 @@ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicode-ident" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" [[package]] name = "unicode-segmentation" @@ -5328,9 +5332,9 @@ checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-width" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "unicode-xid" @@ -5338,6 +5342,16 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -5443,15 +5457,6 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasi" -version = "0.14.7+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" -dependencies = [ - "wasip2", -] - [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" @@ -5484,7 +5489,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", "wasm-bindgen-shared", ] @@ -5519,7 +5524,7 @@ checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5573,14 +5578,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.2", + "webpki-roots 1.0.3", ] [[package]] name = "webpki-roots" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8" dependencies = [ "rustls-pki-types", ] @@ -5604,9 +5609,9 @@ dependencies = [ [[package]] name = "widestring" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" [[package]] name = "winapi" @@ -5632,37 +5637,37 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.62.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.2.0", - "windows-result 0.4.0", - "windows-strings 0.5.0", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -5673,9 +5678,9 @@ checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-link" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-registry" @@ -5699,11 +5704,11 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -5717,11 +5722,11 @@ dependencies = [ [[package]] name = "windows-strings" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -5748,16 +5753,16 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.3", + "windows-targets 0.53.5", ] [[package]] name = "windows-sys" -version = "0.61.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -5778,19 +5783,19 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.3" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link 0.1.3", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -5801,9 +5806,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -5813,9 +5818,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -5825,9 +5830,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -5837,9 +5842,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -5849,9 +5854,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -5861,9 +5866,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -5873,9 +5878,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -5885,9 +5890,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" @@ -5929,7 +5934,7 @@ dependencies = [ "pharos", "rustc_version 0.4.1", "send_wrapper", - "thiserror 2.0.16", + "thiserror 2.0.17", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -5964,7 +5969,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", "synstructure", ] @@ -5985,7 +5990,7 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -6005,15 +6010,15 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" dependencies = [ "zeroize_derive", ] @@ -6026,7 +6031,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -6059,7 +6064,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 5092f5e..a66de2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,21 +14,21 @@ sha3 = "0.10.8" tokio = { version = "1.45.1", features = ["full"] } zeroize = "1.8.1" # Alloy dependencies - successor to ethers-rs with security fixes -alloy = { version = "0.6", features = ["full", "provider-http", "signer-local", "contract", "rpc-types", "consensus"] } -alloy-provider = "0.6" -alloy-signer = "0.6" -alloy-signer-local = "0.6" -alloy-contract = "0.6" -alloy-primitives = "0.6" -alloy-rpc-types = "0.6" -alloy-transport-http = "0.6" -alloy-consensus = "0.6" +alloy = { version = "0.6.4", features = ["full", "provider-http", "signer-local", "contract", "rpc-types", "consensus"] } +alloy-provider = "0.6.4" +alloy-signer = "0.6.4" +alloy-signer-local = "0.6.4" +alloy-contract = "0.6.4" +alloy-primitives = "0.6.4" +alloy-rpc-types = "0.6.4" +alloy-transport-http = "0.6.4" +alloy-consensus = "0.6.4" thiserror = "2.0.12" clap = { version = "4.5.36", features = ["derive"] } k256 = "0.13.4" env_logger = "0.11.8" log = "0.4.28" -dotenv = "0.15.0" +dotenvy = "0.15.7" reqwest = { version = "0.12.15", features = ["json", "rustls-tls"] } colored = "3.0.0" tempfile = "3.20.0" @@ -41,13 +41,14 @@ dirs = "6.0.0" toml = "0.9.7" url = "2.5.7" aes = "0.8.4" +aes-gcm = "0.10.3" scrypt = "0.11.0" base64 = "0.22.1" rpassword = "7.4.0" cbc = "0.1.2" cipher = "0.4.4" typenum = "1.18.0" -generic-array = "0.14.7" +generic-array = "1.0" dialoguer = { version = "0.11", features = ["fuzzy-select"] } console = "0.15" clearscreen = "4.0.2" diff --git a/README.MD b/README.MD index 2304679..a833c38 100644 --- a/README.MD +++ b/README.MD @@ -46,8 +46,9 @@ This tool provides comprehensive functionality to connect to an Ethereum-compati - Switch between Mainnet and Testnet - Configure custom RPC endpoints -- Manage API keys for services +- Manage API keys with validation for Rootstock networks - View network status and connection details +- Offline functionality for wallet and contact management ## 🛠️ Installation @@ -96,56 +97,48 @@ The wallet features an intuitive interactive interface. Simply run: cargo run ``` -## Demo +### 📋 Individual Feature Demos -### First Time Setup - -[![First Time Setup](https://asciinema.org/a/V8Qf1AboSZu08l12KJFxTfyeV.svg)](https://asciinema.org/a/V8Qf1AboSZu08l12KJFxTfyeV) - -### Importing Existing Wallet - -[![Importing Existing Wallet](https://asciinema.org/a/cVrvEOP4LvJQLvTitxcayRBFt.svg)](https://asciinema.org/a/cVrvEOP4LvJQLvTitxcayRBFt) +For detailed walkthroughs of specific features: -### Wallet Creation - -[![Wallet Creation](https://asciinema.org/a/Lj7YVm8idPbAEpHYPfjFuTfns.svg)](https://asciinema.org/a/Lj7YVm8idPbAEpHYPfjFuTfns) - -### List Wallets & Switch Wallets +### First Time Setup -[![List Wallets](https://asciinema.org/a/HUpONPUC4OgQKFyStNm5KwaqT.svg)](https://asciinema.org/a/HUpONPUC4OgQKFyStNm5KwaqT) +[![First Time Setup](https://asciinema.org/a/lbHMriiOt5ZNfpyP3LnjCRJ6e.svg)]( https://asciinema.org/a/lbHMriiOt5ZNfpyP3LnjCRJ6e) -### Delete Wallet & Rename Wallet +### Wallet Features -[![Delete Wallet](https://asciinema.org/a/cqjxdJjfaY1wqvO4upgRUMKDG.svg)](https://asciinema.org/a/cqjxdJjfaY1wqvO4upgRUMKDG) +Create Wallet, Importing Existing Wallet, List Wallets, Switch Wallets, Delete Wallet & Rename Wallet +[![Importing Existing Wallet](https://asciinema.org/a/BoPr2TFlggYem2b1zhhEzS52X.svg)](https://asciinema.org/a/BoPr2TFlggYem2b1zhhEzS52X) ### Check Balance of RBTC and ERC-20 Tokens Check your RBTC and ERC-20 token balances (e.g., RIF). -[View example transaction on explorer](https://explorer.testnet.rootstock.io/tx/0x0293c59578303f3dc88daeda6c8564fd39b612dd85d7a1e025a37e611dc5b900) -[![Check Balance](https://asciinema.org/a/rqlxYIWZ2Gh1Pn5sEFZZFK5xz.svg)](https://asciinema.org/a/rqlxYIWZ2Gh1Pn5sEFZZFK5xz +[![Check Balance](https://asciinema.org/a/ChSOgxrI2VXuA5Yq6Q0Jgc2nI.svg)](https://asciinema.org/a/ChSOgxrI2VXuA5Yq6Q0Jgc2nI ) ### Send Funds Transfer RBTC from one wallet to another (e.g., from personal to lock wallet). -[![Send Funds](https://asciinema.org/a/tEt4dSOgmEEguP6pgQ7setuPV.svg)](https://asciinema.org/a/tEt4dSOgmEEguP6pgQ7setuPV +[![Send Funds](https://asciinema.org/a/dZTXi9OFBD2npbMpjfA68INX3.svg)](https://asciinema.org/a/dZTXi9OFBD2npbMpjfA68INX3 +) +[View example transaction on explorer](https://explorer.testnet.rootstock.io/tx/c77c5bf53d1492bcf88104a746ca7a21f65add08f92dc0f9cad35db0a97886b0 ) ### Set API Key -Configure your API key for accessing transaction history and status checking. -[![Set API Key](https://asciinema.org/a/6ZdWMvEMMZVsSCFkNHjq3MbPq.svg)](https://asciinema.org/a/6ZdWMvEMMZVsSCFkNHjq3MbPq) +Configure and validate your API key for accessing transaction history and status checking. +[![Set API Key](https://asciinema.org/a/LPdZYLNWcCK0feJyvcXDFZSXn.svg)](https://asciinema.org/a/LPdZYLNWcCK0feJyvcXDFZSXn) -### Transaction History +### Transaction History(make sure alchemy api key is set) [![Set API Key](https://asciinema.org/a/Sh7qW67bHkDz0KGSjiqBLc8JC.svg)](https://asciinema.org/a/Sh7qW67bHkDz0KGSjiqBLc8JC) ### Check Transaction Status -[![Check Transaction Status](https://asciinema.org/a/CYYjrSV58KRGMgOmbdknm85Am.svg)](https://asciinema.org/a/CYYjrSV58KRGMgOmbdknm85Am) +[![Check Transaction Status](https://asciinema.org/a/jwQkdPRDSaUx8WC61ZNb8RnJ5.svg)](https://asciinema.org/a/jwQkdPRDSaUx8WC61ZNb8RnJ5) -You can also view it at [View Transaction](https://explorer.testnet.rsk.co/tx/0293c59578303f3dc88daeda6c8564fd39b612dd85d7a1e025a37e611dc5b900) +You can also view it at [View Transaction](https://explorer.testnet.rsk.co/tx/c77c5bf53d1492bcf88104a746ca7a21f65add08f92dc0f9cad35db0a97886b0) ### Bulk Transfer @@ -157,12 +150,12 @@ Send multiple transactions at once. ### Token Management Add, list, and delete tokens from your wallet. -[![Add Token](https://asciinema.org/a/dY7GpSsmk6uxsZdB9n6C2Fzgs.svg)](https://asciinema.org/a/dY7GpSsmk6uxsZdB9n6C2Fzgs) +[![Add Token](https://asciinema.org/a/H4vFOowEHUDBi9txo8PGntedp.svg)](https://asciinema.org/a/H4vFOowEHUDBi9txo8PGntedp) ### Contact Add, list, and delete contacts from your address book. -[![Add Contact](https://asciinema.org/a/QaN1SZIXud4dxJ8woqDqnb1v5.svg)](https://asciinema.org/a/QaN1SZIXud4dxJ8woqDqnb1v5) +[![Add Contact](https://asciinema.org/a/q7aRPz3nUxuwopSSUFTZlNmxj.svg)](https://asciinema.org/a/q7aRPz3nUxuwopSSUFTZlNmxj) ## 🔍 Troubleshooting @@ -177,6 +170,14 @@ Add, list, and delete contacts from your address book. #### Connection Issues - Verify your internet connection +- App automatically switches to offline mode when network unavailable +- Limited functionality available offline (wallet management, contacts, tokens) + +#### API Key Issues + +- Ensure API key is valid for the selected network (mainnet/testnet) +- Keys are validated against Rootstock RPC endpoints +- Invalid keys will show clear error messages #### Wallet Issues @@ -192,6 +193,20 @@ Add, list, and delete contacts from your address book. - [RSK Developer Portal](https://developers.rsk.co/) - [Issue Tracker](https://github.com/rsksmart/rsk-rust-cli/issues) +## Security + +This application handles private keys and financial transactions. Security measures include: + +- **Encryption**: AES-256-GCM encryption for private keys with authenticated encryption +- **File Security**: Secure file permissions (0o600) for wallet files +- **Password Security**: Strong password validation with complexity requirements +- **Memory Security**: Memory zeroization for sensitive data to prevent leaks +- **Input Validation**: Comprehensive input validation and sanitization +- **API Security**: API key validation against Rootstock networks +- **Offline Security**: Core wallet functions available without network exposure + +**Dependency Status:** All security vulnerabilities resolved. Minor unmaintained dependency warnings (derivative, paste) are transitive dependencies via Alloy and pose no security risk. + ## Contributing We welcome contributions from the community. Please fork the repository and submit pull requests with your changes. Ensure your code adheres to the project's main objective. diff --git a/src/api/mod.rs b/src/api/mod.rs index be132cd..6aa505a 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -23,11 +23,14 @@ impl fmt::Display for ApiProvider { } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, zeroize::Zeroize)] pub struct ApiKey { pub key: String, + #[zeroize(skip)] pub network: String, // "mainnet", "testnet", etc. + #[zeroize(skip)] pub provider: ApiProvider, + #[zeroize(skip)] pub name: Option, } diff --git a/src/commands/api.rs b/src/commands/api.rs index 6a38d18..6b10b9f 100644 --- a/src/commands/api.rs +++ b/src/commands/api.rs @@ -1,4 +1,7 @@ +use crate::api::{ApiKey, ApiProvider}; +use crate::config::ConfigManager; use crate::types::wallet::WalletData; +use crate::utils::api_validator::{validate_api_key, validate_api_key_format, ValidationResult}; use crate::utils::constants; use anyhow::Result; use clap::Parser; @@ -7,24 +10,77 @@ use std::fs; #[derive(Parser, Debug)] pub struct SetApiKeyCommand { - /// Alchemy API key to set + /// API key to set #[arg(long, required = true)] pub api_key: String, } impl SetApiKeyCommand { pub async fn execute(&self) -> Result<()> { - let wallet_file = constants::wallet_file_path(); - let mut wallet_data = if wallet_file.exists() { - let data = fs::read_to_string(&wallet_file)?; - serde_json::from_str::(&data)? - } else { - WalletData::new() + // Get current network from config + let config = ConfigManager::new()?.load()?; + let network = config.default_network.to_string().to_lowercase(); + + // For now, assume RSK RPC provider (can be extended later) + let provider = ApiProvider::RskRpc; + + // Validate format first + if let Err(e) = validate_api_key_format(&provider, &self.api_key) { + println!("{}: {}", "Format Error".red().bold(), e); + return Ok(()); + } + + // Create API key for validation + let api_key = ApiKey { + key: self.api_key.clone(), + network: network.clone(), + provider: provider.clone(), + name: None, }; - wallet_data.api_key = Some(self.api_key.clone()); - fs::write(&wallet_file, serde_json::to_string_pretty(&wallet_data)?)?; - println!("{}: API key set successfully", "Success".green().bold()); + println!("🔍 Validating API key for {} {}...", provider, network); + + // Validate the key + match validate_api_key(&api_key).await? { + ValidationResult::Valid => { + println!("{}: API key is valid", "✅ Success".green().bold()); + + // Save the key + let wallet_file = constants::wallet_file_path(); + let mut wallet_data = if wallet_file.exists() { + let data = fs::read_to_string(&wallet_file)?; + serde_json::from_str::(&data)? + } else { + WalletData::new() + }; + + wallet_data.api_key = Some(self.api_key.clone()); + crate::utils::secure_fs::write_secure(&wallet_file, &serde_json::to_string_pretty(&wallet_data)?)?; + println!("{}: API key saved successfully", "💾 Saved".green().bold()); + } + ValidationResult::Invalid(reason) => { + println!("{}: {}", "❌ Invalid".red().bold(), reason); + println!("💡 Please check your API key and try again"); + } + ValidationResult::NetworkError(error) => { + println!("{}: {}", "⚠️ Network Error".yellow().bold(), error); + println!("💡 Saving key anyway - validation will retry when network is available"); + + // Save anyway for offline use + let wallet_file = constants::wallet_file_path(); + let mut wallet_data = if wallet_file.exists() { + let data = fs::read_to_string(&wallet_file)?; + serde_json::from_str::(&data)? + } else { + WalletData::new() + }; + + wallet_data.api_key = Some(self.api_key.clone()); + crate::utils::secure_fs::write_secure(&wallet_file, &serde_json::to_string_pretty(&wallet_data)?)?; + println!("{}: API key saved (unvalidated)", "💾 Saved".yellow().bold()); + } + } + Ok(()) } } diff --git a/src/commands/balance.rs b/src/commands/balance.rs index 830f73f..a56f91b 100644 --- a/src/commands/balance.rs +++ b/src/commands/balance.rs @@ -6,6 +6,7 @@ use crate::utils::table::TableBuilder; use anyhow::{Result, anyhow}; use clap::Parser; use alloy::primitives::Address; +use console; use std::fs; use std::str::FromStr; @@ -26,7 +27,19 @@ impl BalanceCommand { let config = ConfigManager::new()?.load()?; let network = config.default_network.to_string().to_lowercase(); - let (_config, eth_client) = Helper::init_eth_client(&network).await?; + // Try to initialize eth client with graceful failure handling + let eth_client_result = Helper::init_eth_client(&network).await; + let (_config, eth_client) = match eth_client_result { + Ok(result) => result, + Err(e) => { + eprintln!("{}", console::style("❌ Network Error").red().bold()); + eprintln!("{}", console::style(format!("Failed to connect to network: {}", e)).red()); + eprintln!("{}", console::style("💡 Try again when internet connection is available").yellow()); + + // Show offline wallet info instead + return self.show_offline_info(&config).await; + } + }; // Get address - use default wallet if none provided let address = if let Some(addr) = &self.address { @@ -48,30 +61,43 @@ impl BalanceCommand { default_wallet.address }; - let (balance, token_name) = if let Some(token) = &self.token { + // Try to get balance with network error handling + let balance_result = if let Some(token) = &self.token { // Check if it's the RBTC zero address if token == "0x0000000000000000000000000000000000000000" { - let balance = eth_client.get_balance(&address, &None).await?; - (balance, "RBTC".to_string()) + eth_client.get_balance(&address, &None).await + .map(|balance| (balance, "RBTC".to_string())) } else { let token_address = Address::from_str(token) .map_err(|_| anyhow!("Invalid token address format: {}", token))?; - let balance = eth_client - .get_balance(&address, &Some(token_address)) - .await?; - - // Try to get token info, but don't fail if we can't - let token_name = match eth_client.get_token_info(token_address).await { - Ok((_, symbol)) => symbol, - Err(_) => format!("Token (0x{})", &token[2..10]), - }; - - (balance, token_name) + + let balance_result = eth_client.get_balance(&address, &Some(token_address)).await; + match balance_result { + Ok(balance) => { + // Try to get token info, but don't fail if we can't + let token_name = match eth_client.get_token_info(token_address).await { + Ok((_, symbol)) => symbol, + Err(_) => format!("Token (0x{})", &token[2..10]), + }; + Ok((balance, token_name)) + } + Err(e) => Err(e) + } } } else { // Native RBTC balance - let balance = eth_client.get_balance(&address, &None).await?; - (balance, "RBTC".to_string()) + eth_client.get_balance(&address, &None).await + .map(|balance| (balance, "RBTC".to_string())) + }; + + let (balance, token_name) = match balance_result { + Ok(result) => result, + Err(e) => { + eprintln!("{}", console::style("❌ Balance Check Failed").red().bold()); + eprintln!("{}", console::style(format!("Error: {}", e)).red()); + eprintln!("{}", console::style("💡 Check your internet connection and try again").yellow()); + return self.show_offline_info(&config).await; + } }; // Format the balance with appropriate decimals @@ -92,4 +118,39 @@ impl BalanceCommand { table.print(); Ok(()) } + + /// Show offline wallet information when network is unavailable + async fn show_offline_info(&self, config: &crate::config::Config) -> Result<()> { + println!("\n{}", console::style("📱 Offline Mode - Wallet Information").cyan().bold()); + println!("{}", "=".repeat(45)); + + // Load wallet data + let wallet_file = constants::wallet_file_path(); + if !wallet_file.exists() { + return Err(anyhow!("No wallets found. Please create or import a wallet first.")); + } + + let data = fs::read_to_string(&wallet_file)?; + let wallet_data = serde_json::from_str::(&data)?; + + let address = if let Some(addr) = &self.address { + Address::from_str(addr).map_err(|_| anyhow!("Invalid address format: {}", addr))? + } else { + let default_wallet = wallet_data.get_current_wallet() + .ok_or_else(|| anyhow!("No default wallet selected."))?; + default_wallet.address + }; + + let mut table = TableBuilder::new(); + table.add_header(&["Address", "Network", "Status"]); + table.add_row(&[ + &Helper::format_address(&address), + &config.default_network.to_string(), + "Offline - Balance unavailable", + ]); + + table.print(); + println!("\n{}", console::style("💡 Connect to internet to check actual balance").dim()); + Ok(()) + } } diff --git a/src/commands/history.rs b/src/commands/history.rs index f5225cb..c92e6f6 100644 --- a/src/commands/history.rs +++ b/src/commands/history.rs @@ -1,7 +1,9 @@ use crate::types::transaction::{RskTransaction, TransactionStatus}; use crate::types::wallet::WalletData; use crate::utils::alchemy::AlchemyClient; +use crate::utils::api_validator::validate_api_key_format; use crate::utils::{constants, table::TableBuilder}; +use crate::api::ApiProvider; use anyhow::Result; use chrono::TimeZone; use clap::Parser; @@ -83,10 +85,10 @@ impl HistoryCommand { let mut stored_api_key: Option = None; // If export is requested, ensure we have a filename - if let Some(filename) = &self.export_csv - && !filename.ends_with(".csv") - { - return Err(anyhow::anyhow!("Export filename must end with .csv")); + if let Some(filename) = &self.export_csv { + if !filename.ends_with(".csv") { + return Err(anyhow::anyhow!("Export filename must end with .csv")); + } } // Try to load API key from wallet file @@ -99,9 +101,16 @@ impl HistoryCommand { // Persist CLI key if supplied and not yet saved if stored_api_key.is_none() && self.api_key.is_some() { - val["alchemyApiKey"] = serde_json::Value::String(self.api_key.clone().unwrap()); - fs::write(&wallet_file, serde_json::to_string_pretty(&val)?)?; - stored_api_key = self.api_key.clone(); + let api_key = self.api_key.clone().unwrap(); + + // Validate API key format before saving + if let Err(e) = validate_api_key_format(&ApiProvider::Alchemy, &api_key) { + return Err(anyhow::anyhow!("Invalid API key format: {}", e)); + } + + val["alchemyApiKey"] = serde_json::Value::String(api_key.clone()); + crate::utils::secure_fs::write_secure(&wallet_file, &serde_json::to_string_pretty(&val)?)?; + stored_api_key = Some(api_key); println!("{}", "Saved Alchemy API key ✅".green()); } } diff --git a/src/commands/transfer.rs b/src/commands/transfer.rs index d5aa5bb..a4c23df 100644 --- a/src/commands/transfer.rs +++ b/src/commands/transfer.rs @@ -11,6 +11,7 @@ use alloy::signers::local::PrivateKeySigner; use rpassword::prompt_password; use std::fs; use std::str::FromStr; +use zeroize::Zeroize; /// Result of a transfer operation #[derive(Debug)] @@ -34,7 +35,7 @@ pub struct TransferCommand { /// Amount to send (in tokens or RBTC) #[arg(long, required = true)] - pub value: f64, + pub value: String, /// Token address (for ERC20 transfers) #[arg(long)] @@ -60,8 +61,12 @@ impl TransferCommand { })?; // Prompt for password and decrypt private key - let password = prompt_password("Enter password for the default wallet: ")?; + let mut password = prompt_password("Enter password for the default wallet: ")?; let private_key = default_wallet.decrypt_private_key(&password)?; + + // Zeroize password after use + password.zeroize(); + let _local_wallet = PrivateKeySigner::from_str(&private_key) .map_err(|e| anyhow!("Failed to create PrivateKeySigner: {}", e))?; @@ -69,11 +74,12 @@ impl TransferCommand { let config = ConfigManager::new()?.load()?; // Create a new helper config with the private key + let mut private_key_copy = private_key.clone(); let client_config = HelperConfig { network: config.default_network.get_config(), wallet: crate::utils::helper::WalletConfig { current_wallet_address: None, - private_key: Some(private_key.clone()), + private_key: Some(private_key_copy.clone()), mnemonic: None, }, }; @@ -107,10 +113,10 @@ impl TransferCommand { (None, Some("RBTC".to_string())) }; - // Parse amount (convert f64 to wei or token units) + // Parse amount (convert string to wei or token units) // Both RBTC and tokens use 18 decimals let decimals = 18; - let amount = alloy::primitives::utils::parse_units(&self.value.to_string(), decimals) + let amount = alloy::primitives::utils::parse_units(&self.value, decimals) .map_err(|e| anyhow!("Invalid amount: {}", e))?; // Send transaction @@ -182,6 +188,9 @@ impl TransferCommand { status_str ); + // Zeroize sensitive data before returning + private_key_copy.zeroize(); + Ok(TransferResult { tx_hash, from: default_wallet.address(), diff --git a/src/commands/tx.rs b/src/commands/tx.rs index d6aecba..073b2d6 100644 --- a/src/commands/tx.rs +++ b/src/commands/tx.rs @@ -33,29 +33,38 @@ impl TxCommand { // Load config let config = ConfigManager::new()?.load()?; - // Get API key from config - let api_key = if let Some(key) = &self.api_key { - key.clone() + // Get API key and determine endpoint + let (api_key, url) = if let Some(key) = &self.api_key { + // Use provided API key with Alchemy + let alchemy_url = if self.testnet { + "https://rootstock-testnet.g.alchemy.com/v2" + } else { + "https://rootstock-mainnet.g.alchemy.com/v2" + }; + (key.clone(), alchemy_url.to_string()) + } else if let Some(rsk_key) = config.get_api_key(&ApiProvider::RskRpc) { + // Use RSK RPC endpoint + let rsk_url = if self.testnet { + "https://public-node.testnet.rsk.co" + } else { + "https://public-node.rsk.co" + }; + (rsk_key.to_string(), rsk_url.to_string()) + } else if let Some(alchemy_key) = config.get_api_key(&ApiProvider::Alchemy) { + // Fall back to Alchemy + let alchemy_url = if self.testnet { + "https://rootstock-testnet.g.alchemy.com/v2" + } else { + "https://rootstock-mainnet.g.alchemy.com/v2" + }; + (alchemy_key.to_string(), alchemy_url.to_string()) } else { - config - .get_api_key(&ApiProvider::Alchemy) - .ok_or_else(|| { - anyhow::anyhow!( - "No API key found for {}. Please set one up using 'wallet config'.", - network - ) - })? - .to_string() - }; - - let base_url = if self.testnet { - "https://rootstock-testnet.g.alchemy.com/v2" - } else { - "https://rootstock-mainnet.g.alchemy.com/v2" + anyhow::bail!( + "No API key found for {}. Please set up RSK RPC or Alchemy API key using 'wallet config'.", + network + ); }; - let url = base_url.to_string(); - // Get receipt first as it contains the status let receipt = self .get_transaction_receipt(&client, &url, &api_key, &self.tx_hash) @@ -87,10 +96,14 @@ impl TxCommand { "params": params }); - let response = client - .post(url) - .header("Authorization", format!("Bearer {}", api_key)) - .json(&request) + let mut request_builder = client.post(url).json(&request); + + // Add authorization header only for Alchemy endpoints + if url.contains("alchemy.com") { + request_builder = request_builder.header("Authorization", format!("Bearer {}", api_key)); + } + + let response = request_builder .send() .await .map_err(|e| anyhow::anyhow!("Request failed: {}", e))? @@ -124,10 +137,14 @@ impl TxCommand { "params": params }); - let response = client - .post(url) - .header("Authorization", format!("Bearer {}", api_key)) - .json(&request) + let mut request_builder = client.post(url).json(&request); + + // Add authorization header only for Alchemy endpoints + if url.contains("alchemy.com") { + request_builder = request_builder.header("Authorization", format!("Bearer {}", api_key)); + } + + let response = request_builder .send() .await .map_err(|e| anyhow::anyhow!("Request failed: {}", e))? @@ -150,8 +167,12 @@ impl TxCommand { // Extract values with defaults let block_number = receipt["blockNumber"] .as_str() - .unwrap_or("pending") - .to_string(); + .and_then(|hex| { + u64::from_str_radix(hex.trim_start_matches("0x"), 16) + .ok() + .map(|num| num.to_string()) + }) + .unwrap_or_else(|| "pending".to_string()); let from = tx_details["from"].as_str().unwrap_or("unknown").to_string(); @@ -205,38 +226,92 @@ impl TxCommand { println!("{}", "-".repeat(60)); println!("{}", style(format!(" Hash: {}", self.tx_hash)).dim()); + + // Show timestamp if available + if let Some(timestamp) = tx_details.get("timestamp").and_then(|t| t.as_str()) { + if let Ok(timestamp_num) = u64::from_str_radix(timestamp.trim_start_matches("0x"), 16) { + let datetime = chrono::DateTime::from_timestamp(timestamp_num as i64, 0) + .unwrap_or_else(|| chrono::Utc::now()); + println!("{}", style(format!(" Timestamp: {} UTC", datetime.format("%Y-%m-%d %H:%M:%S"))).dim()); + } + } + println!("{}", style(format!(" Block: {}", block_number)).dim()); println!("{}", style(format!(" From: {}", from)).dim()); println!("{}", style(format!(" To: {}", to)).dim()); - println!("\n{}", style("Transaction Data").bold().underlined()); + + // Show value in RBTC + if let Some(value_hex) = tx_details.get("value").and_then(|v| v.as_str()) { + if let Ok(value_wei) = u128::from_str_radix(value_hex.trim_start_matches("0x"), 16) { + let value_rbtc = value_wei as f64 / 1e18; + if value_rbtc > 0.0 { + println!("{}", style(format!(" Value: {} RBTC", value_rbtc)).dim()); + } + } + } + + // Show gas information + if let Some(gas_used_hex) = receipt.get("gasUsed").and_then(|g| g.as_str()) { + if let Ok(gas_used) = u64::from_str_radix(gas_used_hex.trim_start_matches("0x"), 16) { + println!("{}", style(format!(" Gas Used: {}", gas_used)).dim()); + } + } + + if let Some(gas_price_hex) = tx_details.get("gasPrice").and_then(|g| g.as_str()) { + if let Ok(gas_price_wei) = u128::from_str_radix(gas_price_hex.trim_start_matches("0x"), 16) { + let gas_price_gwei = gas_price_wei as f64 / 1e9; + println!("{}", style(format!(" Gas Price: {} Gwei", gas_price_gwei)).dim()); + } + } + + // Calculate transaction fee + if let (Some(gas_used_hex), Some(gas_price_hex)) = ( + receipt.get("gasUsed").and_then(|g| g.as_str()), + tx_details.get("gasPrice").and_then(|g| g.as_str()) + ) { + if let (Ok(gas_used), Ok(gas_price)) = ( + u128::from_str_radix(gas_used_hex.trim_start_matches("0x"), 16), + u128::from_str_radix(gas_price_hex.trim_start_matches("0x"), 16) + ) { + let fee_wei = gas_used * gas_price; + let fee_rbtc = fee_wei as f64 / 1e18; + println!("{}", style(format!(" Transaction Fee: {} RBTC", fee_rbtc)).dim()); + } + } + + // Show nonce + if let Some(nonce_hex) = tx_details.get("nonce").and_then(|n| n.as_str()) { + if let Ok(nonce) = u64::from_str_radix(nonce_hex.trim_start_matches("0x"), 16) { + println!("{}", style(format!(" Nonce: {}", nonce)).dim()); + } + } + + println!("\n{}", style("Status").bold().underlined()); println!("{}", "-".repeat(60)); - // println!("{}", style(format!(" Value: {}", value)).dim()); - // println!("{}", style(format!(" Gas Price: {}", gas_price)).dim()); - // println!("{}", style(format!(" Gas Used: {}", gas_used)).dim()); println!("\n{}", style(format!(" Status: {}", status)).dim()); // If there's a contract address, show it - if let Some(contract_addr) = receipt["contractAddress"].as_str() - && !contract_addr.is_empty() - { - println!("\n{}", style("Contract Creation").bold().underlined()); - println!("{}", "-".repeat(60)); - println!("{}", style(format!(" Contract: {}", contract_addr)).dim()); + if let Some(contract_addr) = receipt["contractAddress"].as_str() { + if !contract_addr.is_empty() { + println!("\n{}", style("Contract Creation").bold().underlined()); + println!("{}", "-".repeat(60)); + println!("{}", style(format!(" Contract: {}", contract_addr)).dim()); + } } // Show logs if any - if let Some(logs) = receipt["logs"].as_array() - && !logs.is_empty() - { - println!( - "\n{}", - style(format!(" Logs ({}):", logs.len())) - .bold() - .underlined() - ); - for log in logs { - if let Some(topic) = log["topics"].as_array().and_then(|t| t[0].as_str()) { - println!(" - {}", topic); + if let Some(logs) = receipt["logs"].as_array() { + if !logs.is_empty() { + println!( + "\n{}", + style(format!(" Logs ({}):", logs.len())) + .bold() + .underlined() + ); + for log in logs { + if let Some(topic) = log["topics"].as_array().and_then(|t| t[0].as_str()) { + println!(" - {}", topic); + } } } } diff --git a/src/commands/wallet.rs b/src/commands/wallet.rs index 98c35ff..d3635d7 100644 --- a/src/commands/wallet.rs +++ b/src/commands/wallet.rs @@ -87,7 +87,7 @@ impl WalletCommand { WalletData::new() }; let _ = wallet_data.add_wallet(wallet.clone()); - fs::write(&wallet_file, serde_json::to_string_pretty(&wallet_data)?)?; + crate::utils::secure_fs::write_secure(&wallet_file, &serde_json::to_string_pretty(&wallet_data)?)?; println!("{}", "🎉 Wallet created successfully".green()); println!("Address: {:?}", wallet.address()); println!("Wallet saved at: {}", wallet_file.display()); @@ -111,7 +111,7 @@ impl WalletCommand { WalletData::new() }; let _ = wallet_data.add_wallet(wallet); - fs::write(&wallet_file, serde_json::to_string_pretty(&wallet_data)?)?; + crate::utils::secure_fs::write_secure(&wallet_file, &serde_json::to_string_pretty(&wallet_data)?)?; println!("{}", "✅ Wallet imported successfully".green()); println!("Wallet saved at: {}", wallet_file.display()); Ok(()) @@ -154,7 +154,7 @@ impl WalletCommand { .ok_or_else(|| anyhow!("Wallet '{}' not found", name))? .address; let _ = wallet_data.switch_wallet(&format!("0x{:x}", wallet_address)); - fs::write(&wallet_file, serde_json::to_string_pretty(&wallet_data)?)?; + crate::utils::secure_fs::write_secure(&wallet_file, &serde_json::to_string_pretty(&wallet_data)?)?; println!("{}", format!("✅ Switched to wallet: {}", name).green()); println!("Address: 0x{:x}", wallet_address); Ok(()) @@ -182,7 +182,7 @@ impl WalletCommand { } else { return Err(anyhow!("Failed to rename wallet '{}'", old_name)); } - fs::write(&wallet_file, serde_json::to_string_pretty(&wallet_data)?)?; + crate::utils::secure_fs::write_secure(&wallet_file, &serde_json::to_string_pretty(&wallet_data)?)?; println!( "{}", format!("✅ Wallet renamed from '{}' to '{}'", old_name, new_name).green() @@ -207,20 +207,25 @@ impl WalletCommand { let wallet = wallet_data .get_wallet_by_name(name) .ok_or_else(|| anyhow!("Wallet '{}' not found", name))?; - let filename = path - .file_name() - .and_then(|f| f.to_str()) - .ok_or_else(|| anyhow!("Invalid filename in path: {}", path.display()))?; - let backup_path = PathBuf::from(format!("./{}", filename)); - fs::write(&backup_path, serde_json::to_string_pretty(&wallet)?)?; - if !backup_path.exists() { - return Err(anyhow!( - "Backup file was not created at: {}", - backup_path.display() - )); + + // Create parent directories if they don't exist + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + + // Write backup file with secure permissions (0o600) + fs::write(path, serde_json::to_string_pretty(&wallet)?)?; + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(path)?.permissions(); + perms.set_mode(0o600); + fs::set_permissions(path, perms)?; } + println!("{}", "✅ Backup created successfully".green()); - println!("Backup saved at: {}", backup_path.display()); + println!("Backup saved at: {}", path.display()); Ok(()) } @@ -238,7 +243,7 @@ impl WalletCommand { )); } let _ = wallet_data.remove_wallet(&address); - fs::write(&wallet_file, serde_json::to_string_pretty(&wallet_data)?)?; + crate::utils::secure_fs::write_secure(&wallet_file, &serde_json::to_string_pretty(&wallet_data)?)?; println!("{}", format!("✅ Deleted wallet: {}", name).green()); println!("Address: {}", address); Ok(()) diff --git a/src/config/setup.rs b/src/config/setup.rs index 9fabd46..bc899f1 100644 --- a/src/config/setup.rs +++ b/src/config/setup.rs @@ -1,6 +1,6 @@ use anyhow::Result; use console::style; -use dialoguer::{Confirm, Input, Select, theme::ColorfulTheme}; +use dialoguer::{Confirm, Password, Select, theme::ColorfulTheme}; use crate::config::{Config, ConfigManager, DOCS_URL, RSK_RPC_DOCS_URL}; use crate::types::network::Network; @@ -99,9 +99,9 @@ fn setup_api_keys(config: &mut Config, network: Network) -> Result<()> { println!("\nGet your RSK RPC API key from:"); println!("{}", style(RSK_RPC_DOCS_URL).blue().underlined()); - let rsk_key: String = Input::with_theme(&ColorfulTheme::default()) + let rsk_key: String = Password::with_theme(&ColorfulTheme::default()) .with_prompt(format!("Enter your RSK RPC {} API key", key_type)) - .interact_text()?; + .interact()?; // Add RSK RPC API key to config use crate::api::{ApiKey, ApiProvider}; @@ -133,9 +133,9 @@ fn setup_api_keys(config: &mut Config, network: Network) -> Result<()> { }; println!("{}", style(alchemy_url).blue().underlined()); - let alchemy_key: String = Input::with_theme(&ColorfulTheme::default()) + let alchemy_key: String = Password::with_theme(&ColorfulTheme::default()) .with_prompt(format!("Enter your Alchemy {} API key", key_type)) - .interact_text()?; + .interact()?; // Add Alchemy API key to config use crate::api::{ApiKey, ApiProvider}; diff --git a/src/interactive/balance.rs b/src/interactive/balance.rs index 51de4ea..7f87155 100644 --- a/src/interactive/balance.rs +++ b/src/interactive/balance.rs @@ -1,9 +1,14 @@ use crate::commands::balance::BalanceCommand; use crate::commands::tokens::TokenRegistry; use crate::config::ConfigManager; +use crate::types::wallet::WalletData; +use crate::utils::constants; +use crate::utils::table::TableBuilder; +use crate::utils::helper::Helper; use anyhow::{Result, anyhow}; use console::style; use inquire::Select; +use std::fs; /// Displays the balance checking interface pub async fn show_balance() -> Result<()> { @@ -55,12 +60,36 @@ pub async fn show_balance() -> Result<()> { }) .collect(); - // Get just the display names for the selection menu - let token_display_names: Vec = + // Add back option to token list + let mut token_display_names: Vec = token_choices.iter().map(|(name, _)| name.clone()).collect(); + token_display_names.push("🏠 Back to Main Menu".to_string()); // Let the user select which token to check - let selection = Select::new("Select token to check balance:", token_display_names).prompt()?; + let selection = loop { + match Select::new("Select token to check balance:", token_display_names.clone()).prompt() { + Ok(selection) => break selection, + Err(_) => { + // User pressed ESC - ask for confirmation + use dialoguer::Confirm; + let should_exit = Confirm::new() + .with_prompt("Return to main menu?") + .default(true) + .interact() + .unwrap_or(true); + + if should_exit { + return Ok(()); + } + // Continue loop to show menu again + } + } + }; + + // Handle back option + if selection == "🏠 Back to Main Menu" { + return Ok(()); + } // Find the selected token info let (_, token_info) = token_choices @@ -83,3 +112,49 @@ pub async fn show_balance() -> Result<()> { cmd.execute().await } + +/// Displays offline balance information (wallet addresses only) +pub async fn show_offline_balance() -> Result<()> { + println!("\n{}", style("💰 Check Balance (Offline Mode)").bold()); + println!("{}", "=".repeat(40)); + + println!("{}", style("⚠️ Network connectivity required for balance checking").yellow()); + println!("{}", style(" Showing wallet information instead:").dim()); + println!(); + + // Load wallet data + let wallet_file = constants::wallet_file_path(); + if !wallet_file.exists() { + return Err(anyhow!("No wallets found. Please create or import a wallet first.")); + } + + let data = fs::read_to_string(&wallet_file)?; + let wallet_data = serde_json::from_str::(&data)?; + + if wallet_data.wallets.is_empty() { + return Err(anyhow!("No wallets available.")); + } + + // Get current network for display + let config = ConfigManager::new()?.load()?; + + let mut table = TableBuilder::new(); + table.add_header(&["Wallet Name", "Address", "Network", "Status"]); + + for (name, wallet) in &wallet_data.wallets { + let is_current = wallet_data.current_wallet == *name; + let status = if is_current { "Current" } else { "Available" }; + + table.add_row(&[ + name, + &Helper::format_address(&wallet.address), + &config.default_network.to_string(), + status, + ]); + } + + table.print(); + + println!("\n{}", style("💡 Tip: Connect to internet to check actual balances").dim()); + Ok(()) +} diff --git a/src/interactive/bulk_transfer.rs b/src/interactive/bulk_transfer.rs index a41edb4..0442aca 100644 --- a/src/interactive/bulk_transfer.rs +++ b/src/interactive/bulk_transfer.rs @@ -9,10 +9,11 @@ use alloy::{ primitives::{Address, U256}, providers::{Provider, ProviderBuilder}, signers::local::PrivateKeySigner, - network::TransactionBuilder, + network::{TransactionBuilder, EthereumWallet}, }; use serde::Deserialize; use std::{fs, sync::Arc}; +use zeroize::Zeroize; #[derive(Debug, Clone)] struct Transfer { @@ -53,7 +54,7 @@ pub async fn bulk_transfer() -> Result<()> { let network_config = config.default_network.get_config(); // Get the chain ID based on the network - let chain_id = match config.default_network { + let _chain_id = match config.default_network { Network::RootStockMainnet => 30, Network::RootStockTestnet => 31, Network::Mainnet => 30, @@ -63,17 +64,20 @@ pub async fn bulk_transfer() -> Result<()> { }; // Prompt for password to decrypt the private key - let password = rpassword::prompt_password("Enter password for the wallet: ")?; + let mut password = rpassword::prompt_password("Enter password for the wallet: ")?; // Decrypt the private key - let private_key = current_wallet.decrypt_private_key(&password)?; + let mut private_key = current_wallet.decrypt_private_key(&password)?; + + // Zeroize password after use + password.zeroize(); // Create a wallet let wallet = private_key .parse::() .map_err(|e| anyhow!("Failed to parse private key: {}", e))?; - // Create a provider with the network RPC URL + // Create a standard provider let provider = ProviderBuilder::new() .on_http(network_config.rpc_url.parse()?); @@ -202,6 +206,9 @@ pub async fn bulk_transfer() -> Result<()> { return Ok(()); } + // Get starting nonce + let mut nonce = client.get_transaction_count(wallet.address()).await?; + // Send transactions println!("\n🚀 Sending transactions..."); @@ -215,34 +222,45 @@ pub async fn bulk_transfer() -> Result<()> { let tx = TransactionRequest::default() .with_to(transfer.to) .with_value(transfer.value) + .with_nonce(nonce) .with_gas_limit(gas_per_tx.try_into().unwrap_or(0u64)) .with_gas_price(gas_price.try_into().unwrap_or(0u128)); - match client.send_transaction(tx).await { - Ok(pending_tx) => { - let tx_hash = pending_tx.tx_hash(); - match client.get_transaction_receipt(*tx_hash).await { - Ok(Some(receipt)) => { - if receipt.status() { - println!("✅ Success! Tx: {:?}", receipt.transaction_hash); - successful += 1; - } else { - println!("❌ Failed! Tx: {:?}", receipt.transaction_hash); - failed += 1; + // Build and sign transaction + match tx.build(&EthereumWallet::from(wallet.clone())).await { + Ok(tx_envelope) => { + match client.send_tx_envelope(tx_envelope).await { + Ok(pending_tx) => { + let tx_hash = *pending_tx.tx_hash(); + nonce += 1; // Increment nonce for next transaction + match client.get_transaction_receipt(tx_hash).await { + Ok(Some(receipt)) => { + if receipt.status() { + println!("✅ Success! Tx: {:?}", receipt.transaction_hash); + successful += 1; + } else { + println!("❌ Failed! Tx: {:?}", receipt.transaction_hash); + failed += 1; + } + } + Ok(None) => { + println!("❌ Transaction was dropped from the mempool"); + failed += 1; + } + Err(e) => { + println!("❌ Error getting receipt: {}", e); + failed += 1; + } } } - Ok(None) => { - println!("❌ Transaction was dropped from the mempool"); - failed += 1; - } Err(e) => { - println!("❌ Error: {}", e); + println!("❌ Failed to send transaction: {}", e); failed += 1; } } } Err(e) => { - println!("❌ Failed to send transaction: {}", e); + println!("❌ Failed to build/sign transaction: {}", e); failed += 1; } } @@ -252,11 +270,14 @@ pub async fn bulk_transfer() -> Result<()> { } println!("\n📊 Transaction Summary:"); - println!("===================="); + println! ("====================" ); println!("Total transactions: {}", successful + failed); println!("✅ Successful: {}", successful); println!("❌ Failed: {}", failed); + // Zeroize sensitive data + private_key.zeroize(); + Ok(()) } diff --git a/src/interactive/config.rs b/src/interactive/config.rs index 9446438..87cb923 100644 --- a/src/interactive/config.rs +++ b/src/interactive/config.rs @@ -1,6 +1,6 @@ use anyhow::Result; use console::style; -use dialoguer::{Confirm, Input, Select, theme::ColorfulTheme}; +use dialoguer::{Confirm, Input, Password, Select, theme::ColorfulTheme}; // Import config and API types use crate::api::ApiProvider; @@ -161,9 +161,9 @@ async fn add_api_key(config_manager: &ConfigManager) -> Result<()> { let (provider, _) = &providers[selection]; // Get API key - let key: String = Input::with_theme(&ColorfulTheme::default()) + let key: String = Password::with_theme(&ColorfulTheme::default()) .with_prompt("Enter your API key") - .interact_text()?; + .interact()?; // Get optional name let name: String = Input::with_theme(&ColorfulTheme::default()) diff --git a/src/interactive/contract.rs b/src/interactive/contract.rs deleted file mode 100644 index 7465f8a..0000000 --- a/src/interactive/contract.rs +++ /dev/null @@ -1,154 +0,0 @@ -use crate::{ - config::ConfigManager, - types::network::Network, - wallet::load_wallet, -}; -use anyhow::{anyhow, Result}; -use dialoguer::{Confirm, Input, Select}; -use alloy::{ - primitives::{Address, U256}, - providers::{Provider, ProviderBuilder, RootProvider}, - signers::local::PrivateKeySigner, - transports::http::{Client, Http}, - sol, -}; -use std::sync::Arc; -use std::str::FromStr; - -/// Interactive menu for interacting with smart contracts -pub async fn interact_with_contract() -> Result<()> { - println!("\n📝 Smart Contract Interaction"); - println!("========================"); - - // Load wallet - let wallet_data = match load_wallet()? { - Some(w) => w, - None => return Err(anyhow!("No wallet found. Please create a wallet first.")), - }; - - // Load config - let config_manager = ConfigManager::new()?; - let config = config_manager.load()?; - - // Get the network configuration - let network_config = config.default_network.get_config(); - - // Get the chain ID based on the network - let chain_id = match config.default_network { - Network::RootStockMainnet => 30, - Network::RootStockTestnet => 31, - Network::Mainnet => 1, - Network::Testnet => 5, // Goerli - Network::Regtest => 1337, - _ => return Err(anyhow!("Unsupported network for contract interaction")), - }; - - // Create a wallet with the chain ID - let private_key = wallet_data.private_key - .as_ref() - .ok_or_else(|| anyhow!("No private key found in wallet"))?; - - let wallet = private_key - .parse::() - .map_err(|e| anyhow!("Failed to parse private key: {}", e))?; - - // Create provider with signer - let provider = ProviderBuilder::new() - .with_recommended_fillers() - .wallet(wallet) - .on_http(network_config.rpc_url.parse()?) - .map_err(|e| anyhow!("Failed to connect to RPC: {}", e))?; - - // Get contract address - let contract_address: String = Input::new() - .with_prompt("Enter contract address (0x...)") - .validate_with(|input: &String| { - if input.starts_with("0x") && input.len() == 42 { - Ok(()) - } else { - Err("Please enter a valid contract address starting with 0x".to_string()) - } - }) - .interact()?; - - let contract_address = contract_address.parse::
() - .map_err(|e| anyhow!("Invalid contract address: {}", e))?; - - // Get ABI file path - let abi_path: String = Input::new() - .with_prompt("Enter path to ABI JSON file") - .interact()?; - - // Read and parse ABI - let abi_content = std::fs::read_to_string(&abi_path) - .map_err(|e| anyhow!("Failed to read ABI file: {}", e))?; - - let abi: Abi = serde_json::from_str(&abi_content) - .map_err(|e| anyhow!("Failed to parse ABI: {}", e))?; - - println!("\n📋 Available functions:"); - for (i, function) in abi.functions().enumerate() { - println!("{:2}. {}", i + 1, function.signature()); - } - - // Select function - let function_index: usize = Input::new() - .with_prompt("Select function to call") - .default(0) - .interact()?; - - let selected_function = abi.functions().nth(function_index) - .ok_or_else(|| anyhow!("Invalid function index"))?; - - println!("\n🔧 Function: {}", selected_function.signature()); - - // TODO: Add parameter input and function call logic - - Ok(()) -} - -// Helper function to load wallet -fn load_wallet() -> Result { - // TODO: Implement wallet loading logic - // This is a placeholder - replace with actual wallet loading logic - let private_key = "0x...".to_string(); - - private_key.parse::() - .map_err(|e| anyhow!("Failed to parse private key: {}", e)) -} - -// Helper function to load config -fn load_config() -> Result { - // TODO: Implement config loading logic - // This is a placeholder - replace with actual config loading logic - Ok(Config::default()) -} - -#[derive(Default)] -struct Config { - default_network: Network, -} - -#[derive(Default)] -enum Network { - #[default] - Mainnet, - Testnet, -} - -impl Network { - fn get_config(&self) -> NetworkConfig { - match self { - Network::Mainnet => NetworkConfig { - rpc_url: "https://public-node.rsk.co".to_string(), - }, - Network::Testnet => NetworkConfig { - rpc_url: "https://public-node.testnet.rsk.co".to_string(), - }, - } - } -} - -struct NetworkConfig { - rpc_url: String, -} diff --git a/src/interactive/history.rs b/src/interactive/history.rs index 1231d10..4f9ec01 100644 --- a/src/interactive/history.rs +++ b/src/interactive/history.rs @@ -1,9 +1,11 @@ use crate::commands::history::HistoryCommand; use crate::commands::tokens::{TokenRegistry, list_tokens}; use crate::config::ConfigManager; -use anyhow::{Context, Result}; +use crate::utils::api_validator::{validate_api_key_format, validate_api_key, ValidationResult}; +use crate::api::{ApiKey, ApiProvider}; +use anyhow::Result; use console::style; -use inquire::{Confirm, Select, Text, validator::Validation}; +use inquire::{Confirm, Select, Text, Password, validator::Validation}; /// Shows the transaction history in an interactive way pub async fn show_history() -> Result<()> { @@ -100,29 +102,67 @@ pub async fn show_history() -> Result<()> { .unwrap_or(false); if should_add_key { - let api_key = Text::new("Enter your Alchemy API key:") + let api_key = Password::new("Enter your Alchemy API key:") .with_help_message("Get one at https://www.alchemy.com/") + .with_validator(|input: &str| { + if input.trim().is_empty() { + return Ok(Validation::Invalid("API key cannot be empty".into())); + } + if let Err(e) = validate_api_key_format(&ApiProvider::Alchemy, input.trim()) { + return Ok(Validation::Invalid(e.to_string().into())); + } + Ok(Validation::Valid) + }) .prompt()?; - if !api_key.trim().is_empty() { - // Save the API key using ConfigManager - let mut config = config_manager.load()?; - match network_selection { - "mainnet" => config.alchemy_mainnet_key = Some(api_key.trim().to_string()), - "testnet" => config.alchemy_testnet_key = Some(api_key.trim().to_string()), - _ => {} - } - config_manager.save(&config)?; + // Validate the API key + let api_key_obj = ApiKey { + key: api_key.trim().to_string(), + network: network_selection.to_string(), + provider: ApiProvider::Alchemy, + name: None, + }; - println!("\n{}", style("✅ API key saved successfully!").green()); - command.api_key = Some(api_key.trim().to_string()); - } else { - println!( - "\n{}", - style("❌ No API key provided. Cannot fetch transaction history.").red() - ); - println!("You can add an API key later from the Configuration menu."); - return Ok(()); + println!("🔍 Validating API key..."); + match validate_api_key(&api_key_obj).await { + Ok(ValidationResult::Valid) => { + // Save the API key using ConfigManager + let mut config = config_manager.load()?; + match network_selection { + "mainnet" => config.alchemy_mainnet_key = Some(api_key.trim().to_string()), + "testnet" => config.alchemy_testnet_key = Some(api_key.trim().to_string()), + _ => {} + } + config_manager.save(&config)?; + + println!("{}", style("✅ API key validated and saved successfully!").green()); + command.api_key = Some(api_key.trim().to_string()); + } + Ok(ValidationResult::Invalid(reason)) => { + println!("{}: {}", style("❌ Invalid API key").red().bold(), reason); + println!("Please check your API key and try again."); + return Ok(()); + } + Ok(ValidationResult::NetworkError(error)) => { + println!("{}: {}", style("⚠️ Network Error").yellow().bold(), error); + println!("Saving key anyway - validation will retry when network is available"); + + // Save anyway for offline use + let mut config = config_manager.load()?; + match network_selection { + "mainnet" => config.alchemy_mainnet_key = Some(api_key.trim().to_string()), + "testnet" => config.alchemy_testnet_key = Some(api_key.trim().to_string()), + _ => {} + } + config_manager.save(&config)?; + + println!("{}", style("💾 API key saved (unvalidated)").yellow()); + command.api_key = Some(api_key.trim().to_string()); + } + Err(e) => { + println!("{}: {}", style("❌ Validation Error").red().bold(), e); + return Ok(()); + } } } else { println!( @@ -138,16 +178,25 @@ pub async fn show_history() -> Result<()> { match command.execute().await { Ok(_) => {} Err(e) => { - if e.to_string().contains("API key") { - println!( - "\n{}", - style("❌ Error: Invalid or missing Alchemy API key").red() - ); - println!("Please check your API key and try again."); - println!("You can update your API key in the Configuration menu."); + let error_msg = e.to_string(); + if error_msg.contains("Must be authenticated") || error_msg.contains("API key") { + println!("{}", style("❌ Authentication Failed").red().bold()); + println!("Your API key appears to be invalid or expired."); + println!("💡 Please check your API key in the Configuration menu."); + + // Clear the invalid API key + command.api_key = None; + continue; + } else if error_msg.contains("network") || error_msg.contains("connection") { + println!("{}", style("⚠️ Network Error").yellow().bold()); + println!("Unable to connect to the API service."); + println!("💡 Please check your internet connection and try again."); return Ok(()); } else { - return Err(e).context("Failed to fetch transaction history"); + println!("{}", style("❌ Transaction History Error").red().bold()); + println!("Error: {}", error_msg); + println!("💡 This might be a temporary issue. Please try again later."); + return Ok(()); } } } diff --git a/src/interactive/mod.rs b/src/interactive/mod.rs index a4046d1..e5abf04 100644 --- a/src/interactive/mod.rs +++ b/src/interactive/mod.rs @@ -19,7 +19,7 @@ use dialoguer::{Select, theme::ColorfulTheme}; // Re-export public functions pub use self::{ - balance::show_balance, bulk_transfer::bulk_transfer, config::show_config_menu, + balance::{show_balance, show_offline_balance}, bulk_transfer::bulk_transfer, config::show_config_menu, contacts::manage_contacts, history::show_history, system::system_menu, tokens::token_menu, transfer::send_funds, tx::check_transaction_status, wallet::create_wallet_with_name, wallet::wallet_menu, @@ -27,6 +27,7 @@ pub use self::{ // Import for network status display use crate::config::ConfigManager; +use crate::utils::network::{check_connectivity, NetworkStatus}; // Import Network from the types module use crate::types::network::Network; @@ -63,11 +64,19 @@ pub async fn start() -> Result<()> { ); println!("{}\n", "-".repeat(40)); + // Check network connectivity + let network_status = check_connectivity().await; + let is_online = network_status == NetworkStatus::Online; + // Display current status let config_manager = ConfigManager::new()?; let config = config_manager.load()?; - println!(" {}", style("🟢 Online").green()); + if is_online { + println!(" {}", style("🟢 Online").green()); + } else { + println!(" {}", style("🔴 Offline").red()); + } println!(" {}", get_network_status(config.default_network)); // Check if wallet data file exists and count wallets @@ -93,29 +102,78 @@ pub async fn start() -> Result<()> { }; println!(" {}\n", style(wallet_text).dim()); + if !is_online { + println!(" {}", style("ℹ️ Limited functionality available offline").yellow()); + println!(); + } + loop { - let options = vec![ - format!("{} Check Balance", style("💰").bold().green()), - format!("{} Send Funds", style("💸").bold().yellow()), - format!("{} Bulk Transfer", style("📤").bold().yellow()), - format!("{} Check Transaction Status", style("🔍").bold().cyan()), - format!("{} Transaction History", style("📜").bold().cyan()), - format!("{} Wallet Management", style("🔑").bold().blue()), - format!("{} Token Management", style("🪙").bold().magenta()), - format!("{} Contact Management", style("📇").bold().cyan()), - format!("{} Configuration", style("⚙️").bold().white()), - format!("{} System", style("💻").bold().cyan()), - format!("{} Exit", style("🚪").bold().red()), - ]; - - let selection = Select::with_theme(&ColorfulTheme::default()) + let mut options = vec![]; + let mut option_map = vec![]; + + // Add options based on network status + if is_online { + options.push(format!("{} Check Balance", style("💰").bold().green())); + option_map.push(0); + options.push(format!("{} Send Funds", style("💸").bold().yellow())); + option_map.push(1); + options.push(format!("{} Bulk Transfer", style("📤").bold().yellow())); + option_map.push(2); + options.push(format!("{} Check Transaction Status", style("🔍").bold().cyan())); + option_map.push(3); + options.push(format!("{} Transaction History", style("📜").bold().cyan())); + option_map.push(4); + } else { + options.push(format!("{} Check Balance {}", style("💰").bold().dim(), style("(offline)").dim())); + option_map.push(0); + } + + // Always available options + options.push(format!("{} Wallet Management", style("🔑").bold().blue())); + option_map.push(5); + options.push(format!("{} Token Management", style("🪙").bold().magenta())); + option_map.push(6); + options.push(format!("{} Contact Management", style("📇").bold().cyan())); + option_map.push(7); + options.push(format!("{} Configuration", style("⚙️").bold().white())); + option_map.push(8); + options.push(format!("{} System", style("💻").bold().cyan())); + option_map.push(9); + options.push(format!("{} Exit", style("🚪").bold().red())); + option_map.push(10); + + let selection = match Select::with_theme(&ColorfulTheme::default()) .with_prompt("\nWhat would you like to do?") .items(&options) .default(0) - .interact()?; - - match selection { - 0 => show_balance().await?, + .interact() { + Ok(selection) => selection, + Err(_) => { + // User pressed ESC or interrupted - ask for confirmation + use dialoguer::Confirm; + let should_exit = Confirm::new() + .with_prompt("Are you sure you want to exit?") + .default(false) + .interact() + .unwrap_or(false); + + if should_exit { + println!("👋 Goodbye!"); + return Ok(()); + } else { + continue; // Go back to menu + } + } + }; + + match option_map[selection] { + 0 => { + if is_online { + show_balance().await?; + } else { + show_offline_balance().await?; + } + }, 1 => send_funds().await?, 2 => bulk_transfer().await?, 3 => check_transaction_status().await?, diff --git a/src/interactive/system.rs b/src/interactive/system.rs index 5adb6d3..71ad382 100644 --- a/src/interactive/system.rs +++ b/src/interactive/system.rs @@ -6,7 +6,6 @@ use crate::utils::terminal::{self, show_version}; use anyhow::Result; use console::style; use dialoguer::{Select, theme::ColorfulTheme}; -use alloy::primitives::U256; use alloy::providers::Provider; use std::io; use std::time::Duration; diff --git a/src/interactive/transfer.rs b/src/interactive/transfer.rs index 5f07bd0..e44ea7b 100644 --- a/src/interactive/transfer.rs +++ b/src/interactive/transfer.rs @@ -106,11 +106,35 @@ pub async fn send_funds() -> Result<()> { .collect(); // Get just the display names for the selection menu - let token_display_names: Vec = + let mut token_display_names: Vec = token_choices.iter().map(|(name, _)| name.clone()).collect(); + token_display_names.push("🏠 Back to Main Menu".to_string()); // Let the user select which token to send - let selection = Select::new("Select token to send:", token_display_names).prompt()?; + let selection = loop { + match Select::new("Select token to send:", token_display_names.clone()).prompt() { + Ok(selection) => break selection, + Err(_) => { + // User pressed ESC - ask for confirmation + use dialoguer::Confirm; + let should_exit = Confirm::new() + .with_prompt("Return to main menu?") + .default(true) + .interact() + .unwrap_or(true); + + if should_exit { + return Ok(()); + } + // Continue loop to show menu again + } + } + }; + + // Handle back option + if selection == "🏠 Back to Main Menu" { + return Ok(()); + } // Find the selected token info let (display_name, token_info) = token_choices @@ -146,6 +170,7 @@ pub async fn send_funds() -> Result<()> { &to, &wei.to_string(), config.default_network, + &display_name, ) .await?; @@ -184,24 +209,49 @@ pub async fn send_funds() -> Result<()> { // Execute the transfer command let cmd = TransferCommand { - address: to, - value: amount - .parse::() - .map_err(|_| anyhow::anyhow!("Invalid amount format"))?, + address: to.clone(), + value: amount.clone(), token: if token_address == "0x0000000000000000000000000000000000000000" { None } else { - Some(token_address) + Some(token_address.clone()) }, }; - let result = cmd.execute().await?; - - println!( - "\n{}: Transaction confirmed! Tx Hash: {}", - "Success".green().bold(), - result.tx_hash - ); + match cmd.execute().await { + Ok(result) => { + println!( + "\n{}: Transaction confirmed! Tx Hash: {}", + "Success".green().bold(), + result.tx_hash + ); + + let explorer_url = if network.to_string().to_lowercase().contains("testnet") { + format!("https://explorer.testnet.rsk.co/tx/{:x}", result.tx_hash) + } else { + format!("https://explorer.rsk.co/tx/{:x}", result.tx_hash) + }; + + println!("🔗 View on Explorer: {}", explorer_url); + } + Err(e) => { + let error_msg = e.to_string(); + if error_msg.contains("Insufficient") { + println!("{}", style("❌ Transaction Failed").red().bold()); + println!("{}", error_msg); + println!("💡 Please check your balance and try again with a smaller amount."); + } else if error_msg.contains("gas") { + println!("{}", style("⛽ Gas Error").yellow().bold()); + println!("{}", error_msg); + println!("💡 Try again when network conditions improve."); + } else { + println!("{}", style("❌ Transaction Failed").red().bold()); + println!("Error: {}", error_msg); + println!("💡 Please check your inputs and network connection."); + } + return Ok(()); + } + } Ok(()) } diff --git a/src/interactive/transfer_preview.rs b/src/interactive/transfer_preview.rs index 19f698b..b37522b 100644 --- a/src/interactive/transfer_preview.rs +++ b/src/interactive/transfer_preview.rs @@ -21,7 +21,7 @@ fn convert_wei_to_rbtc(wei: U256) -> f64 { } /// Displays transaction details and asks for confirmation -pub async fn show_transaction_preview(to: &str, amount: &str, network: Network) -> Result { +pub async fn show_transaction_preview(to: &str, amount: &str, network: Network, token_symbol: &str) -> Result { println!("\n{}", style("Transaction Preview").bold().underlined()); println!("• To: {}", style(to).cyan()); @@ -29,11 +29,12 @@ pub async fn show_transaction_preview(to: &str, amount: &str, network: Network) let amount_wei = U256::from_str(amount).map_err(|e| anyhow::anyhow!("Invalid amount format: {}", e))?; - // Convert to RBTC for display - let amount_rbtc = convert_wei_to_rbtc(amount_wei); + // Convert to token units for display + let amount_tokens = convert_wei_to_rbtc(amount_wei); println!( - "• Amount: {} RBTC ({} wei)", - style(amount_rbtc).green(), + "• Amount: {} {} ({} wei)", + style(amount_tokens).green(), + style(token_symbol).green(), style(amount_wei).dim() ); @@ -80,12 +81,21 @@ pub async fn show_transaction_preview(to: &str, amount: &str, network: Network) println!("• Estimated Gas: {}", style(estimated_gas).yellow()); println!("• Estimated Fee: {} RBTC", style(gas_cost_rbtc).red()); - let total_amount = amount_wei.checked_add(gas_cost).unwrap_or(amount_wei); - let total_rbtc = convert_wei_to_rbtc(total_amount); - println!( - "• Total (Amount + Fee): {} RBTC", - style(total_rbtc).green().bold() - ); + if token_symbol == "RBTC" { + let total_amount = amount_wei.checked_add(gas_cost).unwrap_or(amount_wei); + let total_rbtc = convert_wei_to_rbtc(total_amount); + println!( + "• Total (Amount + Fee): {} RBTC", + style(total_rbtc).green().bold() + ); + } else { + println!( + "• Total: {} {} + {} RBTC (gas fee)", + style(amount_tokens).green().bold(), + style(token_symbol).green().bold(), + style(gas_cost_rbtc).red() + ); + } // Ask for confirmation let confirm = Confirm::new() diff --git a/src/interactive/tx.rs b/src/interactive/tx.rs index 917fe5a..c1a0f14 100644 --- a/src/interactive/tx.rs +++ b/src/interactive/tx.rs @@ -2,7 +2,7 @@ use anyhow::Result; use console::style; use dialoguer::Input; -use crate::{commands::tx::TxCommand, config::ConfigManager, types::network::Network}; +use crate::{commands::tx::TxCommand, config::ConfigManager, types::network::Network, interactive::config::show_config_menu}; /// Interactive transaction status checker pub async fn check_transaction_status() -> Result<()> { @@ -25,11 +25,16 @@ pub async fn check_transaction_status() -> Result<()> { if input.to_lowercase() == "q" { return Ok(()); } - if input.starts_with("0x") && input.len() == 66 { - Ok(()) - } else { - Err("Please enter a valid transaction hash (0x followed by 64 hex characters) or 'q' to go back") + if !input.starts_with("0x") { + return Err("Transaction hash must start with '0x'"); + } + if input.len() != 66 { + return Err("Transaction hash must be exactly 66 characters (0x + 64 hex chars)"); } + if !input[2..].chars().all(|c| c.is_ascii_hexdigit()) { + return Err("Transaction hash contains invalid characters (only 0-9, a-f, A-F allowed)"); + } + Ok(()) }) .interact_text()?; @@ -62,7 +67,32 @@ pub async fn check_transaction_status() -> Result<()> { } Err(e) => { let error_msg = e.to_string(); - if error_msg.contains("not found") || error_msg.contains("does not exist") { + if error_msg.contains("No API key found") { + println!("\n{}", style("❌ API Configuration Required").red().bold()); + println!("To check transaction status, you need to configure an API key."); + println!("\n{}", style("Available options:").bold()); + println!(" • RSK Public Node (free)"); + println!(" • Alchemy API (requires signup)"); + + println!("\n{}", style("To configure:").bold()); + println!(" 1. Run: {} or {}", + style("rsk-rust-cli config").cyan(), + style("wallet config").cyan() + ); + println!(" 2. Select your preferred API provider"); + println!(" 3. Enter your API key or endpoint"); + + let setup_now = dialoguer::Confirm::new() + .with_prompt("\nWould you like to set up API configuration now?") + .default(true) + .interact()?; + + if setup_now { + show_config_menu().await?; + continue; // Return to transaction input after config + } + break; + } else if error_msg.contains("not found") || error_msg.contains("does not exist") { println!( "\n{}", style("❌ Transaction not found or still pending.").yellow() @@ -73,18 +103,42 @@ pub async fn check_transaction_status() -> Result<()> { "\n{}", style("💡 Tip: Transactions usually take 15-30 seconds to be mined.").dim() ); + } else if error_msg.contains("timeout") || error_msg.contains("timed out") { + println!("\n{}", style("❌ Network timeout").red()); + println!("The request took too long. Check your internet connection."); + } else if error_msg.contains("dns") || error_msg.contains("resolve") || error_msg.contains("No such host") { + println!("\n{}", style("❌ DNS Resolution Failed").red()); + println!("Cannot resolve the API endpoint. Check your internet connection."); + } else if error_msg.contains("Connection refused") || error_msg.contains("unreachable") { + println!("\n{}", style("❌ Connection Failed").red()); + println!("Cannot connect to the network. You may be offline."); + println!("\n{}", style("💡 Try:").blue()); + println!(" • Check your internet connection"); + println!(" • Verify firewall settings"); + println!(" • Try again in a few moments"); + } else if error_msg.contains("401") || error_msg.contains("403") || error_msg.contains("invalid") && error_msg.contains("key") { + println!("\n{}", style("❌ Invalid API Key").red()); + println!("Your API key appears to be invalid or expired."); + println!("Please update your configuration with a valid API key."); + } else if error_msg.contains("Request failed") { + println!("\n{}", style("❌ Network Error").red()); + println!("Failed to connect to the API. Check your internet connection."); } else { println!("\n{}", style("❌ Error checking transaction status:").red()); println!("{}", error_msg); } - // Ask if user wants to try again - let try_again = dialoguer::Confirm::new() - .with_prompt("Would you like to try again?") - .default(true) - .interact()?; + // Ask if user wants to try again (except for API key errors) + if !error_msg.contains("No API key found") { + let try_again = dialoguer::Confirm::new() + .with_prompt("Would you like to try again?") + .default(true) + .interact()?; - if !try_again { + if !try_again { + break; + } + } else { break; } } diff --git a/src/interactive/wallet.rs b/src/interactive/wallet.rs index d85feb3..455d09f 100644 --- a/src/interactive/wallet.rs +++ b/src/interactive/wallet.rs @@ -1,6 +1,27 @@ use crate::commands::wallet::{WalletAction, WalletCommand}; use anyhow::Result; use console::style; +use zeroize::Zeroize; + +/// Validates password strength +fn validate_password(password: &str) -> Result> { + if password.len() < 8 { + return Ok(inquire::validator::Validation::Invalid("Password must be at least 8 characters long".into())); + } + if !password.chars().any(|c| c.is_ascii_lowercase()) { + return Ok(inquire::validator::Validation::Invalid("Password must contain at least one lowercase letter".into())); + } + if !password.chars().any(|c| c.is_ascii_uppercase()) { + return Ok(inquire::validator::Validation::Invalid("Password must contain at least one uppercase letter".into())); + } + if !password.chars().any(|c| c.is_ascii_digit()) { + return Ok(inquire::validator::Validation::Invalid("Password must contain at least one number".into())); + } + if !password.chars().any(|c| c.is_ascii_punctuation()) { + return Ok(inquire::validator::Validation::Invalid("Password must contain at least one symbol (!@#$%^&* etc.)".into())); + } + Ok(inquire::validator::Validation::Valid) +} /// Displays the wallet management menu pub async fn wallet_menu() -> Result<()> { @@ -11,6 +32,7 @@ pub async fn wallet_menu() -> Result<()> { String::from("📋 List Wallets"), String::from("🔄 Switch Wallet"), String::from("✏️ Rename Wallet"), + String::from("🔑 Export Private Key"), String::from("💾 Backup Wallet"), String::from("🗑️ Delete Wallet"), String::from("🏠 Back to Main Menu"), @@ -26,6 +48,7 @@ pub async fn wallet_menu() -> Result<()> { "📋 List Wallets" => list_wallets().await, "🔄 Switch Wallet" => switch_wallet().await, "✏️ Rename Wallet" => rename_wallet().await, + "🔑 Export Private Key" => export_private_key().await, "💾 Backup Wallet" => backup_wallet().await, "🗑️ Delete Wallet" => delete_wallet().await, _ => break, @@ -80,6 +103,7 @@ pub async fn create_wallet_with_name(name: &str) -> Result<()> { .with_custom_confirmation_error_message("The passwords don't match.") .with_custom_confirmation_message("Please confirm your password:") .with_formatter(&|_| String::from("✓ Password set")) + .with_validator(validate_password) .prompt()?; println!( @@ -87,15 +111,20 @@ pub async fn create_wallet_with_name(name: &str) -> Result<()> { style("⏳ Creating your wallet. This may take a few seconds...").dim() ); + let mut password_copy = password.clone(); let cmd = WalletCommand { action: WalletAction::Create { name: name.to_string(), - password: password.clone(), + password: password_copy.clone(), }, }; - cmd.execute().await?; - Ok(()) + let result = cmd.execute().await; + + // Zeroize sensitive data + password_copy.zeroize(); + + result } async fn import_wallet() -> Result<()> { @@ -111,9 +140,29 @@ async fn import_wallet() -> Result<()> { style("This should start with '0x' followed by 64 hexadecimal characters.").dim() ); - let private_key = inquire::Password::new("Private key (0x...):") - .with_display_mode(inquire::PasswordDisplayMode::Hidden) - .with_help_message("The private key of the wallet to import") + let private_key = inquire::Text::new("Private key (0x...):") + .with_help_message("The private key of the wallet to import (will be masked)") + .with_validator(|input: &str| { + if !input.starts_with("0x") { + return Ok(inquire::validator::Validation::Invalid("Private key must start with '0x'".into())); + } + if input.len() != 66 { + return Ok(inquire::validator::Validation::Invalid("Private key must be 66 characters (0x + 64 hex chars)".into())); + } + if !input[2..].chars().all(|c| c.is_ascii_hexdigit()) { + return Ok(inquire::validator::Validation::Invalid("Private key must contain only hexadecimal characters".into())); + } + Ok(inquire::validator::Validation::Valid) + }) + .with_formatter(&|input| { + if input.is_empty() { + String::new() + } else if input.len() <= 2 { + input.to_string() + } else { + format!("0x{}", "*".repeat(input.len() - 2)) + } + }) .prompt()?; let name = inquire::Text::new("Wallet name:") @@ -135,6 +184,7 @@ async fn import_wallet() -> Result<()> { .with_custom_confirmation_error_message("The passwords don't match.") .with_custom_confirmation_message("Please confirm your password:") .with_formatter(&|_| String::from("✓ Password set")) + .with_validator(validate_password) .prompt()?; println!( @@ -142,17 +192,32 @@ async fn import_wallet() -> Result<()> { style("⏳ Importing your wallet. This may take a few seconds...").dim() ); + let mut private_key_copy = private_key.clone(); + let mut password_copy = password.clone(); let cmd = WalletCommand { action: WalletAction::Import { - private_key: private_key.clone(), + private_key: private_key_copy.clone(), name: name.clone(), - password: password.clone(), + password: password_copy.clone(), }, }; - cmd.execute().await?; + let result = cmd.execute().await; + + // Zeroize sensitive data + private_key_copy.zeroize(); + password_copy.zeroize(); + + match result { + Ok(_) => { + println!("\n{}", style("✅ Wallet imported successfully!").green()); + } + Err(e) => { + println!("\n{}", style(&format!("❌ Failed to import wallet: {}", e)).red()); + return Err(e); + } + } - println!("\n{}", style("✅ Wallet imported successfully!").green()); Ok(()) } @@ -226,6 +291,63 @@ async fn rename_wallet() -> Result<()> { Ok(()) } +/// Show private key for the current wallet (like MetaMask) +async fn export_private_key() -> Result<()> { + use dialoguer::Confirm; + use std::fs; + + println!("\n{}", style("🔑 Show Private Key").bold().red()); + println!("{}", "=".repeat(30)); + + // Security warning + println!("{}", style("⚠️ WARNING: Never share your private key!").red().bold()); + println!("{}", style("• Anyone with this key can access your funds").yellow()); + println!("{}", style("• Make sure no one is watching your screen").yellow()); + + let confirm = Confirm::new() + .with_prompt("I understand the risks, show my private key") + .default(false) + .interact()?; + + if !confirm { + return Ok(()); + } + + // Load wallet data from file + let wallet_file = crate::utils::constants::wallet_file_path(); + if !wallet_file.exists() { + println!("{}", style("❌ No wallets found").red()); + return Ok(()); + } + + let data = fs::read_to_string(&wallet_file)?; + let wallet_data: crate::types::wallet::WalletData = serde_json::from_str(&data)?; + + let current_wallet = wallet_data.get_current_wallet().ok_or_else(|| { + anyhow::anyhow!("No wallet selected") + })?; + + let password = inquire::Password::new("Enter wallet password:") + .with_display_mode(inquire::PasswordDisplayMode::Masked) + .prompt()?; + + println!( + "\n{}", + style("⏳ Decrypting your private key. This may take a few seconds...").dim() + ); + + match current_wallet.decrypt_private_key(&password) { + Ok(private_key) => { + println!("\n{}", style("Your Private Key:").bold()); + println!("{}", style(&private_key).cyan().bold()); + println!("\n{}", style("⚠️ Keep this safe and never share it!").red()); + } + Err(_) => println!("{}", style("❌ Incorrect password").red()), + } + + Ok(()) +} + async fn backup_wallet() -> Result<()> { use std::path::PathBuf; diff --git a/src/main.rs b/src/main.rs index d48baee..8fefc9a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ #![allow(warnings)] use anyhow::{Result, anyhow}; -use dotenv::dotenv; +use dotenvy::dotenv; use std::env; mod api; diff --git a/src/types/contacts.rs b/src/types/contacts.rs index bc1a267..d5a2f7e 100644 --- a/src/types/contacts.rs +++ b/src/types/contacts.rs @@ -210,13 +210,14 @@ impl Contact { if self.created_at.timestamp() < 0 { return Err(anyhow::anyhow!("Created at timestamp cannot be negative")); } - if let Some(stats) = &self.transaction_stats - && let Some(last_tx) = stats.last_transaction - && last_tx.timestamp() > chrono::Local::now().timestamp() - { - return Err(anyhow::anyhow!( - "Last transaction timestamp cannot be in the future" - )); + if let Some(stats) = &self.transaction_stats { + if let Some(last_tx) = stats.last_transaction { + if last_tx.timestamp() > chrono::Local::now().timestamp() { + return Err(anyhow::anyhow!( + "Last transaction timestamp cannot be in the future" + )); + } + } } if self.created_at.timestamp() < 1_000_000_000 { diff --git a/src/types/transaction.rs b/src/types/transaction.rs index fd9be41..9af6b41 100644 --- a/src/types/transaction.rs +++ b/src/types/transaction.rs @@ -3,7 +3,6 @@ use anyhow::{Result, anyhow}; use chrono::{DateTime, Utc}; use alloy::primitives::{Address, Bytes, B256, U64, U256}; use alloy::providers::{Provider, ProviderBuilder}; -use alloy::transports::http::{Client, Http}; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::str::FromStr; diff --git a/src/types/wallet.rs b/src/types/wallet.rs index a6609a7..3551e65 100644 --- a/src/types/wallet.rs +++ b/src/types/wallet.rs @@ -1,18 +1,15 @@ use crate::types::contacts::Contact; -use aes::Aes256; +use aes_gcm::{Aes256Gcm, Key, Nonce, aead::{Aead, KeyInit}}; use anyhow::Result; use anyhow::{Error, anyhow}; use base64::engine::general_purpose::STANDARD; use base64::{self, Engine as _}; -use cbc::cipher::block_padding::Pkcs7; -use cbc::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit}; -use cbc::{Decryptor, Encryptor}; use chrono::Utc; use alloy::primitives::{Address, U256}; -use alloy::signers::{local::PrivateKeySigner, Signer}; -use generic_array::GenericArray; +use alloy::signers::local::PrivateKeySigner; use rand::{RngCore, rngs::OsRng}; use scrypt::{Params, scrypt}; +use zeroize::Zeroize; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fmt; @@ -63,28 +60,30 @@ impl Wallet { ) -> anyhow::Result<(Vec, Vec, Vec)> { let mut salt = [0u8; 16]; OsRng.fill_bytes(&mut salt); - let mut iv = [0u8; 16]; - OsRng.fill_bytes(&mut iv); + let mut nonce = [0u8; 12]; // GCM uses 12-byte nonce + OsRng.fill_bytes(&mut nonce); let params = Params::recommended(); let mut key = [0u8; 32]; scrypt(password.as_bytes(), &salt, ¶ms, &mut key)?; - let mut buffer = private_key.to_vec(); - let pos = buffer.len(); - let pad_len = 16 - (pos % 16); - buffer.extend(std::iter::repeat_n(pad_len as u8, pad_len)); - let encryptor = Encryptor::::new(&key.into(), &iv.into()); - let _ = encryptor.encrypt_padded_mut::(&mut buffer, pos); - Ok((buffer, iv.to_vec(), salt.to_vec())) + + let cipher = Aes256Gcm::new(Key::::from_slice(&key)); + let ciphertext = cipher.encrypt(Nonce::from_slice(&nonce), private_key) + .map_err(|e| anyhow!("Encryption failed: {}", e))?; + + // Zeroize sensitive data + key.zeroize(); + + Ok((ciphertext, nonce.to_vec(), salt.to_vec())) } pub fn decrypt_private_key(&self, password: &str) -> Result { - // Decode Base64-encoded salt, IV, and encrypted key + // Decode Base64-encoded salt, nonce/IV, and encrypted key let salt = STANDARD .decode(&self.salt) .map_err(|e| anyhow!("Failed to decode salt: {}", e))?; - let iv = STANDARD + let nonce_or_iv = STANDARD .decode(&self.iv) - .map_err(|e| anyhow!("Failed to decode IV: {}", e))?; + .map_err(|e| anyhow!("Failed to decode nonce/IV: {}", e))?; let encrypted_key = STANDARD .decode(&self.encrypted_private_key) .map_err(|e| anyhow!("Failed to decode encrypted private key: {}", e))?; @@ -93,45 +92,32 @@ impl Wallet { if salt.len() != 16 { return Err(anyhow!("Salt must be 16 bytes, got {} bytes", salt.len())); } - if iv.len() != 16 { - return Err(anyhow!("IV must be 16 bytes, got {} bytes", iv.len())); - } - if encrypted_key.len() % 16 != 0 { - return Err(anyhow!( - "Encrypted key length ({}) is not a multiple of 16", - encrypted_key.len() - )); - } - // Derive the key using scrypt with parameters matching encryption + // Derive the key using scrypt let mut key = [0u8; 32]; - let params = Params::recommended(); // Ensure this matches your encryption params + let params = Params::recommended(); scrypt(password.as_bytes(), &salt, ¶ms, &mut key) .map_err(|e| anyhow!("Key derivation failed: {}", e))?; - // Convert key and IV to GenericArray for the cipher - let key_array = GenericArray::from_slice(&key[..]); // returns &GenericArray - let iv_array = GenericArray::from_slice(&iv[..]); // returns &GenericArray - // Set up AES-256-CBC decryptor - type Aes256CbcDec = Decryptor; - let cipher = Aes256CbcDec::new(key_array, iv_array); - - // Create a mutable buffer for decryption - let mut buffer = encrypted_key.clone(); // Clone to make it mutable - let decrypted = cipher - .decrypt_padded_mut::(&mut buffer) - .map_err(|e| anyhow!("Decryption failed: {}", e))?; - - // Ensure the decrypted key is exactly 32 bytes - if decrypted.len() != 32 { - return Err(anyhow!( - "Decrypted private key has invalid length: {} bytes (expected 32)", - decrypted.len() - )); - } + // Try GCM first (new format), fallback to CBC (legacy) + let result = if nonce_or_iv.len() == 12 { + // New GCM format + let cipher = Aes256Gcm::new(Key::::from_slice(&key)); + let plaintext = cipher.decrypt(Nonce::from_slice(&nonce_or_iv), encrypted_key.as_ref()) + .map_err(|e| anyhow!("GCM decryption failed: {}", e))?; + + if plaintext.len() != 32 { + return Err(anyhow!("Decrypted private key has invalid length: {} bytes (expected 32)", plaintext.len())); + } + format!("0x{}", hex::encode(&plaintext)) + } else { + return Err(anyhow!("Unsupported encryption format")); + }; - // Return the decrypted private key as a 0x-prefixed hex string - Ok(format!("0x{}", hex::encode(decrypted))) + // Zeroize sensitive data + key.zeroize(); + + Ok(result) } } diff --git a/src/utils/api_validator.rs b/src/utils/api_validator.rs new file mode 100644 index 0000000..955fe9f --- /dev/null +++ b/src/utils/api_validator.rs @@ -0,0 +1,129 @@ +use crate::api::{ApiProvider, ApiKey}; +use anyhow::{Result, anyhow}; +use reqwest::Client; +use serde_json::{json, Value}; +use std::time::Duration; + +#[derive(Debug, Clone, PartialEq)] +pub enum ValidationResult { + Valid, + Invalid(String), + NetworkError(String), +} + +pub async fn validate_api_key(api_key: &ApiKey) -> Result { + let client = Client::builder() + .timeout(Duration::from_secs(10)) + .build()?; + + match api_key.provider { + ApiProvider::RskRpc => validate_rsk_key(&client, api_key).await, + ApiProvider::Alchemy => validate_alchemy_rsk_key(&client, api_key).await, + ApiProvider::Custom(_) => Ok(ValidationResult::Valid), + } +} + +async fn validate_rsk_key(client: &Client, api_key: &ApiKey) -> Result { + let base_url = match api_key.network.as_str() { + "mainnet" => "https://public-node.rsk.co", + "testnet" => "https://public-node.testnet.rsk.co", + _ => return Ok(ValidationResult::Invalid("Unsupported Rootstock network".to_string())), + }; + + let url = format!("{}?apikey={}", base_url, api_key.key); + + let payload = json!({ + "jsonrpc": "2.0", + "method": "eth_blockNumber", + "params": [], + "id": 1 + }); + + match client.post(&url).json(&payload).send().await { + Ok(response) => { + if response.status().is_success() { + match response.json::().await { + Ok(json) => { + if json.get("error").is_some() { + let error_msg = json["error"]["message"].as_str() + .unwrap_or("Invalid API key"); + Ok(ValidationResult::Invalid(error_msg.to_string())) + } else if json.get("result").is_some() { + Ok(ValidationResult::Valid) + } else { + Ok(ValidationResult::Invalid("Unexpected response".to_string())) + } + } + Err(_) => Ok(ValidationResult::Invalid("Invalid response format".to_string())) + } + } else if response.status() == 401 || response.status() == 403 { + Ok(ValidationResult::Invalid("Invalid or expired API key".to_string())) + } else { + Ok(ValidationResult::Invalid(format!("HTTP {}", response.status()))) + } + } + Err(e) => Ok(ValidationResult::NetworkError(e.to_string())) + } +} + +async fn validate_alchemy_rsk_key(client: &Client, api_key: &ApiKey) -> Result { + // Alchemy for Rootstock (if they support it) + let network_suffix = match api_key.network.as_str() { + "mainnet" => "rsk-mainnet", + "testnet" => "rsk-testnet", + _ => return Ok(ValidationResult::Invalid("Unsupported network".to_string())), + }; + + let url = format!("https://{}.g.alchemy.com/v2/{}", network_suffix, api_key.key); + + let payload = json!({ + "jsonrpc": "2.0", + "method": "eth_blockNumber", + "params": [], + "id": 1 + }); + + match client.post(&url).json(&payload).send().await { + Ok(response) => { + if response.status().is_success() { + match response.json::().await { + Ok(json) => { + if json.get("error").is_some() { + Ok(ValidationResult::Invalid("Invalid API key".to_string())) + } else if json.get("result").is_some() { + Ok(ValidationResult::Valid) + } else { + Ok(ValidationResult::Invalid("Unexpected response".to_string())) + } + } + Err(_) => Ok(ValidationResult::Invalid("Invalid response format".to_string())) + } + } else if response.status() == 401 { + Ok(ValidationResult::Invalid("Invalid API key".to_string())) + } else { + Ok(ValidationResult::Invalid(format!("HTTP {}", response.status()))) + } + } + Err(e) => Ok(ValidationResult::NetworkError(e.to_string())) + } +} + +pub fn validate_api_key_format(provider: &ApiProvider, key: &str) -> Result<()> { + match provider { + ApiProvider::RskRpc => { + if key.is_empty() { + return Err(anyhow!("RSK RPC API key cannot be empty")); + } + } + ApiProvider::Alchemy => { + if key.len() < 32 { + return Err(anyhow!("Alchemy API key should be at least 32 characters")); + } + if !key.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') { + return Err(anyhow!("Alchemy API key contains invalid characters")); + } + } + ApiProvider::Custom(_) => {} + } + Ok(()) +} diff --git a/src/utils/constants.rs b/src/utils/constants.rs index 3b0bfbc..b94c027 100644 --- a/src/utils/constants.rs +++ b/src/utils/constants.rs @@ -1,12 +1,13 @@ use std::path::PathBuf; +use crate::utils::secure_fs; pub fn wallet_file_path() -> PathBuf { let dir = dirs::data_local_dir() .expect("Failed to get data directory") .join("rsk-rust-cli"); - // Ensure the directory exists - std::fs::create_dir_all(&dir).expect("Failed to create wallet directory"); + // Ensure the directory exists with secure permissions + secure_fs::create_dir_secure(&dir).expect("Failed to create wallet directory"); dir.join("rsk-rust-cli.json") } diff --git a/src/utils/eth.rs b/src/utils/eth.rs index 0cb3bbc..1fb2402 100644 --- a/src/utils/eth.rs +++ b/src/utils/eth.rs @@ -6,7 +6,7 @@ use alloy::primitives::{Address, B256, U256}; use alloy::providers::{Provider, ProviderBuilder, RootProvider}; use alloy::signers::local::PrivateKeySigner; use alloy::transports::http::{Client, Http}; -use alloy::network::TransactionBuilder; +use alloy::network::{EthereumWallet, TransactionBuilder}; use alloy::sol; use std::fs; use std::sync::Arc; @@ -41,7 +41,7 @@ impl EthClient { let _api_key = if let Some(key) = cli_api_key { wallet_data.api_key = Some(key.clone()); - fs::write(&wallet_file, serde_json::to_string_pretty(&wallet_data)?)?; + crate::utils::secure_fs::write_secure(&wallet_file, &serde_json::to_string_pretty(&wallet_data)?)?; Some(key) } else { wallet_data.api_key.clone() @@ -115,7 +115,12 @@ impl EthClient { .map_err(|e| anyhow!("Failed to get RBTC balance: {}", e))?; let estimated_gas_cost = U256::from(gas_price) * U256::from(100_000); if rbtc_balance < estimated_gas_cost { - return Err(anyhow!("Insufficient RBTC for gas fees")); + let balance_rbtc = rbtc_balance.to::() as f64 / 1e18; + let gas_cost_rbtc = estimated_gas_cost.to::() as f64 / 1e18; + return Err(anyhow!( + "Insufficient RBTC for gas fees. Balance: {:.6} RBTC, Required: {:.6} RBTC", + balance_rbtc, gas_cost_rbtc + )); } let chain_id = self.provider.get_chain_id().await?; @@ -128,7 +133,12 @@ impl EthClient { .await .map_err(|e| anyhow!("Failed to get token balance: {}", e))?; if token_balance._0 < amount { - return Err(anyhow!("Insufficient token balance")); + let balance_f64 = token_balance._0.to::() as f64 / 1e18; + let amount_f64 = amount.to::() as f64 / 1e18; + return Err(anyhow!( + "Insufficient token balance. Balance: {:.6}, Required: {:.6}", + balance_f64, amount_f64 + )); } use alloy::rpc::types::TransactionRequest; @@ -150,13 +160,15 @@ impl EthClient { let tx = tx.with_gas_limit(gas_estimate); + // Build transaction for signing + let tx_envelope = tx.build(&EthereumWallet::from(wallet.clone())).await?; + let pending_tx = self .provider - .send_transaction(tx) + .send_tx_envelope(tx_envelope) .await .map_err(|e| anyhow!("Failed to send token transaction: {}", e))?; - let tx_hash = pending_tx.tx_hash(); - Ok(*tx_hash) + Ok(*pending_tx.tx_hash()) } None => { if rbtc_balance < amount + estimated_gas_cost { @@ -180,13 +192,15 @@ impl EthClient { let tx = tx.with_gas_limit(gas_estimate); + // Build transaction for signing + let tx_envelope = tx.build(&EthereumWallet::from(wallet.clone())).await?; + let pending_tx = self .provider - .send_transaction(tx) + .send_tx_envelope(tx_envelope) .await .map_err(|e| anyhow!("Failed to send RBTC transaction: {}", e))?; - let tx_hash = pending_tx.tx_hash(); - Ok(*tx_hash) + Ok(*pending_tx.tx_hash()) } } } diff --git a/src/utils/helper.rs b/src/utils/helper.rs index 673fc3d..f49f30a 100644 --- a/src/utils/helper.rs +++ b/src/utils/helper.rs @@ -11,8 +11,9 @@ pub struct Config { pub wallet: WalletConfig, } -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, zeroize::Zeroize)] pub struct WalletConfig { + #[zeroize(skip)] pub current_wallet_address: Option, pub private_key: Option, pub mnemonic: Option, diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 0d0c7dd..2bdf16f 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,6 +1,10 @@ pub mod alchemy; +pub mod api; +pub mod api_validator; pub mod constants; pub mod eth; pub mod helper; +pub mod network; +pub mod secure_fs; pub mod table; pub mod terminal; diff --git a/src/utils/network.rs b/src/utils/network.rs new file mode 100644 index 0000000..3ce95df --- /dev/null +++ b/src/utils/network.rs @@ -0,0 +1,51 @@ +use std::time::Duration; +use tokio::time::timeout; + +/// Network connectivity status +#[derive(Debug, Clone, PartialEq)] +pub enum NetworkStatus { + Online, + Offline, +} + +/// Check if network connectivity is available +pub async fn check_connectivity() -> NetworkStatus { + // Try to make a simple HTTP request with a short timeout + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(3)) + .build() + .unwrap_or_default(); + + // Test with a reliable endpoint + let test_urls = [ + "https://httpbin.org/status/200", + "https://www.google.com", + "https://public-node.testnet.rsk.co", + ]; + + for url in &test_urls { + if let Ok(Ok(response)) = timeout(Duration::from_secs(2), client.get(*url).send()).await { + if response.status().is_success() { + return NetworkStatus::Online; + } + } + } + + NetworkStatus::Offline +} + +/// Features available in offline mode +pub fn get_offline_features() -> Vec<&'static str> { + vec![ + "Wallet Management", + "Contact Management", + "Token Management", + "Configuration", + "System", + ] +} + +/// Check if a feature is available offline +pub fn is_offline_feature(feature: &str) -> bool { + get_offline_features().contains(&feature) +} diff --git a/src/utils/secure_fs.rs b/src/utils/secure_fs.rs new file mode 100644 index 0000000..c444c2d --- /dev/null +++ b/src/utils/secure_fs.rs @@ -0,0 +1,43 @@ +use std::fs; +use std::path::Path; +use anyhow::Result; + +/// Write data to a file with secure permissions (0o600 - owner read/write only) +pub fn write_secure>(path: P, contents: &str) -> Result<()> { + let path = path.as_ref(); + + // Create parent directories if they don't exist + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + + // Write the file + fs::write(path, contents)?; + + // Set secure permissions on Unix systems + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(path)?.permissions(); + perms.set_mode(0o600); + fs::set_permissions(path, perms)?; + } + + Ok(()) +} + +/// Create directory with secure permissions (0o700 - owner access only) +pub fn create_dir_secure>(path: P) -> Result<()> { + let path = path.as_ref(); + fs::create_dir_all(path)?; + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(path)?.permissions(); + perms.set_mode(0o700); + fs::set_permissions(path, perms)?; + } + + Ok(()) +} diff --git a/src/utils/terminal.rs b/src/utils/terminal.rs index fd63728..b4b2140 100644 --- a/src/utils/terminal.rs +++ b/src/utils/terminal.rs @@ -1,20 +1,15 @@ use std::io::{self, Write}; -use std::process::Command; -/// Clears the terminal screen in a cross-platform way +/// Clears the terminal screen in a cross-platform way using secure method pub fn clear_screen() { - if cfg!(target_os = "windows") { - Command::new("cmd").args(["/c", "cls"]).status().unwrap(); - } else { - // For Unix-like systems - Command::new("clear").status().unwrap(); - } - + // Use clearscreen crate which doesn't rely on PATH + clearscreen::clear().ok(); + // Ensure the screen is cleared before continuing io::stdout().flush().unwrap(); } /// Shows the current wallet version pub fn show_version() { - println!("Rootstock Wallet v{}", env!("CARGO_PKG_VERSION")); + println!("Rsk Rust Cli v{}", env!("CARGO_PKG_VERSION")); } From 8d02fa11cc91c701061dfdd694221f42014aa211 Mon Sep 17 00:00:00 2001 From: cosmasken Date: Mon, 27 Oct 2025 14:46:20 +0300 Subject: [PATCH 3/4] Update Bulk Section --- README.MD | 4 +- src/commands/transfer.rs | 11 +- src/interactive/bulk_transfer.rs | 296 ++++++++++++------------------- src/types/wallet.rs | 2 +- 4 files changed, 130 insertions(+), 183 deletions(-) diff --git a/README.MD b/README.MD index a833c38..1f9272d 100644 --- a/README.MD +++ b/README.MD @@ -143,9 +143,9 @@ You can also view it at [View Transaction](https://explorer.testnet.rsk.co/tx/c7 ### Bulk Transfer Send multiple transactions at once. -[![Bulk Transfer](https://asciinema.org/a/CX1fT6B9prX6Jjg652YWWrLrR.svg)](https://asciinema.org/a/CX1fT6B9prX6Jjg652YWWrLrR) +[![Bulk Transfer](https://asciinema.org/a/AC39AUnt9U0ih5MDUWltPdJWV.svg)](https://asciinema.org/a/AC39AUnt9U0ih5MDUWltPdJWV) - [View Transaction 1](https://explorer.testnet.rsk.co/tx/87f26638a688477230855acc548595c6eb6baaf3fdb5ffba4d2b1cf788d2aaec) , [View Transaction 2](https://explorer.testnet.rsk.co/tx/de62ba82e458e52ae36f47750a86c73a9710ced3f24264a8e65780d42d77b72e) + [View Transaction 1](https://explorer.testnet.rsk.co/tx/0xfeeb73fb060f59352ff0aa99373fb502edd9702d999e8043158db18eb969ffa3) , [View Transaction 2](https://explorer.testnet.rsk.co/tx/0xbe16c77a67ee99edd492c14ca5668402d2a6a21baa39bdd2bd582ad301086951) ### Token Management diff --git a/src/commands/transfer.rs b/src/commands/transfer.rs index a4c23df..4e380e5 100644 --- a/src/commands/transfer.rs +++ b/src/commands/transfer.rs @@ -45,6 +45,11 @@ pub struct TransferCommand { impl TransferCommand { /// Execute the transfer command and return the transfer result pub async fn execute(&self) -> Result { + self.execute_with_password(None).await + } + + /// Execute the transfer command with an optional pre-validated password + pub async fn execute_with_password(&self, password: Option<&str>) -> Result { // Load wallet file and get current wallet let wallet_file = constants::wallet_file_path(); if !wallet_file.exists() { @@ -61,7 +66,11 @@ impl TransferCommand { })?; // Prompt for password and decrypt private key - let mut password = prompt_password("Enter password for the default wallet: ")?; + let mut password = if let Some(pwd) = password { + pwd.to_string() + } else { + prompt_password("Enter password for the default wallet: ")? + }; let private_key = default_wallet.decrypt_private_key(&password)?; // Zeroize password after use diff --git a/src/interactive/bulk_transfer.rs b/src/interactive/bulk_transfer.rs index 0442aca..16f7dde 100644 --- a/src/interactive/bulk_transfer.rs +++ b/src/interactive/bulk_transfer.rs @@ -1,30 +1,29 @@ use crate::{ + commands::{tokens::TokenRegistry, transfer::TransferCommand}, config::ConfigManager, - types::{network::Network, wallet::WalletData}, + types::wallet::WalletData, utils::constants, }; use anyhow::{Result, anyhow}; -use dialoguer::{Confirm, Input}; -use alloy::{ - primitives::{Address, U256}, - providers::{Provider, ProviderBuilder}, - signers::local::PrivateKeySigner, - network::{TransactionBuilder, EthereumWallet}, -}; +use dialoguer::{Confirm, Input, Select}; +use alloy::primitives::Address; use serde::Deserialize; -use std::{fs, sync::Arc}; +use std::fs; use zeroize::Zeroize; #[derive(Debug, Clone)] struct Transfer { to: Address, - value: U256, + value: String, // Keep as string to avoid precision loss + token_address: Option, + token_symbol: String, } #[derive(Debug, Deserialize)] struct TransferInput { to: String, value: String, + token: Option, // Optional token address for JSON input } /// Interactive menu for bulk token transfers @@ -32,6 +31,58 @@ pub async fn bulk_transfer() -> Result<()> { println!("\n💸 Bulk Token Transfer"); println!("====================="); + // Load config to get network + let config_manager = ConfigManager::new()?; + let config = config_manager.load()?; + let network = config.default_network.to_string().to_lowercase(); + + // Load token registry + let registry = TokenRegistry::load().unwrap_or_default(); + let mut tokens = registry.list_tokens(Some(&network)); + + // Add RBTC as the first option + tokens.insert( + 0, + ( + "RBTC (Native)".to_string(), + crate::commands::tokens::TokenInfo { + address: "0x0000000000000000000000000000000000000000".to_string(), + decimals: 18, + }, + ), + ); + + if tokens.is_empty() { + return Err(anyhow!("No tokens found for {} network", network)); + } + + // Let user select token + let token_choices: Vec = tokens.iter().map(|(name, _)| name.clone()).collect(); + let selected_token_name = Select::new() + .with_prompt("Select token to send:") + .items(&token_choices) + .interact()?; + + let selected_token_name = &token_choices[selected_token_name]; + + let (_, selected_token) = tokens + .into_iter() + .find(|(name, _)| name == selected_token_name) + .ok_or_else(|| anyhow!("Selected token not found"))?; + + // Extract token symbol from display name + let token_symbol = selected_token_name + .split_whitespace() + .next() + .unwrap_or("UNKNOWN") + .to_string(); + + let token_address = if selected_token.address == "0x0000000000000000000000000000000000000000" { + None + } else { + Some(selected_token.address.clone()) + }; + // Load wallet data let wallet_file = constants::wallet_file_path(); let wallet_data = if wallet_file.exists() { @@ -46,42 +97,18 @@ pub async fn bulk_transfer() -> Result<()> { .get_current_wallet() .ok_or_else(|| anyhow!("No active wallet found. Please select a wallet first."))?; - // Load config - let config_manager = ConfigManager::new()?; - let config = config_manager.load()?; - - // Get the network configuration - let network_config = config.default_network.get_config(); - - // Get the chain ID based on the network - let _chain_id = match config.default_network { - Network::RootStockMainnet => 30, - Network::RootStockTestnet => 31, - Network::Mainnet => 30, - Network::Testnet => 31, - Network::Regtest => 1337, - _ => return Err(anyhow!("Unsupported network for bulk transfers")), - }; - - // Prompt for password to decrypt the private key - let mut password = rpassword::prompt_password("Enter password for the wallet: ")?; - - // Decrypt the private key - let mut private_key = current_wallet.decrypt_private_key(&password)?; + // Prompt for password once at the beginning and validate it + let password = rpassword::prompt_password("Enter password for the wallet: ")?; - // Zeroize password after use - password.zeroize(); - - // Create a wallet - let wallet = private_key - .parse::() - .map_err(|e| anyhow!("Failed to parse private key: {}", e))?; - - // Create a standard provider - let provider = ProviderBuilder::new() - .on_http(network_config.rpc_url.parse()?); - - let client = Arc::new(provider); + // Validate password by trying to decrypt + match current_wallet.decrypt_private_key(&password) { + Ok(_) => { + println!("✅ Password validated successfully"); + } + Err(_) => { + return Err(anyhow!("Incorrect password. Please try again.")); + } + } // Ask if user wants to use a file or manual input let use_file = Confirm::new() @@ -108,10 +135,15 @@ pub async fn bulk_transfer() -> Result<()> { .to .parse::
() .map_err(|e| anyhow!("Invalid address {}: {}", input.to, e))?; - let value_wei = parse_amount(&input.value)?; + + // Use token from JSON or default to selected token + let transfer_token_address = input.token.or_else(|| token_address.clone()); + Ok(Transfer { to: to_addr, - value: value_wei, + value: input.value, + token_address: transfer_token_address, + token_symbol: token_symbol.clone(), }) }) .collect::>>()? @@ -142,7 +174,7 @@ pub async fn bulk_transfer() -> Result<()> { if input.starts_with("0x") && input.len() == 42 { Ok(()) } else { - Err("Please enter a valid rBTC address starting with 0x".to_string()) + Err("Please enter a valid address starting with 0x".to_string()) } }) .interact()?; @@ -152,12 +184,15 @@ pub async fn bulk_transfer() -> Result<()> { .map_err(|e| anyhow!("Invalid address: {}", e))?; let amount: String = Input::new() - .with_prompt("Amount to send (e.g., 1.0)") + .with_prompt(&format!("Amount of {} to send (e.g., 1.0)", token_symbol)) .interact()?; - let value = parse_amount(&amount)?; - - transfers.push(Transfer { to, value }); + transfers.push(Transfer { + to, + value: amount, + token_address: token_address.clone(), + token_symbol: token_symbol.clone(), + }); } transfers }; @@ -165,35 +200,19 @@ pub async fn bulk_transfer() -> Result<()> { // Show summary println!("\n📋 Transaction Summary:"); println!("===================="); - let total = transfers.iter().fold(U256::ZERO, |acc, t| acc + t.value); for (i, transfer) in transfers.iter().enumerate() { println!( - "{:2}. To: {} - Amount: {} rBTC", + "{:2}. To: {} - Amount: {} {}", i + 1, transfer.to, - format_eth(transfer.value) + transfer.value, + transfer.token_symbol ); } - println!("\nTotal to send: {} rBTC", format_eth(total)); - - // Get current gas price - let gas_price = client.get_gas_price().await?; - println!("Current gas price: {} Gwei", format_gwei(U256::from(gas_price))); - - // Estimate gas cost (21,000 gas per basic transfer) - let gas_per_tx = U256::from(21000u64); - let total_gas = gas_per_tx - .checked_mul(U256::from(transfers.len())) - .unwrap_or_default(); - let total_gas_cost = total_gas.checked_mul(U256::from(gas_price)).unwrap_or_default(); - - println!("Estimated gas cost: {} rBTC", format_eth(total_gas_cost)); - println!( - "Total cost (amount + gas): {} rBTC", - format_eth(total + total_gas_cost) - ); + println!("\nToken: {}", token_symbol); + println!("Total transactions: {}", transfers.len()); // Confirm before sending let confirm = Confirm::new() @@ -206,61 +225,34 @@ pub async fn bulk_transfer() -> Result<()> { return Ok(()); } - // Get starting nonce - let mut nonce = client.get_transaction_count(wallet.address()).await?; - - // Send transactions + // Send transactions using TransferCommand println!("\n🚀 Sending transactions..."); let mut successful = 0; let mut failed = 0; - for (i, transfer) in transfers.clone().into_iter().enumerate() { - print!("Sending {}/{}... ", i + 1, transfers.clone().len()); - - use alloy::rpc::types::TransactionRequest; - let tx = TransactionRequest::default() - .with_to(transfer.to) - .with_value(transfer.value) - .with_nonce(nonce) - .with_gas_limit(gas_per_tx.try_into().unwrap_or(0u64)) - .with_gas_price(gas_price.try_into().unwrap_or(0u128)); - - // Build and sign transaction - match tx.build(&EthereumWallet::from(wallet.clone())).await { - Ok(tx_envelope) => { - match client.send_tx_envelope(tx_envelope).await { - Ok(pending_tx) => { - let tx_hash = *pending_tx.tx_hash(); - nonce += 1; // Increment nonce for next transaction - match client.get_transaction_receipt(tx_hash).await { - Ok(Some(receipt)) => { - if receipt.status() { - println!("✅ Success! Tx: {:?}", receipt.transaction_hash); - successful += 1; - } else { - println!("❌ Failed! Tx: {:?}", receipt.transaction_hash); - failed += 1; - } - } - Ok(None) => { - println!("❌ Transaction was dropped from the mempool"); - failed += 1; - } - Err(e) => { - println!("❌ Error getting receipt: {}", e); - failed += 1; - } - } - } - Err(e) => { - println!("❌ Failed to send transaction: {}", e); - failed += 1; - } - } + for (i, transfer) in transfers.iter().enumerate() { + print!("Sending {}/{}... ", i + 1, transfers.len()); + + let transfer_cmd = TransferCommand { + address: format!("{:?}", transfer.to), + value: transfer.value.clone(), + token: transfer.token_address.clone(), + }; + + match transfer_cmd.execute_with_password(Some(&password)).await { + Ok(result) => { + println!("✅ Success! Tx: {:?}", result.tx_hash); + successful += 1; } Err(e) => { - println!("❌ Failed to build/sign transaction: {}", e); + // Check if it's a password error and provide better message + let error_msg = if e.to_string().contains("Incorrect password") { + "Incorrect password entered" + } else { + &e.to_string() + }; + println!("❌ Failed: {}", error_msg); failed += 1; } } @@ -270,71 +262,17 @@ pub async fn bulk_transfer() -> Result<()> { } println!("\n📊 Transaction Summary:"); - println! ("====================" ); + println!("===================="); println!("Total transactions: {}", successful + failed); println!("✅ Successful: {}", successful); println!("❌ Failed: {}", failed); - // Zeroize sensitive data - private_key.zeroize(); + // Clean up password from memory + let mut password_mut = password; + password_mut.zeroize(); Ok(()) } -/// Parse amount string (e.g., "1.0" or "0.5") into wei -fn parse_amount(amount: &str) -> Result { - let parts: Vec<&str> = amount.split('.').collect(); - match parts.len() { - 1 => { - // Whole number - let whole = parts[0] - .parse::() - .map_err(|_| anyhow!("Invalid amount: {}", amount))?; - Ok(U256::from(whole) * U256::from(10u128).pow(U256::from(18))) - } - 2 => { - // With decimal part - let whole = parts[0] - .parse::() - .map_err(|_| anyhow!("Invalid amount: {}", amount))?; - let decimals = parts[1]; - let decimals = if decimals.len() > 18 { - &decimals[..18] - } else { - decimals - }; - - let decimal_part = decimals - .parse::() - .map_err(|_| anyhow!("Invalid decimal part: {}", decimals))?; - let decimal_places = decimals.len() as u32; - - let value = U256::from(whole) * U256::from(10u128).pow(U256::from(18)) - + U256::from(decimal_part) * U256::from(10u128).pow(U256::from(18 - decimal_places as usize)); - - Ok(value) - } - _ => Err(anyhow!("Invalid amount format: {}", amount)), - } -} - -/// Format wei amount to rBTC with 6 decimal places -fn format_eth(wei: U256) -> String { - let wei_str = wei.to_string(); - let len = wei_str.len(); - if len <= 18 { - format!("0.{:0>18}", wei_str) - } else { - let (whole, decimal) = wei_str.split_at(len - 18); - let decimal = &decimal[..6.min(decimal.len())]; // Show up to 6 decimal places - format!("{}.{}", whole, decimal) - } -} - -/// Format wei to Gwei -fn format_gwei(wei: U256) -> String { - let gwei = wei / U256::from(1_000_000_000u64); - format!("{} Gwei", gwei) -} diff --git a/src/types/wallet.rs b/src/types/wallet.rs index 3551e65..7422cf4 100644 --- a/src/types/wallet.rs +++ b/src/types/wallet.rs @@ -104,7 +104,7 @@ impl Wallet { // New GCM format let cipher = Aes256Gcm::new(Key::::from_slice(&key)); let plaintext = cipher.decrypt(Nonce::from_slice(&nonce_or_iv), encrypted_key.as_ref()) - .map_err(|e| anyhow!("GCM decryption failed: {}", e))?; + .map_err(|_| anyhow!("Incorrect password. Please try again."))?; if plaintext.len() != 32 { return Err(anyhow!("Decrypted private key has invalid length: {} bytes (expected 32)", plaintext.len())); From 7a4397cc150d0494e31048051649bc2b2ea24dfe Mon Sep 17 00:00:00 2001 From: cosmasken Date: Mon, 10 Nov 2025 07:47:04 +0300 Subject: [PATCH 4/4] fix: add comprehensive zeroization for sensitive data - Fix L44: Zeroize temporary private key bytes in wallet.new() - Fix L106: Zeroize plaintext after decryption in decrypt_private_key() - Fix L34: Add Drop trait for WalletData to zeroize api_key - Fix L74: Zeroize private_key string in transfer command - Fix L100,L181,L330: Zeroize password strings in interactive flows - Fix L143,L340: Zeroize private_key strings in interactive flows - Fix L22,L25,L27: Add Drop trait for WalletAction enum Addresses Issue 9 - Missing zeroization points for sensitive data --- src/commands/transfer.rs | 3 ++- src/commands/wallet.rs | 16 ++++++++++++++++ src/interactive/wallet.rs | 20 ++++++++++++++------ src/types/wallet.rs | 18 +++++++++++++++--- 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/src/commands/transfer.rs b/src/commands/transfer.rs index 4e380e5..7e88cc6 100644 --- a/src/commands/transfer.rs +++ b/src/commands/transfer.rs @@ -71,7 +71,7 @@ impl TransferCommand { } else { prompt_password("Enter password for the default wallet: ")? }; - let private_key = default_wallet.decrypt_private_key(&password)?; + let mut private_key = default_wallet.decrypt_private_key(&password)?; // Zeroize password after use password.zeroize(); @@ -198,6 +198,7 @@ impl TransferCommand { ); // Zeroize sensitive data before returning + private_key.zeroize(); private_key_copy.zeroize(); Ok(TransferResult { diff --git a/src/commands/wallet.rs b/src/commands/wallet.rs index d3635d7..d52c6ee 100644 --- a/src/commands/wallet.rs +++ b/src/commands/wallet.rs @@ -4,6 +4,7 @@ use anyhow::{Result, anyhow}; use clap::Parser; use colored::Colorize; use alloy::signers::local::PrivateKeySigner; +use zeroize::Zeroize; use std::fs; use std::path::{Path, PathBuf}; @@ -43,6 +44,21 @@ pub enum WalletAction { }, } +impl Drop for WalletAction { + fn drop(&mut self) { + match self { + WalletAction::Create { password, .. } => { + password.zeroize(); + } + WalletAction::Import { private_key, password, .. } => { + private_key.zeroize(); + password.zeroize(); + } + _ => {} + } + } +} + impl WalletCommand { pub async fn execute(&self) -> Result<()> { let config = Config::default(); // Use default config diff --git a/src/interactive/wallet.rs b/src/interactive/wallet.rs index 455d09f..68e6671 100644 --- a/src/interactive/wallet.rs +++ b/src/interactive/wallet.rs @@ -97,7 +97,7 @@ pub async fn create_wallet_with_name(name: &str) -> Result<()> { style("This password will be required to access your wallet.").dim() ); - let password = inquire::Password::new("Enter password:") + let mut password = inquire::Password::new("Enter password:") .with_display_toggle_enabled() .with_display_mode(inquire::PasswordDisplayMode::Masked) .with_custom_confirmation_error_message("The passwords don't match.") @@ -112,6 +112,7 @@ pub async fn create_wallet_with_name(name: &str) -> Result<()> { ); let mut password_copy = password.clone(); + password.zeroize(); let cmd = WalletCommand { action: WalletAction::Create { name: name.to_string(), @@ -140,7 +141,7 @@ async fn import_wallet() -> Result<()> { style("This should start with '0x' followed by 64 hexadecimal characters.").dim() ); - let private_key = inquire::Text::new("Private key (0x...):") + let mut private_key = inquire::Text::new("Private key (0x...):") .with_help_message("The private key of the wallet to import (will be masked)") .with_validator(|input: &str| { if !input.starts_with("0x") { @@ -178,7 +179,7 @@ async fn import_wallet() -> Result<()> { style("This password will be required to access your wallet.").dim() ); - let password = inquire::Password::new("Enter password:") + let mut password = inquire::Password::new("Enter password:") .with_display_toggle_enabled() .with_display_mode(inquire::PasswordDisplayMode::Masked) .with_custom_confirmation_error_message("The passwords don't match.") @@ -194,6 +195,8 @@ async fn import_wallet() -> Result<()> { let mut private_key_copy = private_key.clone(); let mut password_copy = password.clone(); + private_key.zeroize(); + password.zeroize(); let cmd = WalletCommand { action: WalletAction::Import { private_key: private_key_copy.clone(), @@ -327,7 +330,7 @@ async fn export_private_key() -> Result<()> { anyhow::anyhow!("No wallet selected") })?; - let password = inquire::Password::new("Enter wallet password:") + let mut password = inquire::Password::new("Enter wallet password:") .with_display_mode(inquire::PasswordDisplayMode::Masked) .prompt()?; @@ -337,12 +340,17 @@ async fn export_private_key() -> Result<()> { ); match current_wallet.decrypt_private_key(&password) { - Ok(private_key) => { + Ok(mut private_key) => { + password.zeroize(); println!("\n{}", style("Your Private Key:").bold()); println!("{}", style(&private_key).cyan().bold()); + private_key.zeroize(); println!("\n{}", style("⚠️ Keep this safe and never share it!").red()); } - Err(_) => println!("{}", style("❌ Incorrect password").red()), + Err(_) => { + password.zeroize(); + println!("{}", style("❌ Incorrect password").red()); + } } Ok(()) diff --git a/src/types/wallet.rs b/src/types/wallet.rs index 7422cf4..3b5cb5d 100644 --- a/src/types/wallet.rs +++ b/src/types/wallet.rs @@ -34,14 +34,24 @@ pub struct WalletData { pub api_key: Option, } +impl Drop for WalletData { + fn drop(&mut self) { + if let Some(ref mut key) = self.api_key { + key.zeroize(); + } + } +} + impl Wallet { pub fn address(&self) -> Address { self.address } pub fn new(wallet: PrivateKeySigner, name: &str, password: &str) -> Result { + let mut private_key_bytes = wallet.to_bytes().to_vec(); let (encrypted_key, iv, salt) = - Self::encrypt_private_key(wallet.to_bytes().as_ref(), password)?; + Self::encrypt_private_key(&private_key_bytes, password)?; + private_key_bytes.zeroize(); Ok(Self { address: wallet.address(), balance: U256::ZERO, @@ -103,13 +113,15 @@ impl Wallet { let result = if nonce_or_iv.len() == 12 { // New GCM format let cipher = Aes256Gcm::new(Key::::from_slice(&key)); - let plaintext = cipher.decrypt(Nonce::from_slice(&nonce_or_iv), encrypted_key.as_ref()) + let mut plaintext = cipher.decrypt(Nonce::from_slice(&nonce_or_iv), encrypted_key.as_ref()) .map_err(|_| anyhow!("Incorrect password. Please try again."))?; if plaintext.len() != 32 { return Err(anyhow!("Decrypted private key has invalid length: {} bytes (expected 32)", plaintext.len())); } - format!("0x{}", hex::encode(&plaintext)) + let result = format!("0x{}", hex::encode(&plaintext)); + plaintext.zeroize(); + result } else { return Err(anyhow!("Unsupported encryption format")); };