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..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 = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "opaque-debug"
+version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
+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]]
@@ -4085,10 +4097,22 @@ 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",
+ "aes-gcm",
"alloy",
"alloy-consensus",
"alloy-contract",
@@ -4111,10 +4135,10 @@ dependencies = [
"csv",
"dialoguer",
"dirs",
- "dotenv",
+ "dotenvy",
"env_logger",
"eth-keystore",
- "generic-array",
+ "generic-array 1.3.4",
"hex",
"image",
"inquire",
@@ -4130,25 +4154,14 @@ 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",
]
-[[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"
@@ -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 54ebd24..a66de2d 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"
@@ -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 e67e4bd..1f9272d 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,73 +97,65 @@ The wallet features an intuitive interactive interface. Simply run:
cargo run
```
-## Demo
+### š Individual Feature Demos
-### First Time Setup
-
-[](https://asciinema.org/a/V8Qf1AboSZu08l12KJFxTfyeV)
-
-### Importing Existing Wallet
-
-[](https://asciinema.org/a/cVrvEOP4LvJQLvTitxcayRBFt)
+For detailed walkthroughs of specific features:
-### Wallet Creation
-
-[](https://asciinema.org/a/Lj7YVm8idPbAEpHYPfjFuTfns)
-
-### List Wallets & Switch Wallets
+### First Time Setup
-[](https://asciinema.org/a/HUpONPUC4OgQKFyStNm5KwaqT)
+[]( https://asciinema.org/a/lbHMriiOt5ZNfpyP3LnjCRJ6e)
-### Delete Wallet & Rename Wallet
+### Wallet Features
-[](https://asciinema.org/a/cqjxdJjfaY1wqvO4upgRUMKDG)
+Create Wallet, Importing Existing Wallet, List Wallets, Switch Wallets, Delete Wallet & Rename Wallet
+[](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)
-[](https://asciinema.org/a/rqlxYIWZ2Gh1Pn5sEFZZFK5xz
+[](https://asciinema.org/a/ChSOgxrI2VXuA5Yq6Q0Jgc2nI
)
### Send Funds
Transfer RBTC from one wallet to another (e.g., from personal to lock wallet).
-[](https://asciinema.org/a/tEt4dSOgmEEguP6pgQ7setuPV
+[](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.
-[](https://asciinema.org/a/6ZdWMvEMMZVsSCFkNHjq3MbPq)
+Configure and validate your API key for accessing transaction history and status checking.
+[](https://asciinema.org/a/LPdZYLNWcCK0feJyvcXDFZSXn)
-### Transaction History
+### Transaction History(make sure alchemy api key is set)
[](https://asciinema.org/a/Sh7qW67bHkDz0KGSjiqBLc8JC)
### Check Transaction Status
-[](https://asciinema.org/a/CYYjrSV58KRGMgOmbdknm85Am)
+[](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
Send multiple transactions at once.
-[](https://asciinema.org/a/CX1fT6B9prX6Jjg652YWWrLrR)
+[](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
Add, list, and delete tokens from your wallet.
-[](https://asciinema.org/a/dY7GpSsmk6uxsZdB9n6C2Fzgs)
+[](https://asciinema.org/a/H4vFOowEHUDBi9txo8PGntedp)
### Contact
Add, list, and delete contacts from your address book.
-[](https://asciinema.org/a/QaN1SZIXud4dxJ8woqDqnb1v5)
+[](https://asciinema.org/a/q7aRPz3nUxuwopSSUFTZlNmxj)
## š Troubleshooting
@@ -177,10 +170,18 @@ 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
-- 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)
@@ -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/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/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..7e88cc6 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)]
@@ -44,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() {
@@ -60,8 +66,16 @@ impl TransferCommand {
})?;
// Prompt for password and decrypt private key
- let password = prompt_password("Enter password for the default wallet: ")?;
- let private_key = default_wallet.decrypt_private_key(&password)?;
+ let mut password = if let Some(pwd) = password {
+ pwd.to_string()
+ } else {
+ prompt_password("Enter password for the default wallet: ")?
+ };
+ let mut 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 +83,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 +122,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 +197,10 @@ impl TransferCommand {
status_str
);
+ // Zeroize sensitive data before returning
+ private_key.zeroize();
+ 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..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
@@ -87,7 +103,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 +127,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 +170,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 +198,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 +223,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 +259,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/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..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;
@@ -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(())
}
@@ -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..16f7dde 100644
--- a/src/interactive/bulk_transfer.rs
+++ b/src/interactive/bulk_transfer.rs
@@ -1,29 +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,
-};
+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
@@ -31,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() {
@@ -45,39 +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
+ // Prompt for password once at the beginning and validate it
let password = rpassword::prompt_password("Enter password for the wallet: ")?;
-
- // Decrypt the private key
- let private_key = current_wallet.decrypt_private_key(&password)?;
-
- // 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
- 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()
@@ -104,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::>>()?
@@ -138,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()?;
@@ -148,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
};
@@ -161,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()
@@ -202,47 +225,34 @@ pub async fn bulk_transfer() -> Result<()> {
return Ok(());
}
- // 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_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;
- }
- }
- Ok(None) => {
- println!("ā Transaction was dropped from the mempool");
- failed += 1;
- }
- Err(e) => {
- println!("ā Error: {}", 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 send 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;
}
}
@@ -257,63 +267,12 @@ pub async fn bulk_transfer() -> Result<()> {
println!("ā
Successful: {}", successful);
println!("ā Failed: {}", failed);
- Ok(())
-}
+ // Clean up password from memory
+ let mut password_mut = password;
+ password_mut.zeroize();
-/// 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)),
- }
+ Ok(())
}
-/// 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/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..68e6671 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,
@@ -74,12 +97,13 @@ 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.")
.with_custom_confirmation_message("Please confirm your password:")
.with_formatter(&|_| String::from("ā Password set"))
+ .with_validator(validate_password)
.prompt()?;
println!(
@@ -87,15 +111,21 @@ 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();
+ password.zeroize();
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 +141,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 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") {
+ 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:")
@@ -129,12 +179,13 @@ 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.")
.with_custom_confirmation_message("Please confirm your password:")
.with_formatter(&|_| String::from("ā Password set"))
+ .with_validator(validate_password)
.prompt()?;
println!(
@@ -142,17 +193,34 @@ 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();
+ private_key.zeroize();
+ password.zeroize();
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 +294,68 @@ 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 mut 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(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(_) => {
+ password.zeroize();
+ 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..3b5cb5d 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;
@@ -37,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,
@@ -63,28 +70,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 +102,34 @@ 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 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()));
+ }
+ let result = format!("0x{}", hex::encode(&plaintext));
+ plaintext.zeroize();
+ result
+ } 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.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/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 26a523c..b94c027 100644
--- a/src/utils/constants.rs
+++ b/src/utils/constants.rs
@@ -1,14 +1,15 @@
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("rootstock-wallet");
+ .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("rootstock-wallet.json")
+ dir.join("rsk-rust-cli.json")
}
pub const METHOD_TYPES: &str = "read";
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 a3f871b..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,
@@ -69,7 +70,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()
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"));
}