diff --git a/.cargo/config.toml b/.cargo/config.toml index add62220f6..1a9c1dcd47 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -7,6 +7,8 @@ ci-check-nostd = "check --no-default-features -F serde -p iota-sdk --target risc ci-check-types = "check --no-default-features -p iota-sdk" ci-test = "nextest run --all-features --profile ci --cargo-profile ci -p iota-sdk -p iota-sdk-bindings-core" +ci-tangle-test = "nextest run --tests --all-features --run-ignored ignored-only --profile ci --cargo-profile ci -p iota-sdk -p iota-sdk-bindings-core" +ci-coverage = "llvm-cov nextest --lcov --output-path lcov.info --tests -p iota-sdk --all-features --run-ignored all --profile ci" ci-clippy = "clippy --all-targets --all-features -- -D warnings" @@ -21,6 +23,4 @@ ci-license = "license-template --template .license_template" # # - RUSTSEC-2021-0065: https://rustsec.org/advisories/RUSTSEC-2021-0065 # - anymap is unmaintained 🤷‍♀️ -# - RUSTSEC-2023-0052: https://rustsec.org/advisories/RUSTSEC-2023-0052 -# - TODO: waiting for fix in dependency -ci-audit = "audit --file Cargo.lock --deny warnings --ignore RUSTSEC-2021-0065 --ignore RUSTSEC-2023-0052" +ci-audit = "audit --file Cargo.lock --deny warnings --ignore RUSTSEC-2021-0065" diff --git a/.config/nextest.toml b/.config/nextest.toml index 4c00415a4f..448dccbba0 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -6,3 +6,7 @@ fail-fast = false retries = 2 test-threads = "num-cpus" slow-timeout = { period = "60s", terminate-after = 2 } + +[[profile.ci.overrides]] +filter = 'test(/^pow::/)' +slow-timeout = "5m" diff --git a/.github/workflows/bindings-nodejs-publish.yml b/.github/workflows/bindings-nodejs-publish.yml index 7bec4952f8..3a5f76218b 100644 --- a/.github/workflows/bindings-nodejs-publish.yml +++ b/.github/workflows/bindings-nodejs-publish.yml @@ -88,8 +88,8 @@ jobs: with: python-version: "3.10" - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + - name: Set up Rust + uses: ./.github/actions/setup-rust # This step can be removed as soon as official Windows arm64 builds are published: # https://github.com/nodejs/build/issues/2450#issuecomment-705853342 diff --git a/.github/workflows/bindings-wallet-covector-publish.yml b/.github/workflows/bindings-wallet-covector-publish.yml index 429216b1d1..344ee9bbb7 100644 --- a/.github/workflows/bindings-wallet-covector-publish.yml +++ b/.github/workflows/bindings-wallet-covector-publish.yml @@ -89,8 +89,8 @@ jobs: with: python-version: "3.10" - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + - name: Set up Rust + uses: ./.github/actions/setup-rust # This step can be removed as soon as official Windows arm64 builds are published: # https://github.com/nodejs/build/issues/2450#issuecomment-705853342 diff --git a/.github/workflows/bindings-wasm-publish.yml b/.github/workflows/bindings-wasm-publish.yml index b3ce1b40ae..b1ab3c026d 100644 --- a/.github/workflows/bindings-wasm-publish.yml +++ b/.github/workflows/bindings-wasm-publish.yml @@ -16,8 +16,8 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Install stable toolchain - uses: dtolnay/rust-toolchain@stable + - name: Set up Rust + uses: ./.github/actions/setup-rust with: target: "wasm32-unknown-unknown" diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index cc032c53a8..1c260a1603 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -4,6 +4,7 @@ on: push: branches: [develop, production, 1.1] paths: + - ".cargo/config.toml" - ".github/workflows/build-and-test.yml" - ".github/actions/**" - "**.rs" # Include all rust files @@ -13,6 +14,7 @@ on: pull_request: branches: [develop, production, 1.1] paths: + - ".cargo/config.toml" - ".github/workflows/build-and-test.yml" - ".github/actions/**" - "**.rs" # Include all rust files @@ -50,4 +52,4 @@ jobs: uses: taiki-e/install-action@nextest - name: Run tests - run: cargo nextest run --all-features --profile ci --cargo-profile ci -p iota-sdk -p iota-sdk-bindings-core + run: cargo ci-test diff --git a/.github/workflows/cli-publish.yml b/.github/workflows/cli-publish.yml index 365586eaf8..aea483b561 100644 --- a/.github/workflows/cli-publish.yml +++ b/.github/workflows/cli-publish.yml @@ -56,8 +56,9 @@ jobs: steps: - uses: actions/checkout@v3 - - name: install rust stable - uses: dtolnay/rust-toolchain@stable + + - name: Set up Rust + uses: ./.github/actions/setup-rust - name: Install required packages (Ubuntu) if: matrix.os == 'ubuntu-latest' diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 435a078b01..ba813b3826 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -4,6 +4,7 @@ on: push: branches: [develop, production, 1.1] paths: + - ".cargo/config.toml" - ".github/workflows/coverage.yml" - ".github/actions/**" - "coverage.sh" @@ -54,16 +55,7 @@ jobs: uses: "./.github/actions/ledger-nano" - name: Collect coverage data - run: > - cargo llvm-cov nextest - --lcov - --output-path coverage.info - --tests - --package iota-sdk - --all-features - --run-ignored all - --test-threads "num-cpus" - --retries 2 + run: cargo ci-coverage - name: Tear down private tangle if: always() @@ -73,5 +65,4 @@ jobs: uses: coverallsapp/github-action@v2.2.1 with: github-token: ${{ secrets.GITHUB_TOKEN }} - path-to-lcov: coverage.info flag-name: Unit diff --git a/.github/workflows/private-tangle-tests.yml b/.github/workflows/private-tangle-tests.yml index 48dfda2b02..60bad53595 100644 --- a/.github/workflows/private-tangle-tests.yml +++ b/.github/workflows/private-tangle-tests.yml @@ -4,6 +4,7 @@ on: push: branches: [develop, production, 1.1] paths: + - ".cargo/config.toml" - ".github/workflows/private-tangle-tests.yml" - ".github/actions/**" - "**.rs" @@ -15,6 +16,7 @@ on: pull_request: branches: [develop, production, 1.1] paths: + - ".cargo/config.toml" - ".github/workflows/private-tangle-tests.yml" - ".github/actions/**" - "**.rs" @@ -42,8 +44,8 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Install toolchain - uses: dtolnay/rust-toolchain@stable + - name: Set up Rust + uses: ./.github/actions/setup-rust - name: Install required packages run: | @@ -63,7 +65,7 @@ jobs: uses: "./.github/actions/ledger-nano" - name: Run tests - run: cargo nextest run --tests --all-features --run-ignored ignored-only --profile ci --cargo-profile ci -p iota-sdk -p iota-sdk-bindings-core + run: cargo ci-tangle-test - name: Tear down private tangle if: always() diff --git a/Cargo.lock b/Cargo.lock index 6c49c54252..0b0026fe57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,9 +65,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a" +checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783" dependencies = [ "memchr", ] @@ -158,7 +158,7 @@ checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.31", ] [[package]] @@ -273,7 +273,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.29", + "syn 2.0.31", ] [[package]] @@ -338,6 +338,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bs58" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5353f36341f7451062466f0b755b96ac3a9547e4d7f6b70d603fc721a7d7896" + [[package]] name = "bumpalo" version = "3.13.0" @@ -352,9 +358,9 @@ checksum = "c3ac9f8b63eca6fd385229b3675f6cc0dc5c8a5c8a54a59d4f52ffd670d87b0c" [[package]] name = "bytemuck" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" +checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" [[package]] name = "byteorder" @@ -364,9 +370,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "bzip2-sys" @@ -430,9 +436,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.27" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f56b4c72906975ca04becb8a30e102dfecddd0c06181e3e95ddc444be28881f8" +checksum = "d87d9d13be47a5b7c3907137f1290b0459a7f80efb26be8c52afb11963bccb02" dependencies = [ "num-traits", ] @@ -461,20 +467,19 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.1" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c8d502cbaec4595d2e7d5f61e318f05417bd2b66fdc3809498f0d3fdf0bea27" +checksum = "6a13b88d2c62ff462f88e4a121f17a82c1af05693a2f192b5c38d14de73c19f6" dependencies = [ "clap_builder", "clap_derive", - "once_cell", ] [[package]] name = "clap_builder" -version = "4.4.1" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5891c7bc0edb3e1c2204fc5e94009affabeb1821c9e5fdc3959536c5c0bb984d" +checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08" dependencies = [ "anstream", "anstyle", @@ -484,14 +489,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.4.0" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9fd1a5729c4548118d7d70ff234a44868d00489a4b6597b0b020918a0e91a1a" +checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.31", ] [[package]] @@ -631,9 +636,9 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "crypto-bigint" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4c2f4e1afd912bc40bfd6fed5d9dc1f288e0ba01bfcc835cc5bc3eb13efe15" +checksum = "740fe28e594155f10cfc383984cbefd529d7396050557148f79cb0f621204124" dependencies = [ "generic-array", "rand_core 0.6.4", @@ -698,7 +703,7 @@ checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.31", ] [[package]] @@ -1106,7 +1111,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.31", ] [[package]] @@ -1555,12 +1560,13 @@ dependencies = [ [[package]] name = "iota-sdk" -version = "1.0.2" +version = "1.0.3" dependencies = [ "anymap", "async-trait", "bech32 0.9.1", "bitflags 2.4.0", + "bs58", "bytemuck", "derive_more", "dotenvy", @@ -1915,9 +1921,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.6.1" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f478948fd84d9f8e86967bf432640e46adfb5a4bd4f14ef7e864ab38220534ae" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" [[package]] name = "memoffset" @@ -2077,9 +2083,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.0" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ac5bbd07aea88c60a577a1ce218075ffd59208b2d7ca97adf9bfc5aeb21ebe" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" dependencies = [ "memchr", ] @@ -2143,9 +2149,9 @@ dependencies = [ [[package]] name = "parity-scale-codec-derive" -version = "3.6.5" +version = "3.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312270ee71e1cd70289dacf597cab7b207aa107d2f28191c2ae45b2ece18a260" +checksum = "a65cebc1b089c96df6203a76279a82b4bbf04fa23659c4093cac6fd245c25d1f" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -2240,7 +2246,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.31", ] [[package]] @@ -2319,12 +2325,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c64d9ba0963cdcea2e1b2230fbae2bab30eb25a174be395c41e764bfb65dd62" +checksum = "8832c0f9be7e3cae60727e6256cfd2cd3c3e2b6cd5dad4190ecb2fd658c9030b" dependencies = [ "proc-macro2", - "syn 2.0.29", + "syn 2.0.31", ] [[package]] @@ -2524,9 +2530,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.4" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12de2eff854e5fa4b1295edd650e227e9d8fb0c9e90b12e7f36d6a6811791a29" +checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" dependencies = [ "aho-corasick", "memchr", @@ -2536,9 +2542,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49530408a136e16e5b486e883fbb6ba058e8e4e8ae6621a77b048b314336e629" +checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" dependencies = [ "aho-corasick", "memchr", @@ -2687,9 +2693,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.10" +version = "0.38.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed6248e1caa625eb708e266e06159f135e8c26f2bb7ceb72dc4b2766d0340964" +checksum = "c0c3dde1fc030af041adc40e79c0e7fbcf431dd24870053d187d7c66e4b87453" dependencies = [ "bitflags 2.4.0", "errno", @@ -2887,7 +2893,7 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.31", ] [[package]] @@ -2909,7 +2915,7 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.31", ] [[package]] @@ -2964,9 +2970,9 @@ checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" [[package]] name = "shlex" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" +checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" [[package]] name = "signature" @@ -3149,9 +3155,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.29" +version = "2.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" +checksum = "718fa2415bcb8d8bd775917a1bf12a7931b6dfa890753378538118181e0cb398" dependencies = [ "proc-macro2", "quote", @@ -3183,22 +3189,22 @@ checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a" [[package]] name = "thiserror" -version = "1.0.47" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f" +checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.47" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" +checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.31", ] [[package]] @@ -3289,7 +3295,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.31", ] [[package]] @@ -3502,9 +3508,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "walkdir" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" dependencies = [ "same-file", "winapi-util", @@ -3548,7 +3554,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.31", "wasm-bindgen-shared", ] @@ -3582,7 +3588,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.31", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3616,9 +3622,9 @@ dependencies = [ [[package]] name = "webpki" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +checksum = "f0e74f82d49d545ad128049b7e88f6576df2da6b02e9ce565c6f533be576957e" dependencies = [ "ring", "untrusted", @@ -3913,5 +3919,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.31", ] diff --git a/README.md b/README.md index f99bb41652..c0df14bca9 100644 --- a/README.md +++ b/README.md @@ -112,88 +112,20 @@ To start using the IOTA SDK in your Rust project, you can include the following ```toml [dependencies] -iota-sdk = { git = "https://github.com/iotaledger/iota-sdk" branch = "develop" } +iota-sdk = { git = "https://github.com/iotaledger/iota-sdk", branch = "develop" } ``` ## Client Usage -The following example creates a [`Client`](https://docs.rs/iota-sdk/latest/iota_sdk/client/core/struct.Client.html) -instance connected to -the [Shimmer Testnet](https://api.testnet.shimmer.network), and retrieves the node's information by -calling [`Client.get_info()`](https://docs.rs/iota-sdk/latest/iota_sdk/client/core/struct.Client.html#method.get_info), -and then print the node's information. - -```rust -use iota_sdk::client::{ - Client, -}; - -#[tokio::main] -async fn main() -> Result<()> { - let client = Client::builder() - .with_node("https://api.testnet.shimmer.network")? // Insert your node URL here - .finish() - .await?; - - let info = client.get_info().await?; - println!("Node Info: {info:?}") - - Ok(()) -} -``` +The following example creates a Client instance connected to the Shimmer Testnet, and retrieves the node's information by calling `Client.get_info()`, and then print the node's information. + +[sdk/examples/client/getting_started.rs](sdk/examples/client/getting_started.rs) ## Wallet Usage -The following example will create a -new [`Wallet`](https://docs.rs/iota-sdk/latest/iota_sdk/wallet/core/struct.Wallet.html) [`Account`](https://docs.rs/iota-sdk/latest/iota_sdk/wallet/account/struct.Account.html) -that connects to the [Shimmer Testnet](https://api.testnet.shimmer.network) using the -[`StrongholdSecretManager`](https://docs.rs/iota-sdk/latest/iota_sdk/client/secret/stronghold/type.StrongholdSecretManager.html) -to store a mnemonic. - -```rust -use iota_sdk::{ - client::{ - constants::SHIMMER_COIN_TYPE, - secret::{stronghold::StrongholdSecretManager, SecretManager}, - }, - wallet::{ClientOptions, Result, Wallet}, -}; -use std::path::PathBuf; - -#[tokio::main] -async fn main() -> Result<()> { - // Setup Stronghold secret manager. - // WARNING: Never hardcode passwords in production code. - let secret_manager = StrongholdSecretManager::builder() - .password("password") // A password to encrypt the stored data. - .build(PathBuf::from("vault.stronghold"))?; // The path to store the account snapshot. - - let client_options = ClientOptions::new().with_node("https://api.testnet.shimmer.network")?; - - // Set up and store the wallet. - let wallet = Wallet::builder() - .with_secret_manager(SecretManager::Stronghold(secret_manager)) - .with_client_options(client_options) - .with_coin_type(SHIMMER_COIN_TYPE) - .finish() - .await?; - - // Generate a mnemonic and store it in the Stronghold vault. - // INFO: It is best practice to back up the mnemonic somewhere secure. - let mnemonic = wallet.generate_mnemonic()?; - wallet.store_mnemonic(mnemonic).await?; - - // Create an account. - let account = wallet - .create_account() - .with_alias("Alice") // A name to associate with the created account. - .finish() - .await?; - - - Ok(()) -} -``` +The following example will create a new Wallet Account using a StrongholdSecretManager. For this `features = ["stronghold"]` is needed in the Cargo.toml import. To persist the wallet in a database, `"rocksdb"` can be added. + +[sdk/examples/wallet/getting_started.rs](sdk/examples/wallet/getting_started.rs) ## Examples diff --git a/bindings/core/Cargo.toml b/bindings/core/Cargo.toml index 785aa72bf5..4ab7562e5f 100644 --- a/bindings/core/Cargo.toml +++ b/bindings/core/Cargo.toml @@ -14,7 +14,7 @@ iota-sdk = { path = "../../sdk", default-features = false, features = [ "tls", ] } -backtrace = { version = "0.3.68", default-features = false, features = ["std"] } +backtrace = { version = "0.3.69", default-features = false, features = ["std"] } derivative = { version = "2.2.0", default-features = false } fern-logger = { version = "0.5.0", default-features = false } futures = { version = "0.3.28", default-features = false } @@ -26,10 +26,10 @@ log = { version = "0.4.20", default-features = false } packable = { version = "0.8.1", default-features = false } prefix-hex = { version = "0.7.1", default-features = false } primitive-types = { version = "0.12.1", default-features = false } -serde = { version = "1.0.183", default-features = false } +serde = { version = "1.0.188", default-features = false } serde_json = { version = "1.0.105", default-features = false } -thiserror = { version = "1.0.46", default-features = false } -tokio = { version = "1.31.0", default-features = false } +thiserror = { version = "1.0.48", default-features = false } +tokio = { version = "1.32.0", default-features = false } zeroize = { version = "1.6.0", default-features = false } [features] diff --git a/bindings/core/src/method/wallet.rs b/bindings/core/src/method/wallet.rs index 90960d056a..6cbd87b2f8 100644 --- a/bindings/core/src/method/wallet.rs +++ b/bindings/core/src/method/wallet.rs @@ -52,6 +52,11 @@ pub enum WalletMethod { /// Read accounts. /// Expected response: [`Accounts`](crate::Response::Accounts) GetAccounts, + /// Get historic chrysalis data. + /// Expected response: [`ChrysalisData`](crate::Response::ChrysalisData) + #[cfg(feature = "storage")] + #[cfg_attr(docsrs, doc(cfg(feature = "storage")))] + GetChrysalisData, /// Consume an account method. /// Returns [`Response`](crate::Response) #[serde(rename_all = "camelCase")] diff --git a/bindings/core/src/method_handler/wallet.rs b/bindings/core/src/method_handler/wallet.rs index 01510cace4..7673a1b2df 100644 --- a/bindings/core/src/method_handler/wallet.rs +++ b/bindings/core/src/method_handler/wallet.rs @@ -63,6 +63,7 @@ pub(crate) async fn call_wallet_method_internal(wallet: &Wallet, method: WalletM } Response::Accounts(account_dtos) } + WalletMethod::GetChrysalisData => Response::ChrysalisData(wallet.get_chrysalis_data().await?), WalletMethod::CallAccountMethod { account_id, method } => { let account = wallet.get_account(account_id).await?; call_account_method_internal(&account, method).await? diff --git a/bindings/core/src/response.rs b/bindings/core/src/response.rs index b82f05f1a0..182cb97ccc 100644 --- a/bindings/core/src/response.rs +++ b/bindings/core/src/response.rs @@ -1,6 +1,7 @@ // Copyright 2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use std::collections::HashMap; #[cfg(not(target_family = "wasm"))] use std::collections::HashSet; @@ -46,7 +47,6 @@ use serde::Serialize; use { iota_sdk::types::api::plugins::participation::types::{ParticipationEventId, ParticipationEventStatus}, iota_sdk::wallet::account::{AccountParticipationOverview, ParticipationEventWithNodes}, - std::collections::HashMap, }; use crate::{error::Error, OmittedDebug}; @@ -308,6 +308,9 @@ pub enum Response { /// - [`AddressesWithUnspentOutputs`](crate::method::AccountMethod::AddressesWithUnspentOutputs) AddressesWithUnspentOutputs(Vec), /// Response for: + /// - [`GetChrysalisData`](crate::method::WalletMethod::GetChrysalisData) + ChrysalisData(Option>), + /// Response for: /// - [`MinimumRequiredStorageDeposit`](crate::method::ClientMethod::MinimumRequiredStorageDeposit) /// - [`ComputeStorageDeposit`](crate::method::UtilsMethod::ComputeStorageDeposit) MinimumRequiredStorageDeposit(String), diff --git a/bindings/nodejs-old/examples/getting-started.js b/bindings/nodejs-old/examples/getting-started.js index 2c0aa2acc9..d511889011 100644 --- a/bindings/nodejs-old/examples/getting-started.js +++ b/bindings/nodejs-old/examples/getting-started.js @@ -34,9 +34,10 @@ async function main() { const manager = new AccountManager(accountManagerOptions); - // Generate a mnemonic and store it in the Stronghold vault. + // Generate a mnemonic and store its seed in the Stronghold vault. // INFO: It is best practice to back up the mnemonic somewhere secure. const mnemonic = await manager.generateMnemonic(); + console.log("Mnemonic:" + mnemonic); await manager.storeMnemonic(mnemonic); // Create an account. diff --git a/bindings/nodejs/CHANGELOG.md b/bindings/nodejs/CHANGELOG.md index 072e4736de..fe71daefbc 100644 --- a/bindings/nodejs/CHANGELOG.md +++ b/bindings/nodejs/CHANGELOG.md @@ -25,6 +25,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Account::{burn(), consolidateOutputs(), createAliasOutput(), meltNativeToken(), mintNativeToken(), createNativeToken(), mintNfts(), sendTransaction(), sendNativeTokens(), sendNft()}` methods; +## 1.0.10 - 2023-mm-dd + +### Fixed + +- Type of `value` property in `CustomAddress`; + +## 1.0.9 - 2023-09-07 + +### Added + +- `IClientOptions::maxParallelApiRequests`; + +### Fixed + +- The main thread gets blocked when calling client or wallet methods; + +## 1.0.8 - 2023-09-05 + +### Added + +- `migrateDbChrysalisToStardust` function; +- `Wallet::getChrysalisData` method; + ## 1.0.7 - 2023-08-29 ### Fixed diff --git a/bindings/nodejs/Cargo.toml b/bindings/nodejs/Cargo.toml index 47aaed5ab0..7cf4b9002e 100644 --- a/bindings/nodejs/Cargo.toml +++ b/bindings/nodejs/Cargo.toml @@ -36,7 +36,7 @@ neon = { version = "0.10.1", default-features = false, features = [ ] } once_cell = { version = "1.18.0", default-features = false } serde_json = { version = "1.0.105", default-features = false } -tokio = { version = "1.31.0", default-features = false } +tokio = { version = "1.32.0", default-features = false } [profile.production] codegen-units = 1 diff --git a/bindings/nodejs/examples/client/getting-started.ts b/bindings/nodejs/examples/client/getting-started.ts new file mode 100644 index 0000000000..41ec7270c4 --- /dev/null +++ b/bindings/nodejs/examples/client/getting-started.ts @@ -0,0 +1,23 @@ +// Copyright 2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { Client } from '@iota/sdk'; + +// Run with command: +// yarn run-example ./client/getting-started.ts + +// In this example we will get information about the node +async function run() { + const client = new Client({ + nodes: ['https://api.testnet.shimmer.network'], + }); + + try { + const nodeInfo = (await client.getInfo()).nodeInfo; + console.log(nodeInfo); + } catch (error) { + console.error('Error: ', error); + } +} + +run().then(() => process.exit()); diff --git a/bindings/nodejs/examples/wallet/getting-started.ts b/bindings/nodejs/examples/wallet/getting-started.ts new file mode 100644 index 0000000000..1913122e68 --- /dev/null +++ b/bindings/nodejs/examples/wallet/getting-started.ts @@ -0,0 +1,60 @@ +// Copyright 2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { Wallet, CoinType, WalletOptions, Utils } from '@iota/sdk'; + +// Run with command: +// yarn run-example ./wallet/getting-started.ts + +// The database path. +const WALLET_DB_PATH = 'getting-started-db'; + +// A name to associate with the created account. +const ACCOUNT_ALIAS = 'Alice'; + +// The node to connect to. +const NODE_URL = 'https://api.testnet.shimmer.network'; + +// A password to encrypt the stored data. +// WARNING: Never hardcode passwords in production code. +const STRONGHOLD_PASSWORD = 'a-secure-password'; + +// The path to store the account snapshot. +const STRONGHOLD_SNAPSHOT_PATH = 'vault.stronghold'; + +async function main() { + const walletOptions: WalletOptions = { + storagePath: WALLET_DB_PATH, + clientOptions: { + nodes: [NODE_URL], + }, + coinType: CoinType.Shimmer, + secretManager: { + stronghold: { + snapshotPath: STRONGHOLD_SNAPSHOT_PATH, + password: STRONGHOLD_PASSWORD, + }, + }, + }; + + const wallet = new Wallet(walletOptions); + + // Generate a mnemonic and store its seed in the Stronghold vault. + // INFO: It is best practice to back up the mnemonic somewhere secure. + const mnemonic = Utils.generateMnemonic(); + console.log('Mnemonic:' + mnemonic); + await wallet.storeMnemonic(mnemonic); + + // Create an account. + const account = await wallet.createAccount({ + alias: ACCOUNT_ALIAS, + }); + + // Get the first address and print it. + const address = (await account.addresses())[0]; + console.log(`Address: ${address.address}\n`); + + process.exit(0); +} + +main(); diff --git a/bindings/nodejs/examples/wallet/migrate-db-chrysalis-to-stardust.ts b/bindings/nodejs/examples/wallet/migrate-db-chrysalis-to-stardust.ts new file mode 100644 index 0000000000..f33a29de93 --- /dev/null +++ b/bindings/nodejs/examples/wallet/migrate-db-chrysalis-to-stardust.ts @@ -0,0 +1,53 @@ +// Copyright 2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { WalletOptions, Wallet, migrateDbChrysalisToStardust } from '@iota/sdk'; +require('dotenv').config({ path: '.env' }); + +// Run with command: +// yarn run-example wallet/migrate-db-chrysalis-to-stardust.ts + +const walletDbPath = './chrysalis-db'; + +async function run() { + const { initLogger } = require('@iota/sdk'); + initLogger({ + name: './wallet.log', + levelFilter: 'debug', + targetExclusions: ['h2', 'hyper', 'rustls'], + }); + if (!process.env.NODE_URL) { + throw new Error('.env NODE_URL is undefined, see .env.example'); + } + if (!process.env.STRONGHOLD_PASSWORD) { + throw new Error( + '.env STRONGHOLD_PASSWORD is undefined, see .env.example', + ); + } + + migrateDbChrysalisToStardust(walletDbPath, 'password'); + + const walletOptions: WalletOptions = { + storagePath: walletDbPath, + clientOptions: { + nodes: [process.env.NODE_URL], + }, + secretManager: { + stronghold: { + snapshotPath: walletDbPath + 'wallet.stronghold', + password: process.env.STRONGHOLD_PASSWORD, + }, + }, + }; + console.log(walletOptions); + const wallet = new Wallet(walletOptions); + + // Accounts migrated from the Chrysalis db + const accounts = await wallet.getAccounts(); + console.log(accounts); + + const historicChrysalisData = await wallet.getChrysalisData(); + console.log(historicChrysalisData); +} + +run().then(() => process.exit()); diff --git a/bindings/nodejs/lib/bindings.ts b/bindings/nodejs/lib/bindings.ts index 86e960f1c5..55b5dbba1f 100644 --- a/bindings/nodejs/lib/bindings.ts +++ b/bindings/nodejs/lib/bindings.ts @@ -27,6 +27,7 @@ const { getClientFromWallet, getSecretManagerFromWallet, migrateStrongholdSnapshotV2ToV3, + migrateDbChrysalisToStardust, } = addon; const callClientMethodAsync = ( @@ -116,4 +117,5 @@ export { getSecretManagerFromWallet, listenMqtt, migrateStrongholdSnapshotV2ToV3, + migrateDbChrysalisToStardust, }; diff --git a/bindings/nodejs/lib/types/client/client-options.ts b/bindings/nodejs/lib/types/client/client-options.ts index 9036e532cd..cc5491b038 100644 --- a/bindings/nodejs/lib/types/client/client-options.ts +++ b/bindings/nodejs/lib/types/client/client-options.ts @@ -36,6 +36,8 @@ export interface IClientOptions { powWorkerCount?: number; /** Whether the PoW should be done locally or remotely. */ localPow?: boolean; + /** The maximum parallel API requests. */ + maxParallelApiRequests?: number; } /** Time duration */ diff --git a/bindings/nodejs/lib/types/wallet/bridge/index.ts b/bindings/nodejs/lib/types/wallet/bridge/index.ts index a62d11a796..6f4cf77f67 100644 --- a/bindings/nodejs/lib/types/wallet/bridge/index.ts +++ b/bindings/nodejs/lib/types/wallet/bridge/index.ts @@ -64,6 +64,7 @@ import type { __GetAccountMethod__, __GetAccountIndexesMethod__, __GetAccountsMethod__, + __GetChrysalisDataMethod__, __GetLedgerNanoStatusMethod__, __GenerateEd25519AddressMethod__, __IsStrongholdPasswordAvailableMethod__, @@ -153,6 +154,7 @@ export type __Method__ = | __GetAccountMethod__ | __GetAccountIndexesMethod__ | __GetAccountsMethod__ + | __GetChrysalisDataMethod__ | __GetLedgerNanoStatusMethod__ | __GenerateEd25519AddressMethod__ | __IsStrongholdPasswordAvailableMethod__ diff --git a/bindings/nodejs/lib/types/wallet/bridge/wallet.ts b/bindings/nodejs/lib/types/wallet/bridge/wallet.ts index cb2c7259b5..5e4798ae07 100644 --- a/bindings/nodejs/lib/types/wallet/bridge/wallet.ts +++ b/bindings/nodejs/lib/types/wallet/bridge/wallet.ts @@ -55,6 +55,10 @@ export type __GetAccountMethod__ = { data: { accountId: AccountId }; }; +export type __GetChrysalisDataMethod__ = { + name: 'getChrysalisData'; +}; + export type __GetLedgerNanoStatusMethod__ = { name: 'getLedgerNanoStatus'; }; diff --git a/bindings/nodejs/lib/types/wallet/transaction-options.ts b/bindings/nodejs/lib/types/wallet/transaction-options.ts index d82d0614ac..b4cda72a89 100644 --- a/bindings/nodejs/lib/types/wallet/transaction-options.ts +++ b/bindings/nodejs/lib/types/wallet/transaction-options.ts @@ -3,6 +3,7 @@ import { TaggedDataPayload } from '../block/payload/tagged'; import { Burn } from '../client'; +import { AccountAddress } from './address'; /** Options for creating a transaction. */ export interface TransactionOptions { @@ -56,7 +57,7 @@ export type ReuseAddress = { export type CustomAddress = { /** The name of the strategy. */ strategy: 'CustomAddress'; - value: string; + value: AccountAddress; }; /** Options for creating Native Tokens. */ diff --git a/bindings/nodejs/lib/wallet/index.ts b/bindings/nodejs/lib/wallet/index.ts index 8b1189339e..860fa4497c 100644 --- a/bindings/nodejs/lib/wallet/index.ts +++ b/bindings/nodejs/lib/wallet/index.ts @@ -5,3 +5,4 @@ export * from './account'; export * from './wallet'; export * from './wallet-method-handler'; export * from '../types/wallet'; +export { migrateDbChrysalisToStardust } from '../bindings'; diff --git a/bindings/nodejs/lib/wallet/wallet.ts b/bindings/nodejs/lib/wallet/wallet.ts index ec292a58bd..18028faa91 100644 --- a/bindings/nodejs/lib/wallet/wallet.ts +++ b/bindings/nodejs/lib/wallet/wallet.ts @@ -148,6 +148,17 @@ export class Wallet { return this.methodHandler.getClient(); } + /** + * Get chrysalis data. + */ + async getChrysalisData(): Promise> { + const response = await this.methodHandler.callMethod({ + name: 'getChrysalisData', + }); + + return JSON.parse(response).payload; + } + /** * Get secret manager. */ diff --git a/bindings/nodejs/package.json b/bindings/nodejs/package.json index 34c546be14..17e1c54c2d 100644 --- a/bindings/nodejs/package.json +++ b/bindings/nodejs/package.json @@ -1,6 +1,6 @@ { "name": "@iota/sdk", - "version": "1.0.7", + "version": "1.0.9", "description": "Node.js binding to the IOTA SDK library", "main": "out/index.js", "types": "out/index.d.ts", diff --git a/bindings/nodejs/src/client.rs b/bindings/nodejs/src/client.rs index 13697a3c0c..da62dca605 100644 --- a/bindings/nodejs/src/client.rs +++ b/bindings/nodejs/src/client.rs @@ -86,7 +86,6 @@ pub fn call_client_method(mut cx: FunctionContext) -> JsResult { let method_handler = Arc::clone(&cx.argument::>(1)?.0); let callback = cx.argument::(2)?.root(&mut cx); - let (sender, receiver) = std::sync::mpsc::channel(); crate::RUNTIME.spawn(async move { if let Some(method_handler) = &*method_handler.read().await { let (response, is_error) = method_handler.call_method(method).await; @@ -108,18 +107,10 @@ pub fn call_client_method(mut cx: FunctionContext) -> JsResult { Ok(()) }); } else { - // Notify that the client got destroyed - // Safe to unwrap because the receiver is waiting on it - sender.send(()).unwrap(); + panic!("Client got destroyed") } }); - if receiver.recv().is_ok() { - return cx.throw_error( - serde_json::to_string(&Response::Panic("Client got destroyed".to_string())).expect("json to string error"), - ); - } - Ok(cx.undefined()) } diff --git a/bindings/nodejs/src/lib.rs b/bindings/nodejs/src/lib.rs index 2d8b429f3d..758910dc8a 100644 --- a/bindings/nodejs/src/lib.rs +++ b/bindings/nodejs/src/lib.rs @@ -65,6 +65,7 @@ fn main(mut cx: ModuleContext) -> NeonResult<()> { cx.export_function("getClientFromWallet", wallet::get_client)?; cx.export_function("getSecretManagerFromWallet", wallet::get_secret_manager)?; cx.export_function("listenWallet", wallet::listen_wallet)?; + cx.export_function("migrateDbChrysalisToStardust", wallet::migrate_db_chrysalis_to_stardust)?; Ok(()) } diff --git a/bindings/nodejs/src/wallet.rs b/bindings/nodejs/src/wallet.rs index ad3ae44380..7a02d091f5 100644 --- a/bindings/nodejs/src/wallet.rs +++ b/bindings/nodejs/src/wallet.rs @@ -7,6 +7,7 @@ use iota_sdk_bindings_core::{ call_wallet_method as rust_call_wallet_method, iota_sdk::wallet::{ events::types::{Event, WalletEventType}, + migration::migrate_db_chrysalis_to_stardust as rust_migrate_db_chrysalis_to_stardust, Wallet, }, Response, Result, WalletMethod, WalletOptions, @@ -102,7 +103,6 @@ pub fn call_wallet_method(mut cx: FunctionContext) -> JsResult { let method_handler = Arc::clone(&cx.argument::>(1)?.0); let callback = cx.argument::(2)?.root(&mut cx); - let (sender, receiver) = std::sync::mpsc::channel(); crate::RUNTIME.spawn(async move { if let Some(method_handler) = &*method_handler.read().await { let (response, is_error) = method_handler.call_method(method).await; @@ -124,18 +124,10 @@ pub fn call_wallet_method(mut cx: FunctionContext) -> JsResult { Ok(()) }); } else { - // Notify that the wallet got destroyed - // Safe to unwrap because the receiver is waiting on it - sender.send(()).unwrap(); + panic!("Wallet got destroyed") } }); - if receiver.recv().is_ok() { - return cx.throw_error( - serde_json::to_string(&Response::Panic("Wallet got destroyed".to_string())).expect("json to string error"), - ); - } - Ok(cx.undefined()) } @@ -232,3 +224,27 @@ pub fn get_secret_manager(mut cx: FunctionContext) -> JsResult { Ok(promise) } + +pub fn migrate_db_chrysalis_to_stardust(mut cx: FunctionContext) -> JsResult { + let storage_path = cx.argument::(0)?.value(&mut cx); + let password = cx + .argument_opt(1) + .map(|opt| opt.downcast_or_throw::(&mut cx)) + .transpose()? + .map(|opt| opt.value(&mut cx)) + .map(Into::into); + + let channel = cx.channel(); + let (deferred, promise) = cx.promise(); + crate::RUNTIME.spawn(async move { + if let Err(err) = rust_migrate_db_chrysalis_to_stardust(storage_path, password, None).await { + deferred.settle_with(&channel, move |mut cx| { + cx.error(serde_json::to_string(&Response::Error(err.into())).expect("json to string error")) + }); + } else { + deferred.settle_with(&channel, move |mut cx| Ok(cx.boxed(()))); + } + }); + + Ok(promise) +} diff --git a/bindings/nodejs/tests/wallet/wallet.spec.ts b/bindings/nodejs/tests/wallet/wallet.spec.ts index cef6e037d7..9d514e213c 100644 --- a/bindings/nodejs/tests/wallet/wallet.spec.ts +++ b/bindings/nodejs/tests/wallet/wallet.spec.ts @@ -132,53 +132,6 @@ describe('Wallet', () => { await recreatedWallet.destroy() removeDir(storagePath) }, 20000); - - it('error after destroy', async () => { - let storagePath = 'test-error-after-destroy'; - removeDir(storagePath); - - const walletOptions = { - storagePath, - clientOptions: { - nodes: ['https://api.testnet.shimmer.network'], - }, - coinType: CoinType.Shimmer, - secretManager: { - stronghold: { - snapshotPath: `./${storagePath}/wallet.stronghold`, - password: `A12345678*`, - }, - }, - }; - - const wallet = new Wallet(walletOptions); - await wallet.storeMnemonic( - 'vital give early extra blind skin eight discover scissors there globe deal goat fat load robot return rate fragile recycle select live ordinary claim', - ); - - const account = await wallet.createAccount({ - alias: 'Alice', - }); - - expect(account.getMetadata().index).toStrictEqual(0); - - await wallet.destroy(); - - try { - const accounts = await wallet.getAccounts(); - throw 'Should return an error because the wallet got destroyed'; - } catch (err: any) { - expect(err).toContain('Wallet got destroyed'); - } - - try { - const client = await wallet.getClient(); - throw 'Should return an error because the wallet got destroyed'; - } catch (err: any) { - expect(err).toContain('Wallet got destroyed'); - } - removeDir(storagePath) - }, 35000); }) function removeDir(storagePath: string) { diff --git a/bindings/python/CHANGELOG.md b/bindings/python/CHANGELOG.md index 62f858f5fe..ea87791c30 100644 --- a/bindings/python/CHANGELOG.md +++ b/bindings/python/CHANGELOG.md @@ -27,6 +27,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `ConflictReason` display implementation with an explanation of the conflict; - `Account::{burn(), consolidate_outputs(), create_alias_output(), create_native_token(), melt_native_token(), mint_native_token(), mint_nfts(), send_transaction(), send_native_tokens(), send_nft()}` methods; +## 1.0.2 - 2023-MM-DD + +### Added + +- `ClientOptions::maxParallelApiRequests`; + ## 1.0.1 - 2023-08-23 ### Fixed diff --git a/bindings/python/Cargo.toml b/bindings/python/Cargo.toml index 073194d2a3..c5ee966d9b 100644 --- a/bindings/python/Cargo.toml +++ b/bindings/python/Cargo.toml @@ -34,4 +34,4 @@ pyo3 = { version = "0.19.2", default-features = false, features = [ "extension-module", ] } serde_json = { version = "1.0.105", default-features = false } -tokio = { version = "1.31.0", default-features = false } +tokio = { version = "1.32.0", default-features = false } diff --git a/bindings/python/README.md b/bindings/python/README.md index 31218ec834..ada5663e67 100644 --- a/bindings/python/README.md +++ b/bindings/python/README.md @@ -60,51 +60,15 @@ Python binding to the [iota-sdk library](/README.md). ## Client Usage -The following example creates a [`Client`](https://wiki.iota.org/shimmer/iota-sdk/references/python/iota_sdk/client/) -instance connected to -the [Shimmer Testnet](https://api.testnet.shimmer.network), and retrieves the node's information by -calling [`Client.get_info()`](https://wiki.iota.org/shimmer/iota-sdk/references/python/iota_sdk/client/_node_core_api/#get_info), -and then print the node's information. +The following example creates a Client instance connected to the Shimmer Testnet, and retrieves the node's information by calling `Client.get_info()`, and then print the node's information. -```python -from iota_sdk import Client - -# Create a Client instance -client = Client(nodes=['https://api.testnet.shimmer.network']) - -# Get the node info -node_info = client.get_info() -print(f'{node_info}') -``` +[examples/client/getting_started.py](examples/client/getting_started.py) ## Wallet Usage -The following example will create a -new [`Wallet`](https://wiki.iota.org/shimmer/iota-sdk/references/python/iota_sdk/wallet/) [`Account`](https://wiki.iota.org/shimmer/iota-sdk/references/python/iota_sdk/wallet/account/) -that connects to the [Shimmer Testnet](https://api.testnet.shimmer.network) using the -[`StrongholdSecretManager`](https://wiki.iota.org/shimmer/iota-sdk/references/python/iota_sdk/secret_manager/#strongholdsecretmanager-objects) -to safely store a seed derived from a mnemonic, and then print the account's information. - -```python -from iota_sdk import Wallet, StrongholdSecretManager, CoinType, ClientOptions - -# This example creates a new database and account +The following example will create a new Wallet Account using a StrongholdSecretManager, and then print the account's information. -client_options = ClientOptions(nodes=['https://api.testnet.shimmer.network']) - -secret_manager = StrongholdSecretManager( - "wallet.stronghold", "some_hopefully_secure_password") - -wallet = Wallet('./alice-walletdb', client_options, - CoinType.SHIMMER, secret_manager) - -# Store the mnemonic in the Stronghold snapshot. This only needs to be done once -account = wallet.store_mnemonic("flame fever pig forward exact dash body idea link scrub tennis minute " + - "surge unaware prosper over waste kitten ceiling human knife arch situate civil") - -account = wallet.create_account('Alice') -print(account.get_metadata()) -``` +[examples/wallet/getting_started.py](examples/wallet/getting_started.py) ## Examples diff --git a/bindings/python/examples/client/getting_started.py b/bindings/python/examples/client/getting_started.py new file mode 100644 index 0000000000..94d56433a3 --- /dev/null +++ b/bindings/python/examples/client/getting_started.py @@ -0,0 +1,8 @@ +from iota_sdk import Client + +# Create a Client instance +client = Client(nodes=['https://api.testnet.shimmer.network']) + +# Get the node info +node_info = client.get_info() +print(f'{node_info}') diff --git a/bindings/python/examples/wallet/getting-started.py b/bindings/python/examples/wallet/getting_started.py similarity index 93% rename from bindings/python/examples/wallet/getting-started.py rename to bindings/python/examples/wallet/getting_started.py index abfafbc117..bb001d7d75 100644 --- a/bindings/python/examples/wallet/getting-started.py +++ b/bindings/python/examples/wallet/getting_started.py @@ -37,9 +37,10 @@ secret_manager=secret_manager ) -# Generate a mnemonic and store it in the Stronghold vault. +# Generate a mnemonic and store its seed in the Stronghold vault. # INFO: It is best practice to back up the mnemonic somewhere secure. mnemonic = Utils.generate_mnemonic() +print(f'Mnemonic: {mnemonic}') wallet.store_mnemonic(mnemonic) # Create an account. diff --git a/bindings/python/iota_sdk/types/client_options.py b/bindings/python/iota_sdk/types/client_options.py index dcbe0d71b6..dd2631507b 100644 --- a/bindings/python/iota_sdk/types/client_options.py +++ b/bindings/python/iota_sdk/types/client_options.py @@ -84,6 +84,8 @@ class ClientOptions: Timeout when sending a block that requires remote proof of work. powWorkerCount (int): The amount of threads to be used for proof of work. + maxParallelApiRequests (int): + The maximum parallel API requests. """ primaryNode: Optional[str] = None primaryPowNode: Optional[str] = None @@ -103,6 +105,7 @@ class ClientOptions: apiTimeout: Optional[Duration] = None remotePowTimeout: Optional[Duration] = None powWorkerCount: Optional[int] = None + maxParallelApiRequests: Optional[int] = None def as_dict(self): config = {k: v for k, v in self.__dict__.items() if v is not None} diff --git a/bindings/wasm/Cargo.toml b/bindings/wasm/Cargo.toml index 421e42dca4..1472dc7a31 100644 --- a/bindings/wasm/Cargo.toml +++ b/bindings/wasm/Cargo.toml @@ -26,7 +26,7 @@ console_error_panic_hook = { version = "0.1.7", default-features = false } js-sys = { version = "0.3.64", default-features = false, features = [] } log = { version = "0.4.20", default-features = false } serde_json = { version = "1.0.105", default-features = false } -tokio = { version = "1.31.0", default-features = false, features = ["sync"] } +tokio = { version = "1.32.0", default-features = false, features = ["sync"] } wasm-bindgen = { version = "0.2.87", default-features = false, features = [ "spans", "std", diff --git a/bindings/wasm/lib/bindings.ts b/bindings/wasm/lib/bindings.ts index fded283753..295583d6fc 100644 --- a/bindings/wasm/lib/bindings.ts +++ b/bindings/wasm/lib/bindings.ts @@ -9,7 +9,7 @@ import { __UtilsMethods__ } from './utils'; // Import needs to be in a single line, otherwise it breaks // prettier-ignore // @ts-ignore: path is set to match runtime transpiled js path when bundled. -import { initLogger, createClient, destroyClient, createSecretManager, createWallet, callClientMethodAsync, callSecretManagerMethodAsync, callUtilsMethodRust, callWalletMethodAsync, destroyWallet, listenWalletAsync, getClientFromWallet, getSecretManagerFromWallet, listenMqtt, migrateStrongholdSnapshotV2ToV3 } from '../wasm/iota_sdk_wasm'; +import { initLogger, createClient, destroyClient, createSecretManager, createWallet, callClientMethodAsync, callSecretManagerMethodAsync, callUtilsMethodRust, callWalletMethodAsync, destroyWallet, listenWalletAsync, getClientFromWallet, getSecretManagerFromWallet, listenMqtt, migrateStrongholdSnapshotV2ToV3, migrateDbChrysalisToStardust } from '../wasm/iota_sdk_wasm'; const callUtilsMethod = (method: __UtilsMethods__): any => { const response = JSON.parse(callUtilsMethodRust(JSON.stringify(method))); @@ -36,4 +36,5 @@ export { getSecretManagerFromWallet, listenMqtt, migrateStrongholdSnapshotV2ToV3, + migrateDbChrysalisToStardust, }; diff --git a/bindings/wasm/src/wallet.rs b/bindings/wasm/src/wallet.rs index 891065368b..0f4890af73 100644 --- a/bindings/wasm/src/wallet.rs +++ b/bindings/wasm/src/wallet.rs @@ -141,3 +141,14 @@ pub async fn listen_wallet( Ok(JsValue::UNDEFINED) } + +/// Rocksdb chrysalis migration is not supported for WebAssembly bindings. +/// +/// Throws an error if called, only included for compatibility +/// with the Node.js bindings TypeScript definitions. +#[wasm_bindgen(js_name = migrateDbChrysalisToStardust)] +pub fn migrate_db_chrysalis_to_stardust(_storage_path: String, _password: Option) -> Result<(), JsValue> { + let js_error = js_sys::Error::new("Rocksdb chrysalis migration is not supported for WebAssembly"); + + Err(JsValue::from(js_error)) +} diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 22ffcab75a..b37d28a8eb 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -21,8 +21,8 @@ iota-sdk = { path = "../sdk", default-features = false, features = [ "participation", ] } -chrono = { version = "0.4.26", default-features = false, features = ["std"] } -clap = { version = "4.3.21", default-features = false, features = [ +chrono = { version = "0.4.29", default-features = false, features = ["std"] } +clap = { version = "4.4.2", default-features = false, features = [ "std", "color", "help", @@ -44,6 +44,6 @@ humantime = { version = "2.1.0", default-features = false } log = { version = "0.4.20", default-features = false } prefix-hex = { version = "0.7.1", default-features = false, features = ["std"] } serde_json = { version = "1.0.105", default-features = false } -thiserror = { version = "1.0.46", default-features = false } -tokio = { version = "1.31.0", default-features = false, features = ["fs"] } +thiserror = { version = "1.0.48", default-features = false } +tokio = { version = "1.32.0", default-features = false, features = ["fs"] } zeroize = { version = "1.6.0", default-features = false } diff --git a/cli/README.md b/cli/README.md index 999b48d9a2..35d05d6520 100644 --- a/cli/README.md +++ b/cli/README.md @@ -1,7 +1,5 @@ # IOTA Stardust CLI Wallet -![cli-wallet](./documentation/static/img/cli-wallet.gif) - Command line interface application for the [IOTA sdk wallet](https://github.com/iotaledger/iota-sdk). ## Usage @@ -37,7 +35,7 @@ Alternatively, you can select an existing account by its alias: ## Commands -To see the full list of available commands look at the documentation [here](./documentation/docs). +To see the full list of available commands look at the documentation [here](https://wiki.iota.org/shimmer/cli-wallet/welcome/). ## Caveats @@ -48,14 +46,4 @@ By default the database path is `./wallet-cli-database` but you can change this ``` export WALLET_DATABASE_PATH=/path/to/database # or add it to your .bashrc, .zshrc ./wallet [COMMAND] [OPTIONS] -``` - -## Contributing - -To run the CLI from source, install Rust (usually through [Rustup](https://rustup.rs/)) and run the following commands: - -``` -git clone --depth 1 https://github.com/iotaledger/iota-sdk -cd cli -cargo run -- [COMMAND] [OPTIONS] -``` +``` \ No newline at end of file diff --git a/sdk/CHANGELOG.md b/sdk/CHANGELOG.md index 85448cbf4d..ee771bc158 100644 --- a/sdk/CHANGELOG.md +++ b/sdk/CHANGELOG.md @@ -33,7 +33,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `StrongholdAdapterBuilder` updated to be slightly more ergonomic; - `Wallet::{set_stronghold_password, change_stronghold_password, set_stronghold_password_clear_interval, store_mnemonic}` return an `Err` instead of `Ok` in case of a non-stronghold secret manager; -## 1.0.3 - 2023-MM-DD +## 1.0.3 - 2023-09-07 + +### Added + +- `migrate_db_chrysalis_to_stardust()` function; +- `Wallet::get_chrysalis_data()` method; +- `PrivateKeySecretManager` and `SecretManager::PrivateKey`; +- `SecretManager::from` impl for variants; +- `Client` requests now obey a maximum concurrency using a request pool (set via `ClientBuilder::with_max_parallel_api_requests`); ### Fixed diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index c0f2f41b13..b2f726e815 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iota-sdk" -version = "1.0.2" +version = "1.0.3" authors = ["IOTA Stiftung"] edition = "2021" description = "The IOTA SDK provides developers with a seamless experience to develop on IOTA by providing account abstractions and clients to interact with node APIs." @@ -20,7 +20,7 @@ rustdoc-args = ["--cfg", "docsrs"] # Mandatory dependencies bech32 = { version = "0.9.1", default-features = false } bitflags = { version = "2.4.0", default-features = false } -bytemuck = { version = "1.13.1", default-features = false } +bytemuck = { version = "1.14.0", default-features = false } derive_more = { version = "0.99.17", default-features = false, features = [ "from", "as_ref", @@ -50,7 +50,7 @@ prefix-hex = { version = "0.7.1", default-features = false, features = [ "primitive-types", ] } primitive-types = { version = "0.12.1", default-features = false } -serde = { version = "1.0.183", default-features = false, features = ["derive"] } +serde = { version = "1.0.188", default-features = false, features = ["derive"] } serde_json = { version = "1.0.105", default-features = false, features = [ "alloc", ] } @@ -58,6 +58,7 @@ serde_json = { version = "1.0.105", default-features = false, features = [ # Optional dependencies anymap = { version = "0.12.1", default-features = false, optional = true } async-trait = { version = "0.1.73", default-features = false, optional = true } +bs58 = { version = "0.5.0", default-features = false, optional = true } fern-logger = { version = "0.5.0", default-features = false, optional = true } futures = { version = "0.3.28", default-features = false, features = [ "thread-pool", @@ -72,10 +73,10 @@ once_cell = { version = "1.18.0", default-features = false, optional = true } rand = { version = "0.8.5", default-features = false, features = [ "min_const_gen", ], optional = true } -regex = { version = "1.9.3", default-features = false, features = [ +regex = { version = "1.9.5", default-features = false, features = [ "unicode-perl", ], optional = true } -reqwest = { version = "0.11.18", default-features = false, features = [ +reqwest = { version = "0.11.20", default-features = false, features = [ "json", ], optional = true } rocksdb = { version = "0.21.0", default-features = false, features = [ @@ -85,12 +86,12 @@ rumqttc = { version = "0.22.0", default-features = false, features = [ "websocket", ], optional = true } serde_repr = { version = "0.1.16", default-features = false, optional = true } -thiserror = { version = "1.0.46", default-features = false, optional = true } -time = { version = "0.3.25", default-features = false, features = [ +thiserror = { version = "1.0.48", default-features = false, optional = true } +time = { version = "0.3.28", default-features = false, features = [ "serde", "macros", ], optional = true } -url = { version = "2.4.0", default-features = false, features = [ +url = { version = "2.4.1", default-features = false, features = [ "serde", ], optional = true } zeroize = { version = "1.6.0", default-features = false, features = [ @@ -98,7 +99,7 @@ zeroize = { version = "1.6.0", default-features = false, features = [ ], optional = true } [target.'cfg(not(target_family = "wasm"))'.dependencies] -tokio = { version = "1.31.0", default-features = false, features = [ +tokio = { version = "1.32.0", default-features = false, features = [ "macros", "rt-multi-thread", "time", @@ -114,7 +115,7 @@ instant = { version = "0.1.12", default-features = false, features = [ "wasm-bindgen", ], optional = true } lazy_static = { version = "1.4.0", default-features = false } -tokio = { version = "1.31.0", default-features = false, features = [ +tokio = { version = "1.32.0", default-features = false, features = [ "macros", "rt", "time", @@ -127,7 +128,7 @@ iota-sdk = { path = ".", default-features = false, features = ["rand"] } dotenvy = { version = "0.15.7", default-features = false } fern-logger = { version = "0.5.0", default-features = false } -tokio = { version = "1.31.0", default-features = false, features = [ +tokio = { version = "1.32.0", default-features = false, features = [ "macros", "rt", "rt-multi-thread", @@ -184,6 +185,7 @@ stronghold = [ "dep:heck", ] tls = ["reqwest?/rustls-tls", "rumqttc?/use-rustls"] +private_key_secret_manager = ["bs58"] client = [ "pow", @@ -621,6 +623,11 @@ name = "get_block" path = "examples/client/get_block.rs" required-features = ["client"] +[[example]] +name = "client_getting_started" +path = "examples/client/getting_started.rs" +required-features = ["client"] + [[example]] name = "ledger_nano" path = "examples/client/ledger_nano.rs" diff --git a/sdk/examples/client/getting_started.rs b/sdk/examples/client/getting_started.rs new file mode 100644 index 0000000000..dce63ed8ff --- /dev/null +++ b/sdk/examples/client/getting_started.rs @@ -0,0 +1,23 @@ +// Copyright 2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +//! This examples shows how to get the node info. +//! +//! ```sh +//! cargo run --release --example client_getting_started +//! ``` + +use iota_sdk::client::{Client, Result}; + +#[tokio::main] +async fn main() -> Result<()> { + let client = Client::builder() + .with_node("https://api.testnet.shimmer.network")? // Insert your node URL here + .finish() + .await?; + + let info = client.get_info().await?; + println!("Node Info: {info:?}"); + + Ok(()) +} diff --git a/sdk/examples/wallet/getting_started.rs b/sdk/examples/wallet/getting_started.rs index abecbe039a..69468337fb 100644 --- a/sdk/examples/wallet/getting_started.rs +++ b/sdk/examples/wallet/getting_started.rs @@ -4,8 +4,6 @@ //! In this example we will create a new wallet, a mnemonic, and an initial account. Then, we'll print the first address //! of that account. //! -//! Make sure there's no `STRONGHOLD_SNAPSHOT_PATH` file and no `WALLET_DB_PATH` folder yet! -//! //! Rename `.env.example` to `.env` first, then run the command: //! ```sh //! cargo run --release --all-features --example wallet_getting_started @@ -21,45 +19,38 @@ use iota_sdk::{ #[tokio::main] async fn main() -> Result<()> { - // This example uses secrets in environment variables for simplicity which should not be done in production. - dotenvy::dotenv().ok(); - - // Setup Stronghold secret_manager + // Setup Stronghold secret manager. + // WARNING: Never hardcode passwords in production code. let secret_manager = StrongholdSecretManager::builder() - .password(std::env::var("STRONGHOLD_PASSWORD").unwrap()) - .build(std::env::var("STRONGHOLD_SNAPSHOT_PATH").unwrap())?; + .password("password".to_owned()) // A password to encrypt the stored data. + .build("vault.stronghold")?; // The path to store the account snapshot. - let client_options = ClientOptions::new().with_node(&std::env::var("NODE_URL").unwrap())?; + let client_options = ClientOptions::new().with_node("https://api.testnet.shimmer.network")?; - // Create the wallet + // Set up and store the wallet. let wallet = Wallet::builder() .with_secret_manager(SecretManager::Stronghold(secret_manager)) - .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) .with_client_options(client_options) .with_coin_type(SHIMMER_COIN_TYPE) + .with_storage_path("getting-started-db") .finish() .await?; - // Generate a mnemonic and store it in the Stronghold vault - // INFO: It is best practice to back up the mnemonic somewhere secure + // Generate a mnemonic and store its seed in the Stronghold vault. + // INFO: It is best practice to back up the mnemonic somewhere secure. let mnemonic = wallet.generate_mnemonic()?; - wallet.store_mnemonic(mnemonic.clone()).await?; - println!("Created a wallet from the mnemonic:\n'{}'", mnemonic.as_ref()); + println!("Mnemonic: {}", mnemonic.as_ref()); + wallet.store_mnemonic(mnemonic).await?; - // Create an account - let alias = "Alice"; - let account = wallet.create_account().with_alias(alias).finish().await?; - println!("Created account '{alias}'"); + // Create an account. + let account = wallet + .create_account() + .with_alias("Alice") // A name to associate with the created account. + .finish() + .await?; - // Display the adresses in the account (only 1 for a new account) - let addresses = account.addresses().await?; - println!( - "{alias}'s addresses:\n{:#?}", - addresses - .iter() - .map(|addr| addr.address().to_string()) - .collect::>() - ); + let first_address = &account.addresses().await?[0]; + println!("{}", first_address.address()); Ok(()) } diff --git a/sdk/src/client/builder.rs b/sdk/src/client/builder.rs index 576042a576..8282525dbf 100644 --- a/sdk/src/client/builder.rs +++ b/sdk/src/client/builder.rs @@ -48,6 +48,10 @@ pub struct ClientBuilder { #[cfg(not(target_family = "wasm"))] #[serde(default, skip_serializing_if = "Option::is_none")] pub pow_worker_count: Option, + /// The maximum parallel API requests + #[cfg(not(target_family = "wasm"))] + #[serde(default = "default_max_parallel_api_requests")] + pub max_parallel_api_requests: usize, } fn default_api_timeout() -> Duration { @@ -58,6 +62,11 @@ fn default_remote_pow_timeout() -> Duration { DEFAULT_REMOTE_POW_API_TIMEOUT } +#[cfg(not(target_family = "wasm"))] +fn default_max_parallel_api_requests() -> usize { + super::constants::MAX_PARALLEL_API_REQUESTS +} + impl Default for NetworkInfo { fn default() -> Self { Self { @@ -82,6 +91,8 @@ impl Default for ClientBuilder { remote_pow_timeout: DEFAULT_REMOTE_POW_API_TIMEOUT, #[cfg(not(target_family = "wasm"))] pow_worker_count: None, + #[cfg(not(target_family = "wasm"))] + max_parallel_api_requests: super::constants::MAX_PARALLEL_API_REQUESTS, } } } @@ -237,6 +248,13 @@ impl ClientBuilder { self } + /// Set maximum parallel API requests. + #[cfg(not(target_family = "wasm"))] + pub fn with_max_parallel_api_requests(mut self, max_parallel_api_requests: usize) -> Self { + self.max_parallel_api_requests = max_parallel_api_requests; + self + } + /// Build the Client instance. #[cfg(not(target_family = "wasm"))] pub async fn finish(self) -> Result { @@ -269,6 +287,7 @@ impl ClientBuilder { sender: RwLock::new(mqtt_event_tx), receiver: RwLock::new(mqtt_event_rx), }, + request_pool: crate::client::request_pool::RequestPool::new(self.max_parallel_api_requests), }); client_inner.sync_nodes(&nodes, ignore_node_health).await?; @@ -327,6 +346,8 @@ impl ClientBuilder { remote_pow_timeout: client.get_remote_pow_timeout().await, #[cfg(not(target_family = "wasm"))] pow_worker_count: *client.pow_worker_count.read().await, + #[cfg(not(target_family = "wasm"))] + max_parallel_api_requests: client.request_pool.size().await, } } } diff --git a/sdk/src/client/core.rs b/sdk/src/client/core.rs index fb34c5764c..0b7249865b 100644 --- a/sdk/src/client/core.rs +++ b/sdk/src/client/core.rs @@ -13,6 +13,8 @@ use { tokio::sync::watch::{Receiver as WatchReceiver, Sender as WatchSender}, }; +#[cfg(not(target_family = "wasm"))] +use super::request_pool::RequestPool; #[cfg(target_family = "wasm")] use crate::client::constants::CACHE_NETWORK_INFO_TIMEOUT_IN_SECONDS; use crate::{ @@ -56,6 +58,8 @@ pub struct ClientInner { pub(crate) mqtt: MqttInner, #[cfg(target_family = "wasm")] pub(crate) last_sync: tokio::sync::Mutex>, + #[cfg(not(target_family = "wasm"))] + pub(crate) request_pool: RequestPool, } #[derive(Default)] @@ -83,10 +87,13 @@ pub(crate) struct MqttInner { impl std::fmt::Debug for Client { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut d = f.debug_struct("Client"); - d.field("node_manager", &self.inner.node_manager); + d.field("node_manager", &self.node_manager); #[cfg(feature = "mqtt")] - d.field("broker_options", &self.inner.mqtt.broker_options); - d.field("network_info", &self.inner.network_info).finish() + d.field("broker_options", &self.mqtt.broker_options); + d.field("network_info", &self.network_info); + #[cfg(not(target_family = "wasm"))] + d.field("request_pool", &self.request_pool); + d.finish() } } diff --git a/sdk/src/client/mod.rs b/sdk/src/client/mod.rs index aabc2d440a..7f6d79f8a3 100644 --- a/sdk/src/client/mod.rs +++ b/sdk/src/client/mod.rs @@ -42,6 +42,8 @@ pub mod core; pub mod error; pub mod node_api; pub mod node_manager; +#[cfg(not(target_family = "wasm"))] +pub(crate) mod request_pool; pub mod secret; pub mod storage; #[cfg(feature = "stronghold")] diff --git a/sdk/src/client/node_api/core/mod.rs b/sdk/src/client/node_api/core/mod.rs index 9ae39e13b5..0e0e57108a 100644 --- a/sdk/src/client/node_api/core/mod.rs +++ b/sdk/src/client/node_api/core/mod.rs @@ -5,8 +5,6 @@ pub mod routes; -#[cfg(not(target_family = "wasm"))] -use crate::client::constants::MAX_PARALLEL_API_REQUESTS; use crate::{ client::{Client, Result}, types::block::output::{OutputId, OutputMetadata, OutputWithMetadata}, @@ -15,87 +13,29 @@ use crate::{ impl Client { /// Request outputs by their output ID in parallel pub async fn get_outputs(&self, output_ids: &[OutputId]) -> Result> { - #[cfg(target_family = "wasm")] - let outputs = futures::future::try_join_all(output_ids.iter().map(|id| self.get_output(id))).await?; - - #[cfg(not(target_family = "wasm"))] - let outputs = - futures::future::try_join_all(output_ids.chunks(MAX_PARALLEL_API_REQUESTS).map(|output_ids_chunk| { - let client = self.clone(); - let output_ids_chunk = output_ids_chunk.to_vec(); - async move { - tokio::spawn(async move { - futures::future::try_join_all(output_ids_chunk.iter().map(|id| client.get_output(id))).await - }) - .await? - } - })) - .await? - .into_iter() - .flatten() - .collect(); - - Ok(outputs) + futures::future::try_join_all(output_ids.iter().map(|id| self.get_output(id))).await } /// Request outputs by their output ID in parallel, ignoring failed requests /// Useful to get data about spent outputs, that might not be pruned yet pub async fn get_outputs_ignore_errors(&self, output_ids: &[OutputId]) -> Result> { - #[cfg(target_family = "wasm")] - let outputs = futures::future::join_all(output_ids.iter().map(|id| self.get_output(id))) - .await - .into_iter() - .filter_map(Result::ok) - .collect(); - - #[cfg(not(target_family = "wasm"))] - let outputs = - futures::future::try_join_all(output_ids.chunks(MAX_PARALLEL_API_REQUESTS).map(|output_ids_chunk| { - let client = self.clone(); - let output_ids_chunk = output_ids_chunk.to_vec(); - tokio::spawn(async move { - futures::future::join_all(output_ids_chunk.iter().map(|id| client.get_output(id))) - .await - .into_iter() - .filter_map(Result::ok) - .collect::>() - }) - })) - .await? - .into_iter() - .flatten() - .collect(); - - Ok(outputs) + Ok( + futures::future::join_all(output_ids.iter().map(|id| self.get_output(id))) + .await + .into_iter() + .filter_map(Result::ok) + .collect(), + ) } /// Requests metadata for outputs by their output ID in parallel, ignoring failed requests pub async fn get_outputs_metadata_ignore_errors(&self, output_ids: &[OutputId]) -> Result> { - #[cfg(target_family = "wasm")] - let metadata = futures::future::join_all(output_ids.iter().map(|id| self.get_output_metadata(id))) - .await - .into_iter() - .filter_map(Result::ok) - .collect(); - - #[cfg(not(target_family = "wasm"))] - let metadata = - futures::future::try_join_all(output_ids.chunks(MAX_PARALLEL_API_REQUESTS).map(|output_ids_chunk| { - let client = self.clone(); - let output_ids_chunk = output_ids_chunk.to_vec(); - tokio::spawn(async move { - futures::future::join_all(output_ids_chunk.iter().map(|id| client.get_output_metadata(id))) - .await - .into_iter() - .filter_map(Result::ok) - .collect::>() - }) - })) - .await? - .into_iter() - .flatten() - .collect(); - - Ok(metadata) + Ok( + futures::future::join_all(output_ids.iter().map(|id| self.get_output_metadata(id))) + .await + .into_iter() + .filter_map(Result::ok) + .collect(), + ) } } diff --git a/sdk/src/client/node_api/core/routes.rs b/sdk/src/client/node_api/core/routes.rs index b0e2120507..ec459f59d2 100644 --- a/sdk/src/client/node_api/core/routes.rs +++ b/sdk/src/client/node_api/core/routes.rs @@ -76,21 +76,13 @@ impl ClientInner { pub async fn get_routes(&self) -> Result { let path = "api/routes"; - self.node_manager - .read() - .await - .get_request(path, None, self.get_timeout().await, false, false) - .await + self.get_request(path, None, false, false).await } /// Returns general information about the node. /// GET /api/core/v2/info pub async fn get_info(&self) -> Result { - self.node_manager - .read() - .await - .get_request(INFO_PATH, None, self.get_timeout().await, false, false) - .await + self.get_request(INFO_PATH, None, false, false).await } // Tangle routes. @@ -100,12 +92,7 @@ impl ClientInner { pub async fn get_tips(&self) -> Result> { let path = "api/core/v2/tips"; - let response = self - .node_manager - .read() - .await - .get_request::(path, None, self.get_timeout().await, false, false) - .await?; + let response = self.get_request::(path, None, false, false).await?; Ok(response.tips) } @@ -224,12 +211,7 @@ impl ClientInner { pub async fn get_block(&self, block_id: &BlockId) -> Result { let path = &format!("api/core/v2/blocks/{block_id}"); - let dto = self - .node_manager - .read() - .await - .get_request::(path, None, self.get_timeout().await, false, true) - .await?; + let dto = self.get_request::(path, None, false, true).await?; Ok(Block::try_from_dto_with_params( dto, @@ -242,11 +224,7 @@ impl ClientInner { pub async fn get_block_raw(&self, block_id: &BlockId) -> Result> { let path = &format!("api/core/v2/blocks/{block_id}"); - self.node_manager - .read() - .await - .get_request_bytes(path, None, self.get_timeout().await) - .await + self.get_request_bytes(path, None).await } /// Returns the metadata of a block. @@ -254,11 +232,7 @@ impl ClientInner { pub async fn get_block_metadata(&self, block_id: &BlockId) -> Result { let path = &format!("api/core/v2/blocks/{block_id}/metadata"); - self.node_manager - .read() - .await - .get_request(path, None, self.get_timeout().await, true, true) - .await + self.get_request(path, None, true, true).await } // UTXO routes. @@ -268,12 +242,7 @@ impl ClientInner { pub async fn get_output(&self, output_id: &OutputId) -> Result { let path = &format!("api/core/v2/outputs/{output_id}"); - let response: OutputWithMetadataResponse = self - .node_manager - .read() - .await - .get_request(path, None, self.get_timeout().await, false, true) - .await?; + let response: OutputWithMetadataResponse = self.get_request(path, None, false, true).await?; let token_supply = self.get_token_supply().await?; let output = Output::try_from_dto_with_params(response.output, token_supply)?; @@ -286,11 +255,7 @@ impl ClientInner { pub async fn get_output_raw(&self, output_id: &OutputId) -> Result> { let path = &format!("api/core/v2/outputs/{output_id}"); - self.node_manager - .read() - .await - .get_request_bytes(path, None, self.get_timeout().await) - .await + self.get_request_bytes(path, None).await } /// Get the metadata for a given `OutputId` (TransactionId + output_index). @@ -298,11 +263,7 @@ impl ClientInner { pub async fn get_output_metadata(&self, output_id: &OutputId) -> Result { let path = &format!("api/core/v2/outputs/{output_id}/metadata"); - self.node_manager - .read() - .await - .get_request::(path, None, self.get_timeout().await, false, true) - .await + self.get_request::(path, None, false, true).await } /// Gets all stored receipts. @@ -310,12 +271,7 @@ impl ClientInner { pub async fn get_receipts(&self) -> Result> { let path = &"api/core/v2/receipts"; - let resp = self - .node_manager - .read() - .await - .get_request::(path, None, DEFAULT_API_TIMEOUT, false, false) - .await?; + let resp = self.get_request::(path, None, false, false).await?; Ok(resp.receipts) } @@ -325,12 +281,7 @@ impl ClientInner { pub async fn get_receipts_migrated_at(&self, milestone_index: u32) -> Result> { let path = &format!("api/core/v2/receipts/{milestone_index}"); - let resp = self - .node_manager - .read() - .await - .get_request::(path, None, DEFAULT_API_TIMEOUT, false, false) - .await?; + let resp = self.get_request::(path, None, false, false).await?; Ok(resp.receipts) } @@ -341,11 +292,7 @@ impl ClientInner { pub async fn get_treasury(&self) -> Result { let path = "api/core/v2/treasury"; - self.node_manager - .read() - .await - .get_request(path, None, DEFAULT_API_TIMEOUT, false, false) - .await + self.get_request(path, None, false, false).await } /// Returns the block, as object, that was included in the ledger for a given TransactionId. @@ -353,12 +300,7 @@ impl ClientInner { pub async fn get_included_block(&self, transaction_id: &TransactionId) -> Result { let path = &format!("api/core/v2/transactions/{transaction_id}/included-block"); - let dto = self - .node_manager - .read() - .await - .get_request::(path, None, self.get_timeout().await, true, true) - .await?; + let dto = self.get_request::(path, None, true, true).await?; Ok(Block::try_from_dto_with_params( dto, @@ -371,11 +313,7 @@ impl ClientInner { pub async fn get_included_block_raw(&self, transaction_id: &TransactionId) -> Result> { let path = &format!("api/core/v2/transactions/{transaction_id}/included-block"); - self.node_manager - .read() - .await - .get_request_bytes(path, None, self.get_timeout().await) - .await + self.get_request_bytes(path, None).await } /// Returns the metadata of the block that was included in the ledger for a given TransactionId. @@ -383,11 +321,7 @@ impl ClientInner { pub async fn get_included_block_metadata(&self, transaction_id: &TransactionId) -> Result { let path = &format!("api/core/v2/transactions/{transaction_id}/included-block/metadata"); - self.node_manager - .read() - .await - .get_request(path, None, self.get_timeout().await, true, true) - .await + self.get_request(path, None, true, true).await } // Milestones routes. @@ -397,12 +331,7 @@ impl ClientInner { pub async fn get_milestone_by_id(&self, milestone_id: &MilestoneId) -> Result { let path = &format!("api/core/v2/milestones/{milestone_id}"); - let dto = self - .node_manager - .read() - .await - .get_request::(path, None, self.get_timeout().await, false, true) - .await?; + let dto = self.get_request::(path, None, false, true).await?; Ok(MilestonePayload::try_from_dto_with_params( dto, @@ -415,11 +344,7 @@ impl ClientInner { pub async fn get_milestone_by_id_raw(&self, milestone_id: &MilestoneId) -> Result> { let path = &format!("api/core/v2/milestones/{milestone_id}"); - self.node_manager - .read() - .await - .get_request_bytes(path, None, self.get_timeout().await) - .await + self.get_request_bytes(path, None).await } /// Gets all UTXO changes of a milestone by its milestone id. @@ -427,11 +352,7 @@ impl ClientInner { pub async fn get_utxo_changes_by_id(&self, milestone_id: &MilestoneId) -> Result { let path = &format!("api/core/v2/milestones/{milestone_id}/utxo-changes"); - self.node_manager - .read() - .await - .get_request(path, None, self.get_timeout().await, false, false) - .await + self.get_request(path, None, false, false).await } /// Gets the milestone by the given milestone index. @@ -439,12 +360,7 @@ impl ClientInner { pub async fn get_milestone_by_index(&self, index: u32) -> Result { let path = &format!("api/core/v2/milestones/by-index/{index}"); - let dto = self - .node_manager - .read() - .await - .get_request::(path, None, self.get_timeout().await, false, true) - .await?; + let dto = self.get_request::(path, None, false, true).await?; Ok(MilestonePayload::try_from_dto_with_params( dto, @@ -457,11 +373,7 @@ impl ClientInner { pub async fn get_milestone_by_index_raw(&self, index: u32) -> Result> { let path = &format!("api/core/v2/milestones/by-index/{index}"); - self.node_manager - .read() - .await - .get_request_bytes(path, None, self.get_timeout().await) - .await + self.get_request_bytes(path, None).await } /// Gets all UTXO changes of a milestone by its milestone index. @@ -469,11 +381,7 @@ impl ClientInner { pub async fn get_utxo_changes_by_index(&self, index: u32) -> Result { let path = &format!("api/core/v2/milestones/by-index/{index}/utxo-changes"); - self.node_manager - .read() - .await - .get_request(path, None, self.get_timeout().await, false, false) - .await + self.get_request(path, None, false, false).await } // Peers routes. @@ -482,12 +390,7 @@ impl ClientInner { pub async fn get_peers(&self) -> Result> { let path = "api/core/v2/peers"; - let resp = self - .node_manager - .read() - .await - .get_request::>(path, None, self.get_timeout().await, false, false) - .await?; + let resp = self.get_request::>(path, None, false, false).await?; Ok(resp) } diff --git a/sdk/src/client/node_api/indexer/mod.rs b/sdk/src/client/node_api/indexer/mod.rs index 128e891f68..6ff4876411 100644 --- a/sdk/src/client/node_api/indexer/mod.rs +++ b/sdk/src/client/node_api/indexer/mod.rs @@ -33,13 +33,9 @@ impl ClientInner { while let Some(cursor) = { let output_ids_response = self - .node_manager - .read() - .await .get_request::( route, query_parameters.to_query_string().as_deref(), - self.get_timeout().await, need_quorum, prefer_permanode, ) diff --git a/sdk/src/client/node_api/participation.rs b/sdk/src/client/node_api/participation.rs index 4962d851ce..928a63a56c 100644 --- a/sdk/src/client/node_api/participation.rs +++ b/sdk/src/client/node_api/participation.rs @@ -29,22 +29,14 @@ impl ClientInner { ParticipationEventType::Staking => "type=1", }); - self.node_manager - .read() - .await - .get_request(route, query, self.get_timeout().await, false, false) - .await + self.get_request(route, query, false, false).await } /// RouteParticipationEvent is the route to access a single participation by its ID. pub async fn event(&self, event_id: &ParticipationEventId) -> Result { let route = format!("api/participation/v1/events/{event_id}"); - self.node_manager - .read() - .await - .get_request(&route, None, self.get_timeout().await, false, false) - .await + self.get_request(&route, None, false, false).await } /// RouteParticipationEventStatus is the route to access the status of a single participation by its ID. @@ -55,28 +47,20 @@ impl ClientInner { ) -> Result { let route = format!("api/participation/v1/events/{event_id}/status"); - self.node_manager - .read() - .await - .get_request( - &route, - milestone_index.map(|index| index.to_string()).as_deref(), - self.get_timeout().await, - false, - false, - ) - .await + self.get_request( + &route, + milestone_index.map(|index| index.to_string()).as_deref(), + false, + false, + ) + .await } /// RouteOutputStatus is the route to get the vote status for a given output ID. pub async fn output_status(&self, output_id: &OutputId) -> Result { let route = format!("api/participation/v1/outputs/{output_id}"); - self.node_manager - .read() - .await - .get_request(&route, None, self.get_timeout().await, false, false) - .await + self.get_request(&route, None, false, false).await } /// RouteAddressBech32Status is the route to get the staking rewards for the given bech32 address. @@ -86,11 +70,7 @@ impl ClientInner { ) -> Result { let route = format!("api/participation/v1/addresses/{}", bech32_address.convert()?); - self.node_manager - .read() - .await - .get_request(&route, None, self.get_timeout().await, false, false) - .await + self.get_request(&route, None, false, false).await } /// RouteAddressBech32Outputs is the route to get the outputs for the given bech32 address. @@ -100,10 +80,6 @@ impl ClientInner { ) -> Result { let route = format!("api/participation/v1/addresses/{}/outputs", bech32_address.convert()?); - self.node_manager - .read() - .await - .get_request(&route, None, self.get_timeout().await, false, false) - .await + self.get_request(&route, None, false, false).await } } diff --git a/sdk/src/client/node_api/plugin/mod.rs b/sdk/src/client/node_api/plugin/mod.rs index aa7260a8f4..493932da79 100644 --- a/sdk/src/client/node_api/plugin/mod.rs +++ b/sdk/src/client/node_api/plugin/mod.rs @@ -27,17 +27,11 @@ impl ClientInner { let req_method = reqwest::Method::from_str(&method); - let node_manager = self.node_manager.read().await; let path = format!("{}{}{}", base_plugin_path, endpoint, query_params.join("&")); - let timeout = self.get_timeout().await; match req_method { - Ok(Method::GET) => node_manager.get_request(&path, None, timeout, false, false).await, - Ok(Method::POST) => { - node_manager - .post_request_json(&path, timeout, request_object.into(), true) - .await - } + Ok(Method::GET) => self.get_request(&path, None, false, false).await, + Ok(Method::POST) => self.post_request_json(&path, request_object.into(), true).await, _ => Err(crate::client::Error::Node( crate::client::node_api::error::Error::NotSupported(method.to_string()), )), diff --git a/sdk/src/client/node_manager/mod.rs b/sdk/src/client/node_manager/mod.rs index cfa8003269..f6468e2bfd 100644 --- a/sdk/src/client/node_manager/mod.rs +++ b/sdk/src/client/node_manager/mod.rs @@ -11,13 +11,18 @@ pub(crate) mod syncing; use std::{ collections::{HashMap, HashSet}, + fmt::Debug, sync::RwLock, time::Duration, }; +use serde::{de::DeserializeOwned, Serialize}; use serde_json::Value; use self::{http_client::HttpClient, node::Node}; +use super::ClientInner; +#[cfg(not(target_family = "wasm"))] +use crate::client::request_pool::RateLimitExt; use crate::{ client::{ error::{Error, Result}, @@ -42,7 +47,7 @@ pub struct NodeManager { pub(crate) http_client: HttpClient, } -impl std::fmt::Debug for NodeManager { +impl Debug for NodeManager { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut d = f.debug_struct("NodeManager"); d.field("primary_node", &self.primary_node); @@ -58,6 +63,43 @@ impl std::fmt::Debug for NodeManager { } } +impl ClientInner { + pub(crate) async fn get_request( + &self, + path: &str, + query: Option<&str>, + need_quorum: bool, + prefer_permanode: bool, + ) -> Result { + let node_manager = self.node_manager.read().await; + let request = node_manager.get_request(path, query, self.get_timeout().await, need_quorum, prefer_permanode); + #[cfg(not(target_family = "wasm"))] + let request = request.rate_limit(&self.request_pool); + request.await + } + + pub(crate) async fn get_request_bytes(&self, path: &str, query: Option<&str>) -> Result> { + let node_manager = self.node_manager.read().await; + let request = node_manager.get_request_bytes(path, query, self.get_timeout().await); + #[cfg(not(target_family = "wasm"))] + let request = request.rate_limit(&self.request_pool); + request.await + } + + pub(crate) async fn post_request_json( + &self, + path: &str, + json: Value, + local_pow: bool, + ) -> Result { + let node_manager = self.node_manager.read().await; + let request = node_manager.post_request_json(path, self.get_timeout().await, json, local_pow); + #[cfg(not(target_family = "wasm"))] + let request = request.rate_limit(&self.request_pool); + request.await + } +} + impl NodeManager { pub(crate) fn builder() -> NodeManagerBuilder { NodeManagerBuilder::new() @@ -164,7 +206,7 @@ impl NodeManager { Ok(nodes_with_modified_url) } - pub(crate) async fn get_request( + pub(crate) async fn get_request( &self, path: &str, query: Option<&str>, @@ -312,7 +354,7 @@ impl NodeManager { Err(error.unwrap()) } - pub(crate) async fn post_request_bytes( + pub(crate) async fn post_request_bytes( &self, path: &str, timeout: Duration, @@ -341,7 +383,7 @@ impl NodeManager { Err(error.unwrap()) } - pub(crate) async fn post_request_json( + pub(crate) async fn post_request_json( &self, path: &str, timeout: Duration, diff --git a/sdk/src/client/request_pool.rs b/sdk/src/client/request_pool.rs new file mode 100644 index 0000000000..d040b2c664 --- /dev/null +++ b/sdk/src/client/request_pool.rs @@ -0,0 +1,93 @@ +// Copyright 2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use alloc::sync::Arc; + +use async_trait::async_trait; +use futures::Future; +use tokio::sync::{ + mpsc::{UnboundedReceiver, UnboundedSender}, + RwLock, +}; + +#[derive(Debug, Clone)] +pub(crate) struct RequestPool { + inner: Arc>, +} + +#[derive(Debug)] +pub(crate) struct RequestPoolInner { + sender: UnboundedSender<()>, + recv: UnboundedReceiver<()>, + size: usize, +} + +#[derive(Debug)] +pub(crate) struct Requester { + sender: UnboundedSender<()>, +} + +impl RequestPool { + pub(crate) fn new(size: usize) -> Self { + Self { + inner: Arc::new(RwLock::new(RequestPoolInner::new(size))), + } + } + + pub(crate) async fn borrow(&self) -> Requester { + // Get permission to request + let mut lock = self.write().await; + lock.recv.recv().await; + let sender = lock.sender.clone(); + drop(lock); + Requester { sender } + } + + pub(crate) async fn size(&self) -> usize { + self.read().await.size + } + + pub(crate) async fn resize(&self, new_size: usize) { + *self.write().await = RequestPoolInner::new(new_size); + } +} + +impl core::ops::Deref for RequestPool { + type Target = RwLock; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl RequestPoolInner { + fn new(size: usize) -> Self { + let (sender, recv) = tokio::sync::mpsc::unbounded_channel(); + // Prepare the channel with the requesters + for _ in 0..size { + sender.send(()).ok(); + } + Self { sender, recv, size } + } +} + +impl Drop for Requester { + fn drop(&mut self) { + // This can only fail if the receiver is closed, in which case we don't care. + self.sender.send(()).ok(); + } +} + +#[async_trait] +pub(crate) trait RateLimitExt: Future { + async fn rate_limit(self, request_pool: &RequestPool) -> Self::Output + where + Self: Sized, + { + let requester = request_pool.borrow().await; + let output = self.await; + drop(requester); + output + } +} +impl RateLimitExt for F {} diff --git a/sdk/src/client/secret/mod.rs b/sdk/src/client/secret/mod.rs index 784c02aa7a..25f3738c2b 100644 --- a/sdk/src/client/secret/mod.rs +++ b/sdk/src/client/secret/mod.rs @@ -3,12 +3,17 @@ //! Secret manager module enabling address generation and transaction essence signing. +/// Module for ledger nano based secret management. #[cfg(feature = "ledger_nano")] #[cfg_attr(docsrs, doc(cfg(feature = "ledger_nano")))] pub mod ledger_nano; -/// Module for signing with a mnemonic or seed +/// Module for mnemonic based secret management. pub mod mnemonic; -/// Module for signing with a Stronghold vault +/// Module for single private key based secret management. +#[cfg(feature = "private_key_secret_manager")] +#[cfg_attr(docsrs, doc(cfg(feature = "private_key_secret_manager")))] +pub mod private_key; +/// Module for stronghold based secret management. #[cfg(feature = "stronghold")] #[cfg_attr(docsrs, doc(cfg(feature = "stronghold")))] pub mod stronghold; @@ -30,6 +35,8 @@ use zeroize::Zeroizing; #[cfg(feature = "ledger_nano")] use self::ledger_nano::LedgerSecretManager; use self::mnemonic::MnemonicSecretManager; +#[cfg(feature = "private_key_secret_manager")] +use self::private_key::PrivateKeySecretManager; #[cfg(feature = "stronghold")] use self::stronghold::StrongholdSecretManager; pub use self::types::{GenerateAddressOptions, LedgerNanoStatus}; @@ -137,11 +144,43 @@ pub enum SecretManager { /// LedgerNano or Stronghold instead. Mnemonic(MnemonicSecretManager), + /// Secret manager that uses a single private key. + #[cfg(feature = "private_key_secret_manager")] + #[cfg_attr(docsrs, doc(cfg(feature = "private_key_secret_manager")))] + PrivateKey(Box), + /// Secret manager that's just a placeholder, so it can be provided to an online wallet, but can't be used for /// signing. Placeholder, } +#[cfg(feature = "stronghold")] +impl From for SecretManager { + fn from(secret_manager: StrongholdSecretManager) -> Self { + Self::Stronghold(secret_manager) + } +} + +#[cfg(feature = "ledger_nano")] +impl From for SecretManager { + fn from(secret_manager: LedgerSecretManager) -> Self { + Self::LedgerNano(secret_manager) + } +} + +impl From for SecretManager { + fn from(secret_manager: MnemonicSecretManager) -> Self { + Self::Mnemonic(secret_manager) + } +} + +#[cfg(feature = "private_key_secret_manager")] +impl From for SecretManager { + fn from(secret_manager: PrivateKeySecretManager) -> Self { + Self::PrivateKey(Box::new(secret_manager)) + } +} + impl Debug for SecretManager { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -150,6 +189,8 @@ impl Debug for SecretManager { #[cfg(feature = "ledger_nano")] Self::LedgerNano(_) => f.debug_tuple("LedgerNano").field(&"...").finish(), Self::Mnemonic(_) => f.debug_tuple("Mnemonic").field(&"...").finish(), + #[cfg(feature = "private_key_secret_manager")] + Self::PrivateKey(_) => f.debug_tuple("PrivateKey").field(&"...").finish(), Self::Placeholder => f.debug_struct("Placeholder").finish(), } } @@ -180,6 +221,11 @@ pub enum SecretManagerDto { /// Mnemonic #[serde(alias = "mnemonic")] Mnemonic(Zeroizing), + /// Private Key + #[cfg(feature = "private_key_secret_manager")] + #[cfg_attr(docsrs, doc(cfg(feature = "private_key_secret_manager")))] + #[serde(alias = "privateKey")] + PrivateKey(Zeroizing), /// Hex seed #[serde(alias = "hexSeed")] HexSeed(Zeroizing), @@ -215,6 +261,11 @@ impl TryFrom for SecretManager { Self::Mnemonic(MnemonicSecretManager::try_from_mnemonic(mnemonic.as_str().to_owned())?) } + #[cfg(feature = "private_key_secret_manager")] + SecretManagerDto::PrivateKey(private_key) => { + Self::PrivateKey(Box::new(PrivateKeySecretManager::try_from_hex(private_key)?)) + } + SecretManagerDto::HexSeed(hex_seed) => { // `SecretManagerDto` is `ZeroizeOnDrop` so it will take care of zeroizing the original. Self::Mnemonic(MnemonicSecretManager::try_from_hex_seed(hex_seed)?) @@ -247,6 +298,10 @@ impl From<&SecretManager> for SecretManagerDto { // the client/wallet we also don't need to convert it in this direction with the mnemonic/seed, we only need // to know the type SecretManager::Mnemonic(_mnemonic) => Self::Mnemonic("...".to_string().into()), + + #[cfg(feature = "private_key_secret_manager")] + SecretManager::PrivateKey(_private_key) => Self::PrivateKey("...".to_string().into()), + SecretManager::Placeholder => Self::Placeholder, } } @@ -277,6 +332,12 @@ impl SecretManage for SecretManager { .generate_ed25519_addresses(coin_type, account_index, address_indexes, options) .await } + #[cfg(feature = "private_key_secret_manager")] + Self::PrivateKey(secret_manager) => { + secret_manager + .generate_ed25519_addresses(coin_type, account_index, address_indexes, options) + .await + } Self::Placeholder => Err(Error::PlaceholderSecretManager), } } @@ -302,6 +363,12 @@ impl SecretManage for SecretManager { .generate_evm_addresses(coin_type, account_index, address_indexes, options) .await } + #[cfg(feature = "private_key_secret_manager")] + Self::PrivateKey(secret_manager) => { + secret_manager + .generate_evm_addresses(coin_type, account_index, address_indexes, options) + .await + } Self::Placeholder => Err(Error::PlaceholderSecretManager), } } @@ -313,6 +380,8 @@ impl SecretManage for SecretManager { #[cfg(feature = "ledger_nano")] Self::LedgerNano(secret_manager) => Ok(secret_manager.sign_ed25519(msg, chain).await?), Self::Mnemonic(secret_manager) => secret_manager.sign_ed25519(msg, chain).await, + #[cfg(feature = "private_key_secret_manager")] + Self::PrivateKey(secret_manager) => secret_manager.sign_ed25519(msg, chain).await, Self::Placeholder => Err(Error::PlaceholderSecretManager), } } @@ -328,6 +397,8 @@ impl SecretManage for SecretManager { #[cfg(feature = "ledger_nano")] Self::LedgerNano(secret_manager) => Ok(secret_manager.sign_secp256k1_ecdsa(msg, chain).await?), Self::Mnemonic(secret_manager) => secret_manager.sign_secp256k1_ecdsa(msg, chain).await, + #[cfg(feature = "private_key_secret_manager")] + Self::PrivateKey(secret_manager) => secret_manager.sign_secp256k1_ecdsa(msg, chain).await, Self::Placeholder => Err(Error::PlaceholderSecretManager), } } @@ -351,6 +422,12 @@ impl SecretManage for SecretManager { .sign_transaction_essence(prepared_transaction_data, time) .await } + #[cfg(feature = "private_key_secret_manager")] + Self::PrivateKey(secret_manager) => { + secret_manager + .sign_transaction_essence(prepared_transaction_data, time) + .await + } Self::Placeholder => Err(Error::PlaceholderSecretManager), } } @@ -365,6 +442,8 @@ impl SecretManage for SecretManager { #[cfg(feature = "ledger_nano")] Self::LedgerNano(secret_manager) => Ok(secret_manager.sign_transaction(prepared_transaction_data).await?), Self::Mnemonic(secret_manager) => secret_manager.sign_transaction(prepared_transaction_data).await, + #[cfg(feature = "private_key_secret_manager")] + Self::PrivateKey(secret_manager) => secret_manager.sign_transaction(prepared_transaction_data).await, Self::Placeholder => Err(Error::PlaceholderSecretManager), } } @@ -390,6 +469,8 @@ impl SecretManagerConfig for SecretManager { #[cfg(feature = "ledger_nano")] Self::LedgerNano(s) => s.to_config().map(Self::Config::LedgerNano), Self::Mnemonic(_) => None, + #[cfg(feature = "private_key_secret_manager")] + Self::PrivateKey(_) => None, Self::Placeholder => None, } } @@ -406,6 +487,10 @@ impl SecretManagerConfig for SecretManager { SecretManagerDto::Mnemonic(mnemonic) => { Self::Mnemonic(MnemonicSecretManager::try_from_mnemonic(mnemonic.as_str().to_owned())?) } + #[cfg(feature = "private_key_secret_manager")] + SecretManagerDto::PrivateKey(private_key) => { + Self::PrivateKey(Box::new(PrivateKeySecretManager::try_from_hex(private_key.to_owned())?)) + } SecretManagerDto::Placeholder => Self::Placeholder, }) } diff --git a/sdk/src/client/secret/private_key.rs b/sdk/src/client/secret/private_key.rs new file mode 100644 index 0000000000..7b11ab0ce9 --- /dev/null +++ b/sdk/src/client/secret/private_key.rs @@ -0,0 +1,132 @@ +// Copyright 2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +//! Implementation of [`PrivateKeySecretManager`]. + +use std::ops::Range; + +use async_trait::async_trait; +use crypto::{ + hashes::{blake2b::Blake2b256, Digest}, + keys::bip44::Bip44, + signatures::{ + ed25519, + secp256k1_ecdsa::{self, EvmAddress}, + }, +}; +use zeroize::{Zeroize, Zeroizing}; + +use super::{GenerateAddressOptions, SecretManage}; +use crate::{ + client::{api::PreparedTransactionData, Error}, + types::block::{ + address::Ed25519Address, payload::transaction::TransactionPayload, signature::Ed25519Signature, unlock::Unlocks, + }, +}; + +/// Secret manager based on a single private key. +pub struct PrivateKeySecretManager(ed25519::SecretKey); + +impl std::fmt::Debug for PrivateKeySecretManager { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_tuple("PrivateKeySecretManager").finish() + } +} + +#[async_trait] +impl SecretManage for PrivateKeySecretManager { + type Error = Error; + + async fn generate_ed25519_addresses( + &self, + _coin_type: u32, + _account_index: u32, + _address_indexes: Range, + _options: impl Into> + Send, + ) -> Result, Self::Error> { + let public_key = self.0.public_key().to_bytes(); + + // Hash the public key to get the address + let result = Blake2b256::digest(public_key).try_into().map_err(|_e| { + crate::client::Error::Blake2b256("hashing the public key while generating the address failed.") + })?; + + crate::client::Result::Ok(vec![Ed25519Address::new(result)]) + } + + async fn generate_evm_addresses( + &self, + _coin_type: u32, + _account_index: u32, + _address_indexes: Range, + _options: impl Into> + Send, + ) -> Result, Self::Error> { + // TODO replace with a more fitting variant. + Err(Error::SecretManagerMismatch) + } + + async fn sign_ed25519(&self, msg: &[u8], _chain: Bip44) -> Result { + let public_key = self.0.public_key(); + let signature = self.0.sign(msg); + + Ok(Ed25519Signature::new(public_key, signature)) + } + + async fn sign_secp256k1_ecdsa( + &self, + _msg: &[u8], + _chain: Bip44, + ) -> Result<(secp256k1_ecdsa::PublicKey, secp256k1_ecdsa::RecoverableSignature), Self::Error> { + // TODO replace with a more fitting variant. + Err(Error::SecretManagerMismatch) + } + + async fn sign_transaction_essence( + &self, + prepared_transaction_data: &PreparedTransactionData, + time: Option, + ) -> Result { + super::default_sign_transaction_essence(self, prepared_transaction_data, time).await + } + + async fn sign_transaction( + &self, + prepared_transaction_data: PreparedTransactionData, + ) -> Result { + super::default_sign_transaction(self, prepared_transaction_data).await + } +} + +impl PrivateKeySecretManager { + /// Create a new [`PrivateKeySecretManager`] from a base 58 encoded private key. + pub fn try_from_b58>(b58: T) -> Result { + let mut bytes = [0u8; ed25519::SecretKey::LENGTH]; + + // TODO replace with a more fitting variant. + if bs58::decode(b58.as_ref()) + .onto(&mut bytes) + .map_err(|_| crypto::Error::PrivateKeyError)? + != ed25519::SecretKey::LENGTH + { + // TODO replace with a more fitting variant. + return Err(crypto::Error::PrivateKeyError.into()); + } + + let private_key = Self(ed25519::SecretKey::from_bytes(&bytes)); + + bytes.zeroize(); + + Ok(private_key) + } + + /// Create a new [`PrivateKeySecretManager`] from an hex encoded private key. + pub fn try_from_hex(hex: impl Into>) -> Result { + let mut bytes = prefix_hex::decode(hex.into())?; + + let private_key = Self(ed25519::SecretKey::from_bytes(&bytes)); + + bytes.zeroize(); + + Ok(private_key) + } +} diff --git a/sdk/src/client/stronghold/common.rs b/sdk/src/client/stronghold/common.rs index 86d41acc51..444c3bc3c2 100644 --- a/sdk/src/client/stronghold/common.rs +++ b/sdk/src/client/stronghold/common.rs @@ -25,7 +25,7 @@ pub(super) const DERIVE_OUTPUT_RECORD_PATH: &[u8] = b"iota-wallet-derived"; /// The client path for the seed. /// /// The value has been hard-coded historically. -pub(super) const PRIVATE_DATA_CLIENT_PATH: &[u8] = b"iota_seed"; +pub(crate) const PRIVATE_DATA_CLIENT_PATH: &[u8] = b"iota_seed"; /// The path for the user-data encryption key for the Stronghold store. pub(super) const USERDATA_STORE_KEY_RECORD_PATH: &[u8] = b"userdata-store-key"; diff --git a/sdk/src/client/stronghold/mod.rs b/sdk/src/client/stronghold/mod.rs index 79c6fe16a3..7cd0552567 100644 --- a/sdk/src/client/stronghold/mod.rs +++ b/sdk/src/client/stronghold/mod.rs @@ -67,7 +67,7 @@ use tokio::{ }; use zeroize::Zeroizing; -use self::common::PRIVATE_DATA_CLIENT_PATH; +pub(crate) use self::common::PRIVATE_DATA_CLIENT_PATH; pub use self::error::Error; use super::{storage::StorageAdapter, utils::Password}; diff --git a/sdk/src/wallet/core/builder.rs b/sdk/src/wallet/core/builder.rs index 978ee198b2..31e2bba876 100644 --- a/sdk/src/wallet/core/builder.rs +++ b/sdk/src/wallet/core/builder.rs @@ -251,7 +251,7 @@ where #[cfg(feature = "storage")] pub(crate) async fn from_wallet(wallet: &Wallet) -> Self { Self { - client_options: Some(ClientOptions::from_client(wallet.client()).await), + client_options: Some(wallet.client_options().await), coin_type: Some(wallet.coin_type.load(Ordering::Relaxed)), storage_options: Some(wallet.storage_options.clone()), secret_manager: Some(wallet.secret_manager.clone()), diff --git a/sdk/src/wallet/core/operations/address_generation.rs b/sdk/src/wallet/core/operations/address_generation.rs index 494894fddd..f84277de38 100644 --- a/sdk/src/wallet/core/operations/address_generation.rs +++ b/sdk/src/wallet/core/operations/address_generation.rs @@ -109,6 +109,17 @@ impl Wallet { ) .await? } + #[cfg(feature = "private_key_secret_manager")] + SecretManager::PrivateKey(private_key) => { + private_key + .generate_ed25519_addresses( + self.coin_type.load(Ordering::Relaxed), + account_index, + address_index..address_index + 1, + options, + ) + .await? + } SecretManager::Placeholder => return Err(crate::client::Error::PlaceholderSecretManager.into()), }; diff --git a/sdk/src/wallet/core/operations/client.rs b/sdk/src/wallet/core/operations/client.rs index bf8c883adb..a1a5f6d654 100644 --- a/sdk/src/wallet/core/operations/client.rs +++ b/sdk/src/wallet/core/operations/client.rs @@ -42,6 +42,8 @@ where remote_pow_timeout, #[cfg(not(target_family = "wasm"))] pow_worker_count, + #[cfg(not(target_family = "wasm"))] + max_parallel_api_requests, } = client_options; self.client .update_node_manager(node_manager_builder.build(HashMap::new())) @@ -50,6 +52,8 @@ where *self.client.api_timeout.write().await = api_timeout; *self.client.remote_pow_timeout.write().await = remote_pow_timeout; #[cfg(not(target_family = "wasm"))] + self.client.request_pool.resize(max_parallel_api_requests).await; + #[cfg(not(target_family = "wasm"))] { *self.client.pow_worker_count.write().await = pow_worker_count; } diff --git a/sdk/src/wallet/core/operations/storage.rs b/sdk/src/wallet/core/operations/storage.rs index 9eb6db79a7..282e421769 100644 --- a/sdk/src/wallet/core/operations/storage.rs +++ b/sdk/src/wallet/core/operations/storage.rs @@ -13,8 +13,8 @@ mod storage_stub { }, wallet::{ core::builder::dto::WalletBuilderDto, - storage::constants::{SECRET_MANAGER_KEY, WALLET_INDEXATION_KEY}, - WalletBuilder, + storage::constants::{CHRYSALIS_STORAGE_KEY, SECRET_MANAGER_KEY, WALLET_INDEXATION_KEY}, + Wallet, WalletBuilder, }, }; @@ -84,6 +84,14 @@ mod storage_stub { Ok(res.map(Into::into)) } } + + impl Wallet { + pub async fn get_chrysalis_data( + &self, + ) -> crate::wallet::Result>> { + self.storage_manager.read().await.get(CHRYSALIS_STORAGE_KEY).await + } + } } #[cfg(not(feature = "storage"))] mod storage_stub { diff --git a/sdk/src/wallet/core/operations/stronghold_backup/mod.rs b/sdk/src/wallet/core/operations/stronghold_backup/mod.rs index baa3cf6bb6..ca6ef165cd 100644 --- a/sdk/src/wallet/core/operations/stronghold_backup/mod.rs +++ b/sdk/src/wallet/core/operations/stronghold_backup/mod.rs @@ -13,10 +13,11 @@ use crate::wallet::WalletBuilder; use crate::{ client::{ secret::{stronghold::StrongholdSecretManager, SecretManager, SecretManagerConfig, SecretManagerDto}, + storage::StorageAdapter, utils::Password, }, types::block::address::Hrp, - wallet::{Account, Wallet}, + wallet::{storage::constants::CHRYSALIS_STORAGE_KEY, Account, Wallet}, }; impl Wallet { @@ -102,7 +103,7 @@ impl Wallet { .password(stronghold_password.clone()) .build(backup_path.clone())?; - let (read_client_options, read_coin_type, read_secret_manager, read_accounts) = + let (read_client_options, read_coin_type, read_secret_manager, read_accounts, chrysalis_data) = read_data_from_stronghold_snapshot::(&new_stronghold).await?; // If the coin type is not matching the current one, then the addresses in the accounts will also not be @@ -204,6 +205,13 @@ impl Wallet { for account in accounts.iter() { account.save(None).await?; } + if let Some(chrysalis_data) = chrysalis_data { + self.storage_manager + .read() + .await + .set(CHRYSALIS_STORAGE_KEY, &chrysalis_data) + .await?; + } } Ok(()) @@ -272,7 +280,7 @@ impl Wallet { .password(stronghold_password.clone()) .build(backup_path.clone())?; - let (read_client_options, read_coin_type, read_secret_manager, read_accounts) = + let (read_client_options, read_coin_type, read_secret_manager, read_accounts, chrysalis_data) = read_data_from_stronghold_snapshot::(&new_stronghold).await?; // If the coin type is not matching the current one, then the addresses in the accounts will also not be @@ -367,6 +375,13 @@ impl Wallet { for account in accounts.iter() { account.save(None).await?; } + if let Some(chrysalis_data) = chrysalis_data { + self.storage_manager + .read() + .await + .set(CHRYSALIS_STORAGE_KEY, &chrysalis_data) + .await?; + } } Ok(()) diff --git a/sdk/src/wallet/core/operations/stronghold_backup/stronghold_snapshot.rs b/sdk/src/wallet/core/operations/stronghold_backup/stronghold_snapshot.rs index 1ba02a9500..3b4adbb6a0 100644 --- a/sdk/src/wallet/core/operations/stronghold_backup/stronghold_snapshot.rs +++ b/sdk/src/wallet/core/operations/stronghold_backup/stronghold_snapshot.rs @@ -1,15 +1,22 @@ // Copyright 2022 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use std::sync::atomic::Ordering; +use std::{collections::HashMap, path::Path, sync::atomic::Ordering}; use crate::{ - client::{secret::SecretManagerConfig, storage::StorageAdapter, stronghold::StrongholdAdapter}, + client::{ + constants::IOTA_COIN_TYPE, secret::SecretManagerConfig, storage::StorageAdapter, stronghold::StrongholdAdapter, + Error as ClientError, + }, types::TryFromDto, wallet::{ account::{AccountDetails, AccountDetailsDto}, - migration::{latest_backup_migration_version, migrate, MIGRATION_VERSION_KEY}, - ClientOptions, Wallet, + migration::{ + chrysalis::{migrate_from_chrysalis_data, to_chrysalis_key}, + latest_backup_migration_version, migrate, MigrationData, MIGRATION_VERSION_KEY, + }, + storage::constants::{CHRYSALIS_STORAGE_KEY, WALLET_INDEXATION_KEY}, + ClientOptions, Error as WalletError, Wallet, }, }; @@ -55,7 +62,10 @@ pub(crate) async fn read_data_from_stronghold_snapshot, Option, Option>, + Option>, )> { + let chrysalis_data = migrate_snapshot_from_chrysalis_to_stardust(stronghold).await?; + migrate(stronghold).await?; // Get client_options @@ -89,5 +99,121 @@ pub(crate) async fn read_data_from_stronghold_snapshot crate::wallet::Result>> { + log::debug!("migrate_snapshot_from_chrysalis_to_stardust"); + let stronghold = stronghold_adapter.inner().await; + let stronghold_client = match stronghold.load_client(b"iota-wallet-records") { + Ok(client) => client, + // `iota-wallet-records` was only used in chrysalis + Err(iota_stronghold::ClientError::ClientDataNotPresent) => return Ok(None), + Err(e) => { + return Err(WalletError::Client(Box::new(ClientError::Stronghold(e.into())))); + } + }; + + let stronghold_store = stronghold_client.store(); + let keys = stronghold_store + .keys() + .map_err(|e| WalletError::Client(Box::new(ClientError::Stronghold(e.into()))))?; + + let wallet_indexation_key = to_chrysalis_key(b"iota-wallet-account-indexation", true); + // check if snapshot contains chrysalis data + if !keys.iter().any(|k| k == &wallet_indexation_key) { + return Ok(None); + } + + let mut chrysalis_data: HashMap, String> = HashMap::new(); + for key in keys { + let value = stronghold_store + .get(&key) + .map_err(|e| WalletError::Client(Box::new(ClientError::Stronghold(e.into()))))?; + + let value_utf8 = + String::from_utf8(value.unwrap()).map_err(|_| WalletError::Migration("invalid utf8".into()))?; + + chrysalis_data.insert(key, value_utf8); + } + drop(stronghold_store); + drop(stronghold_client); + drop(stronghold); + + let (new_accounts, secret_manager_dto) = + migrate_from_chrysalis_data(&chrysalis_data, Path::new("wallet.stronghold"), true)?; + + // convert to string keys + let chrysalis_data_with_string_keys = chrysalis_data + .iter() + .map(|(k, v)| { + Ok(( + // the key bytes are a hash in stronghold + prefix_hex::encode(k), + v.clone(), + )) + }) + .collect::>>()?; + + log::debug!( + "Chrysalis data: {}", + serde_json::to_string_pretty(&chrysalis_data_with_string_keys)? + ); + + // store chrysalis data in a new key + stronghold_adapter + .set(CHRYSALIS_STORAGE_KEY, &chrysalis_data_with_string_keys) + .await?; + + stronghold_adapter + .set( + ACCOUNTS_KEY, + &new_accounts + .into_iter() + .map(serde_json::to_value) + .collect::, serde_json::Error>>()?, + ) + .await?; + + if let Some(secret_manager_dto) = secret_manager_dto { + // This is required for the secret manager to be loaded + stronghold_adapter + .set( + WALLET_INDEXATION_KEY, + format!("{{ \"coinType\": {IOTA_COIN_TYPE}}}").as_bytes(), + ) + .await?; + stronghold_adapter + .set_bytes(SECRET_MANAGER_KEY, secret_manager_dto.as_bytes()) + .await?; + } + + // set db migration version + let migration_version = crate::wallet::migration::migrate_4::Migrate::version(); + stronghold_adapter + .set(MIGRATION_VERSION_KEY, &migration_version) + .await?; + + // Remove old entries + let stronghold = stronghold_adapter.inner().await; + let stronghold_client = stronghold + .get_client(b"iota-wallet-records") + .map_err(|e| WalletError::Client(Box::new(ClientError::Stronghold(e.into()))))?; + let stronghold_store = stronghold_client.store(); + + for key in chrysalis_data.keys() { + stronghold_store + .delete(key) + .map_err(|_| WalletError::Migration("couldn't delete old data".into()))?; + } + + Ok(Some(chrysalis_data_with_string_keys)) } diff --git a/sdk/src/wallet/migration/chrysalis.rs b/sdk/src/wallet/migration/chrysalis.rs new file mode 100644 index 0000000000..3a03fcd9a0 --- /dev/null +++ b/sdk/src/wallet/migration/chrysalis.rs @@ -0,0 +1,315 @@ +// Copyright 2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + collections::{HashMap, HashSet}, + convert::TryInto, + io::Read, + path::{Path, PathBuf}, + str::FromStr, +}; + +use crypto::{ + ciphers::{chacha::XChaCha20Poly1305, traits::Aead}, + macs::hmac::HMAC_SHA512, +}; +use rocksdb::{IteratorMode, DB}; +use serde::Serialize; +use serde_json::Value; +use zeroize::Zeroizing; + +use crate::{ + client::{constants::IOTA_COIN_TYPE, storage::StorageAdapter, Password}, + types::block::address::Bech32Address, + wallet::{ + migration::{MigrationData, MIGRATION_VERSION_KEY}, + storage::{ + constants::{ + ACCOUNTS_INDEXATION_KEY, ACCOUNT_INDEXATION_KEY, CHRYSALIS_STORAGE_KEY, SECRET_MANAGER_KEY, + WALLET_INDEXATION_KEY, + }, + StorageManager, + }, + Error, Result, + }, +}; + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +struct AccountAddress { + address: Bech32Address, + key_index: u32, + internal: bool, + used: bool, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AccountDetailsDto { + pub(crate) index: u32, + coin_type: u32, + alias: String, + public_addresses: Vec, + internal_addresses: Vec, + addresses_with_unspent_outputs: Vec, + outputs: HashMap, + locked_outputs: HashSet, + unspent_outputs: HashMap, + transactions: HashMap, + pending_transactions: HashSet, + incoming_transactions: HashMap, + native_token_foundries: HashMap, +} + +#[cfg(feature = "rocksdb")] +pub async fn migrate_db_chrysalis_to_stardust( + storage_path: impl Into + Send, + password: Option, + new_db_encryption_key: impl Into>> + Send, +) -> Result<()> { + let storage_path_string = storage_path.into(); + // `/db` will be appended to the chrysalis storage path, because that's how it was done in the chrysalis wallet + let chrysalis_storage_path = &(*storage_path_string).join("db"); + + let chrysalis_data = get_chrysalis_data(chrysalis_storage_path, password)?; + + // create new accounts base on previous data + let (new_accounts, secret_manager_dto) = migrate_from_chrysalis_data(&chrysalis_data, &storage_path_string, false)?; + + // convert to string keys + let chrysalis_data_with_string_keys = chrysalis_data + .into_iter() + .map(|(k, v)| { + Ok(( + String::from_utf8(k).map_err(|_| Error::Migration("invalid utf8".into()))?, + v, + )) + }) + .collect::>>()?; + + log::debug!( + "Chrysalis data: {}", + serde_json::to_string_pretty(&chrysalis_data_with_string_keys)? + ); + + let stardust_db = crate::wallet::storage::adapter::rocksdb::RocksdbStorageAdapter::new(storage_path_string)?; + + let stardust_storage = StorageManager::new(stardust_db, new_db_encryption_key).await?; + + // store chrysalis data in a new key + stardust_storage + .set(CHRYSALIS_STORAGE_KEY, &chrysalis_data_with_string_keys) + .await?; + // write new accounts to db (with account indexation) + let accounts_indexation_data: Vec = new_accounts.iter().map(|account| account.index).collect(); + stardust_storage + .set(ACCOUNTS_INDEXATION_KEY, &accounts_indexation_data) + .await?; + for new_account in new_accounts { + stardust_storage + .set(&format!("{ACCOUNT_INDEXATION_KEY}{}", new_account.index), &new_account) + .await?; + } + + if let Some(secret_manager_dto) = secret_manager_dto { + // This is required for the secret manager to be loaded + stardust_storage + .set( + WALLET_INDEXATION_KEY, + &serde_json::from_str::(&format!("{{ \"coinType\": {IOTA_COIN_TYPE}}}"))?, + ) + .await?; + stardust_storage + .set(SECRET_MANAGER_KEY, &serde_json::from_str::(&secret_manager_dto)?) + .await?; + } + + // set db migration version + let migration_version = crate::wallet::migration::migrate_4::Migrate::version(); + stardust_storage.set(MIGRATION_VERSION_KEY, &migration_version).await?; + + drop(stardust_storage); + + // remove old db + std::fs::remove_dir_all(chrysalis_storage_path)?; + + Ok(()) +} + +pub(crate) fn migrate_from_chrysalis_data( + chrysalis_data: &HashMap, String>, + storage_path: &Path, + // in stronghold the keys are hashed first + stronghold: bool, +) -> Result<(Vec, Option)> { + let mut new_accounts: Vec = Vec::new(); + let mut secret_manager_dto: Option = None; + + let account_indexation_key = to_chrysalis_key(b"iota-wallet-account-indexation", stronghold); + if let Some(account_indexation) = chrysalis_data.get(&account_indexation_key) { + if let Some(account_keys) = serde_json::from_str::(account_indexation)?.as_array() { + for account_key in account_keys { + let account_key = to_chrysalis_key( + account_key["key"].as_str().expect("key must be a string").as_bytes(), + stronghold, + ); + + if let Some(account_data) = chrysalis_data.get(&account_key) { + let account_data = serde_json::from_str::(account_data)?; + if secret_manager_dto.is_none() { + let dto = match &account_data["signerType"]["type"].as_str() { + Some("Stronghold") => format!( + r#"{{"Stronghold": {{"password": null, "timeout": null, "snapshotPath": "{}/wallet.stronghold"}} }}"#, + storage_path.to_string_lossy() + ), + Some("LedgerNano") => r#"{{"LedgerNano": false }}"#.into(), + Some("LedgerNanoSimulator") => r#"{{"LedgerNano": true }}"#.into(), + _ => return Err(Error::Migration("Missing signerType".into())), + }; + secret_manager_dto = Some(dto); + } + + let mut account_addresses = Vec::new(); + + // Migrate addresses, skips all above potential gaps (for example: index 0, 1, 3 -> 0, 1), public + // and internal addresses on their own + if let Some(addresses) = account_data["addresses"].as_array() { + let mut highest_public_address_index = 0; + let mut highest_internal_address_index = 0; + for address in addresses { + let internal = address["internal"].as_bool().unwrap(); + let key_index = address["keyIndex"].as_u64().unwrap() as u32; + let bech32_address = Bech32Address::from_str(address["address"].as_str().unwrap())?; + if internal { + if key_index != highest_internal_address_index { + log::warn!( + "Skip migrating internal address because of gap: {bech32_address}, index {key_index}" + ); + continue; + } + highest_internal_address_index += 1; + } else { + if key_index != highest_public_address_index { + log::warn!( + "Skip migrating public address because of gap: {bech32_address}, index {key_index}" + ); + continue; + } + highest_public_address_index += 1; + } + account_addresses.push(AccountAddress { + address: bech32_address, + key_index, + internal, + used: !address["outputs"].as_object().unwrap().is_empty(), + }) + } + } + let (internal, public): (Vec, Vec) = + account_addresses.into_iter().partition(|a| a.internal); + + new_accounts.push(AccountDetailsDto { + index: account_data["index"].as_u64().unwrap() as u32, + coin_type: IOTA_COIN_TYPE, + alias: account_data["alias"].as_str().unwrap().to_string(), + public_addresses: public, + internal_addresses: internal, + addresses_with_unspent_outputs: Vec::new(), + outputs: HashMap::new(), + unspent_outputs: HashMap::new(), + transactions: HashMap::new(), + pending_transactions: HashSet::new(), + locked_outputs: HashSet::new(), + incoming_transactions: HashMap::new(), + native_token_foundries: HashMap::new(), + }) + } + } + } + } + // Accounts must be ordered by index + new_accounts.sort_unstable_by_key(|a| a.index); + Ok((new_accounts, secret_manager_dto)) +} + +fn get_chrysalis_data(chrysalis_storage_path: &Path, password: Option) -> Result, String>> { + let encryption_key = password.map(storage_password_to_encryption_key); + let chrysalis_db = DB::open_default(chrysalis_storage_path)?; + let mut chrysalis_data = HashMap::new(); + // iterate over all rocksdb keys + for item in chrysalis_db.iterator(IteratorMode::Start) { + let (key, value) = item?; + + let key_utf8 = String::from_utf8(key.to_vec()).map_err(|_| Error::Migration("invalid utf8".into()))?; + let value = if let Some(encryption_key) = &encryption_key { + let value_utf8 = String::from_utf8(value.to_vec()).map_err(|_| Error::Migration("invalid utf8".into()))?; + // "iota-wallet-key-checksum_value" is never an encrypted value + if key_utf8 == "iota-wallet-key-checksum_value" { + value_utf8 + } else if let Ok(value) = serde_json::from_str::>(&value_utf8) { + decrypt_record(value, encryption_key)? + } else { + value_utf8 + } + } else { + String::from_utf8(value.to_vec()).map_err(|_| Error::Migration("invalid utf8".into()))? + }; + + chrysalis_data.insert(key.to_vec(), value); + } + Ok(chrysalis_data) +} + +fn storage_password_to_encryption_key(password: Password) -> Zeroizing<[u8; 32]> { + let mut dk = [0; 64]; + // safe to unwrap (rounds > 0) + crypto::keys::pbkdf::PBKDF2_HMAC_SHA512( + password.as_bytes(), + b"wallet.rs::storage", + core::num::NonZeroU32::new(100).unwrap(), + &mut dk, + ); + let key: [u8; 32] = dk[0..32][..].try_into().unwrap(); + Zeroizing::new(key) +} + +fn decrypt_record(record_bytes: Vec, encryption_key: &[u8; 32]) -> crate::wallet::Result { + let mut record: &[u8] = &record_bytes; + + let mut nonce = [0; XChaCha20Poly1305::NONCE_LENGTH]; + record.read_exact(&mut nonce)?; + + let mut tag = vec![0; XChaCha20Poly1305::TAG_LENGTH]; + record.read_exact(&mut tag)?; + + let mut ct = Vec::new(); + record.read_to_end(&mut ct)?; + + let mut pt = vec![0; ct.len()]; + // we can unwrap here since we know the lengths are valid + XChaCha20Poly1305::decrypt( + encryption_key.try_into().unwrap(), + &nonce.try_into().unwrap(), + &[], + &mut pt, + &ct, + tag.as_slice().try_into().unwrap(), + ) + .map_err(|e| Error::Migration(format!("{:?}", e)))?; + + String::from_utf8(pt).map_err(|e| Error::Migration(format!("{:?}", e))) +} + +pub(crate) fn to_chrysalis_key(key: &[u8], stronghold: bool) -> Vec { + // key only needs to be hashed for stronghold + if stronghold { + let mut buf = [0; 64]; + HMAC_SHA512(key, key, &mut buf); + + let (id, _) = buf.split_at(24); + + id.try_into().unwrap() + } else { + key.into() + } +} diff --git a/sdk/src/wallet/migration/mod.rs b/sdk/src/wallet/migration/mod.rs index bd9678feb6..139701d14d 100644 --- a/sdk/src/wallet/migration/mod.rs +++ b/sdk/src/wallet/migration/mod.rs @@ -1,16 +1,20 @@ // Copyright 2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +#[cfg(not(target_family = "wasm"))] +pub(crate) mod chrysalis; mod migrate_0; mod migrate_1; mod migrate_2; mod migrate_3; -mod migrate_4; +pub(crate) mod migrate_4; use std::collections::HashMap; use anymap::Map; use async_trait::async_trait; +#[cfg(not(target_family = "wasm"))] +pub use chrysalis::migrate_db_chrysalis_to_stardust; use once_cell::sync::Lazy; use serde::{de::DeserializeOwned, Deserialize, Serialize}; diff --git a/sdk/src/wallet/storage/adapter/rocksdb.rs b/sdk/src/wallet/storage/adapter/rocksdb.rs index b9ad5ad6ae..641fa12c74 100644 --- a/sdk/src/wallet/storage/adapter/rocksdb.rs +++ b/sdk/src/wallet/storage/adapter/rocksdb.rs @@ -9,9 +9,9 @@ use tokio::sync::Mutex; use crate::client::storage::StorageAdapter; /// Key value storage adapter. -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct RocksdbStorageAdapter { - db: Arc>, + pub(crate) db: Arc>, } impl RocksdbStorageAdapter { diff --git a/sdk/src/wallet/storage/constants.rs b/sdk/src/wallet/storage/constants.rs index ae75d94881..29d9fd973c 100644 --- a/sdk/src/wallet/storage/constants.rs +++ b/sdk/src/wallet/storage/constants.rs @@ -31,3 +31,5 @@ pub(crate) const DATABASE_SCHEMA_VERSION_KEY: &str = "database-schema-version"; pub(crate) const PARTICIPATION_EVENTS: &str = "participation-events"; #[cfg(feature = "participation")] pub(crate) const PARTICIPATION_CACHED_OUTPUTS: &str = "participation-cached-outputs"; + +pub(crate) const CHRYSALIS_STORAGE_KEY: &str = "chrysalis-data"; diff --git a/sdk/src/wallet/storage/mod.rs b/sdk/src/wallet/storage/mod.rs index 56ceed479c..4a3baffa2d 100644 --- a/sdk/src/wallet/storage/mod.rs +++ b/sdk/src/wallet/storage/mod.rs @@ -27,7 +27,7 @@ use crate::client::storage::StorageAdapter; #[derive(Debug)] pub struct Storage { - inner: Box, + pub(crate) inner: Box, encryption_key: Option>, } @@ -63,12 +63,6 @@ impl StorageAdapter for Storage { } } -impl Drop for Storage { - fn drop(&mut self) { - log::debug!("drop Storage"); - } -} - #[cfg(test)] mod tests { use serde::{Deserialize, Serialize}; diff --git a/sdk/tests/client/secret_manager/mnemonic.rs b/sdk/tests/client/secret_manager/mnemonic.rs new file mode 100644 index 0000000000..453fc9861c --- /dev/null +++ b/sdk/tests/client/secret_manager/mnemonic.rs @@ -0,0 +1,29 @@ +// Copyright 2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use iota_sdk::client::{ + api::GetAddressesOptions, constants::SHIMMER_TESTNET_BECH32_HRP, secret::SecretManager, Result, +}; + +#[tokio::test] +async fn mnemonic_secret_manager() -> Result<()> { + let dto = r#"{"mnemonic": "acoustic trophy damage hint search taste love bicycle foster cradle brown govern endless depend situate athlete pudding blame question genius transfer van random vast"}"#; + let secret_manager: SecretManager = dto.parse()?; + + let addresses = secret_manager + .generate_ed25519_addresses( + GetAddressesOptions::default() + .with_bech32_hrp(SHIMMER_TESTNET_BECH32_HRP) + .with_account_index(0) + .with_range(0..1), + ) + .await + .unwrap(); + + assert_eq!( + addresses[0], + "rms1qzev36lk0gzld0k28fd2fauz26qqzh4hd4cwymlqlv96x7phjxcw6v3ea5a" + ); + + Ok(()) +} diff --git a/sdk/tests/client/secret_manager/mod.rs b/sdk/tests/client/secret_manager/mod.rs new file mode 100644 index 0000000000..4e2a7988d5 --- /dev/null +++ b/sdk/tests/client/secret_manager/mod.rs @@ -0,0 +1,8 @@ +// Copyright 2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod mnemonic; +#[cfg(feature = "private_key_secret_manager")] +mod private_key; +#[cfg(feature = "stronghold")] +mod stronghold; diff --git a/sdk/tests/client/secret_manager/private_key.rs b/sdk/tests/client/secret_manager/private_key.rs new file mode 100644 index 0000000000..31c8d6f10b --- /dev/null +++ b/sdk/tests/client/secret_manager/private_key.rs @@ -0,0 +1,84 @@ +// Copyright 2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use iota_sdk::client::{ + api::GetAddressesOptions, + constants::SHIMMER_TESTNET_BECH32_HRP, + secret::{private_key::PrivateKeySecretManager, SecretManager}, + Result, +}; + +#[tokio::test] +async fn private_key_secret_manager_hex() -> Result<()> { + let dto = r#"{"privateKey": "0x9e845b327c44e28bdd206c7c9eff09c40680bc2512add57280baf5b064d7e6f6"}"#; + let secret_manager: SecretManager = dto.parse()?; + + let address_0 = secret_manager + .generate_ed25519_addresses( + GetAddressesOptions::default() + .with_bech32_hrp(SHIMMER_TESTNET_BECH32_HRP) + .with_account_index(0) + .with_range(0..1), + ) + .await + .unwrap()[0]; + // Changing range generates the same address. + let address_1 = secret_manager + .generate_ed25519_addresses( + GetAddressesOptions::default() + .with_bech32_hrp(SHIMMER_TESTNET_BECH32_HRP) + .with_account_index(0) + .with_range(1..2), + ) + .await + .unwrap()[0]; + // Changing account generates the same address. + let address_2 = secret_manager + .generate_ed25519_addresses( + GetAddressesOptions::default() + .with_bech32_hrp(SHIMMER_TESTNET_BECH32_HRP) + .with_account_index(1) + .with_range(0..1), + ) + .await + .unwrap()[0]; + + assert_eq!( + address_0, + "rms1qzev36lk0gzld0k28fd2fauz26qqzh4hd4cwymlqlv96x7phjxcw6v3ea5a" + ); + assert_eq!( + address_1, + "rms1qzev36lk0gzld0k28fd2fauz26qqzh4hd4cwymlqlv96x7phjxcw6v3ea5a" + ); + assert_eq!( + address_2, + "rms1qzev36lk0gzld0k28fd2fauz26qqzh4hd4cwymlqlv96x7phjxcw6v3ea5a" + ); + + Ok(()) +} + +#[tokio::test] +async fn private_key_secret_manager_bs58() -> Result<()> { + let secret_manager = SecretManager::from(PrivateKeySecretManager::try_from_b58( + "BfnURR6WSXJA6RyBr3WqGU99UzrVbWk9GSQgJqKtTRxZ", + )?); + + let address = secret_manager + .generate_ed25519_addresses( + GetAddressesOptions::default() + .with_bech32_hrp(SHIMMER_TESTNET_BECH32_HRP) + .with_account_index(0) + .with_range(0..1), + ) + .await + .unwrap()[0]; + + assert_eq!( + address, + "rms1qzev36lk0gzld0k28fd2fauz26qqzh4hd4cwymlqlv96x7phjxcw6v3ea5a" + ); + + Ok(()) +} diff --git a/sdk/tests/client/secret_manager.rs b/sdk/tests/client/secret_manager/stronghold.rs similarity index 77% rename from sdk/tests/client/secret_manager.rs rename to sdk/tests/client/secret_manager/stronghold.rs index 1147e2bd7d..0ff2515c72 100644 --- a/sdk/tests/client/secret_manager.rs +++ b/sdk/tests/client/secret_manager/stronghold.rs @@ -1,4 +1,4 @@ -// Copyright 2022 IOTA Stiftung +// Copyright 2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 use iota_sdk::client::{ @@ -6,31 +6,7 @@ use iota_sdk::client::{ }; #[tokio::test] -async fn mnemonic_secret_manager_dto() -> Result<()> { - let dto = r#"{"mnemonic": "acoustic trophy damage hint search taste love bicycle foster cradle brown govern endless depend situate athlete pudding blame question genius transfer van random vast"}"#; - let secret_manager: SecretManager = dto.parse()?; - - let addresses = secret_manager - .generate_ed25519_addresses( - GetAddressesOptions::default() - .with_bech32_hrp(SHIMMER_TESTNET_BECH32_HRP) - .with_account_index(0) - .with_range(0..1), - ) - .await - .unwrap(); - - assert_eq!( - addresses[0], - "rms1qzev36lk0gzld0k28fd2fauz26qqzh4hd4cwymlqlv96x7phjxcw6v3ea5a".to_string() - ); - - Ok(()) -} - -#[cfg(feature = "stronghold")] -#[tokio::test] -async fn stronghold_secret_manager_dto() -> Result<()> { +async fn stronghold_secret_manager() -> Result<()> { iota_stronghold::engine::snapshot::try_set_encrypt_work_factor(0).unwrap(); let dto = r#"{"stronghold": {"password": "some_hopefully_secure_password", "snapshotPath": "snapshot_test_dir/test.stronghold"}}"#; @@ -59,7 +35,7 @@ async fn stronghold_secret_manager_dto() -> Result<()> { assert_eq!( addresses[0], - "rms1qzev36lk0gzld0k28fd2fauz26qqzh4hd4cwymlqlv96x7phjxcw6v3ea5a".to_string() + "rms1qzev36lk0gzld0k28fd2fauz26qqzh4hd4cwymlqlv96x7phjxcw6v3ea5a" ); // Calling store_mnemonic() twice should fail, because we would otherwise overwrite the stored entry @@ -74,7 +50,6 @@ async fn stronghold_secret_manager_dto() -> Result<()> { Ok(()) } -#[cfg(feature = "stronghold")] #[tokio::test] async fn stronghold_mnemonic_missing() -> Result<()> { iota_stronghold::engine::snapshot::try_set_encrypt_work_factor(0).unwrap(); diff --git a/sdk/tests/wallet/chrysalis_migration.rs b/sdk/tests/wallet/chrysalis_migration.rs new file mode 100644 index 0000000000..0105eabcc2 --- /dev/null +++ b/sdk/tests/wallet/chrysalis_migration.rs @@ -0,0 +1,358 @@ +// Copyright 2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::{fs, io, path::Path}; + +use iota_sdk::{ + client::{constants::IOTA_COIN_TYPE, secret::SecretManager, Password}, + types::block::address::{Hrp, ToBech32Ext}, + wallet::{ + migration::migrate_db_chrysalis_to_stardust, + storage::{StorageKind, StorageOptions}, + ClientOptions, Result, + }, + Wallet, +}; +use zeroize::Zeroizing; + +use crate::wallet::common::{setup, tear_down}; + +#[tokio::test] +async fn migrate_chrysalis_db() -> Result<()> { + iota_stronghold::engine::snapshot::try_set_encrypt_work_factor(0).unwrap(); + let storage_path = "migrate_chrysalis_db/db"; + setup(storage_path)?; + // Copy db so the original doesn't get modified + copy_folder("./tests/wallet/fixtures/chrysalis-db/db", storage_path).unwrap(); + std::fs::copy( + "./tests/wallet/fixtures/chrysalis-db/wallet.stronghold", + "migrate_chrysalis_db/wallet.stronghold", + ) + .unwrap(); + + migrate_db_chrysalis_to_stardust("migrate_chrysalis_db", None, None).await?; + + let client_options = ClientOptions::new(); + let wallet = Wallet::builder() + .with_storage_path("migrate_chrysalis_db") + .with_client_options(client_options) + .finish() + .await?; + + let accounts = wallet.get_accounts().await?; + assert_eq!(accounts.len(), 2); + assert_eq!(accounts[0].alias().await, "Alice"); + assert_eq!(accounts[1].alias().await, "Bob"); + + let alice_acc_details = accounts[0].details().await; + assert_eq!(alice_acc_details.public_addresses().len(), 2); + assert_eq!( + alice_acc_details.public_addresses()[0].address().try_to_bech32("rms")?, + "rms1qqqu7qry22f6v7d2d9aesny9vjtf56unpevkfzfudddlcq5ja9clv44sef6" + ); + assert_eq!(alice_acc_details.internal_addresses().len(), 1); + assert_eq!( + alice_acc_details.internal_addresses()[0] + .address() + .try_to_bech32("rms")?, + "rms1qz4tac74vympq4hqqz8g9egrkhscn9743svd9xxh2w99qf5cd8vcxrmspmw" + ); + + let bob_acc_details = accounts[1].details().await; + assert_eq!(bob_acc_details.public_addresses().len(), 1); + assert_eq!( + bob_acc_details.public_addresses()[0].address().try_to_bech32("rms")?, + "rms1qql3h5vxh2sxa93yadh7f4rkr7f9g9e65wlytazeu688mpcvhvmd2xvfq8y" + ); + assert_eq!(bob_acc_details.internal_addresses().len(), 1); + assert_eq!( + bob_acc_details.internal_addresses()[0].address().try_to_bech32("rms")?, + "rms1qq4c9kl7vz0yssjw02w7jda56lec4ss3anfq03gwzdxzl92hcfjz7daxdfg" + ); + + let chrysalis_data = wallet.get_chrysalis_data().await?.unwrap(); + let accounts_indexation = chrysalis_data.get("iota-wallet-account-indexation").unwrap(); + assert_eq!( + accounts_indexation, + "[{\"key\":\"wallet-account://b5e020ec9a67eb7ce07be742116bd27ae722e1159098c89dd7e50d972a7b13fc\"},{\"key\":\"wallet-account://e59975e320b8433916b4946bb1e21107e8d3f36d1e587782cbd35acf59c90d1a\"}]" + ); + + // Tests if setting stronghold password still works + wallet.set_stronghold_password("password".to_owned()).await?; + // Wallet was created with mnemonic: "extra dinosaur float same hockey cheese motor divert cry misery response + // hawk gift hero pool clerk hill mask man code dragon jacket dog soup" + assert_eq!( + wallet + .generate_ed25519_address(0, 0, None) + .await? + .to_bech32(Hrp::from_str_unchecked("rms")), + "rms1qqqu7qry22f6v7d2d9aesny9vjtf56unpevkfzfudddlcq5ja9clv44sef6" + ); + + tear_down("migrate_chrysalis_db") +} + +#[tokio::test] +async fn migrate_chrysalis_db_encrypted() -> Result<()> { + iota_stronghold::engine::snapshot::try_set_encrypt_work_factor(0).unwrap(); + let storage_path = "migrate_chrysalis_db_encrypted/db"; + setup(storage_path)?; + // Copy db so the original doesn't get modified + copy_folder("./tests/wallet/fixtures/chrysalis-db-encrypted/db", storage_path).unwrap(); + std::fs::copy( + "./tests/wallet/fixtures/chrysalis-db-encrypted/wallet.stronghold", + "migrate_chrysalis_db_encrypted/wallet.stronghold", + ) + .unwrap(); + + // error on wrong password + assert!(matches!( + migrate_db_chrysalis_to_stardust( + "migrate_chrysalis_db_encrypted", + Some("wrong-password".to_string().into()), + None) + .await, + Err(iota_sdk::wallet::Error::Migration(err)) if err.contains("XCHACHA20-POLY1305") + )); + + migrate_db_chrysalis_to_stardust( + "migrate_chrysalis_db_encrypted", + Some("password".to_string().into()), + None, + ) + .await?; + + let client_options = ClientOptions::new(); + let wallet = Wallet::builder() + .with_storage_path("migrate_chrysalis_db_encrypted") + .with_client_options(client_options) + .finish() + .await?; + + let accounts = wallet.get_accounts().await?; + assert_eq!(accounts.len(), 2); + assert_eq!(accounts[0].alias().await, "Alice"); + assert_eq!(accounts[1].alias().await, "Bob"); + + let alice_acc_details = accounts[0].details().await; + assert_eq!(alice_acc_details.public_addresses().len(), 2); + assert_eq!( + alice_acc_details.public_addresses()[0].address().try_to_bech32("rms")?, + "rms1qqqu7qry22f6v7d2d9aesny9vjtf56unpevkfzfudddlcq5ja9clv44sef6" + ); + assert_eq!(alice_acc_details.internal_addresses().len(), 1); + assert_eq!( + alice_acc_details.internal_addresses()[0] + .address() + .try_to_bech32("rms")?, + "rms1qz4tac74vympq4hqqz8g9egrkhscn9743svd9xxh2w99qf5cd8vcxrmspmw" + ); + + let bob_acc_details = accounts[1].details().await; + assert_eq!(bob_acc_details.public_addresses().len(), 1); + assert_eq!( + bob_acc_details.public_addresses()[0].address().try_to_bech32("rms")?, + "rms1qql3h5vxh2sxa93yadh7f4rkr7f9g9e65wlytazeu688mpcvhvmd2xvfq8y" + ); + assert_eq!(bob_acc_details.internal_addresses().len(), 1); + assert_eq!( + bob_acc_details.internal_addresses()[0].address().try_to_bech32("rms")?, + "rms1qq4c9kl7vz0yssjw02w7jda56lec4ss3anfq03gwzdxzl92hcfjz7daxdfg" + ); + + let chrysalis_data = wallet.get_chrysalis_data().await?.unwrap(); + let accounts_indexation = chrysalis_data.get("iota-wallet-account-indexation").unwrap(); + assert_eq!( + accounts_indexation, + "[{\"key\":\"wallet-account://b5e020ec9a67eb7ce07be742116bd27ae722e1159098c89dd7e50d972a7b13fc\"},{\"key\":\"wallet-account://e59975e320b8433916b4946bb1e21107e8d3f36d1e587782cbd35acf59c90d1a\"}]" + ); + + // Tests if setting stronghold password still works + wallet.set_stronghold_password("password".to_owned()).await?; + // Wallet was created with mnemonic: "extra dinosaur float same hockey cheese motor divert cry misery response + // hawk gift hero pool clerk hill mask man code dragon jacket dog soup" + assert_eq!( + wallet + .generate_ed25519_address(0, 0, None) + .await? + .to_bech32(Hrp::from_str_unchecked("rms")), + "rms1qqqu7qry22f6v7d2d9aesny9vjtf56unpevkfzfudddlcq5ja9clv44sef6" + ); + + tear_down("migrate_chrysalis_db_encrypted") +} + +#[tokio::test] +async fn migrate_chrysalis_db_encrypted_encrypt_new() -> Result<()> { + iota_stronghold::engine::snapshot::try_set_encrypt_work_factor(0).unwrap(); + let storage_path = "migrate_chrysalis_db_encrypted_encrypt_new/db"; + setup(storage_path)?; + // Copy db so the original doesn't get modified + copy_folder("./tests/wallet/fixtures/chrysalis-db-encrypted/db", storage_path).unwrap(); + std::fs::copy( + "./tests/wallet/fixtures/chrysalis-db-encrypted/wallet.stronghold", + "migrate_chrysalis_db_encrypted_encrypt_new/wallet.stronghold", + ) + .unwrap(); + + migrate_db_chrysalis_to_stardust( + "migrate_chrysalis_db_encrypted_encrypt_new", + Some("password".to_string().into()), + Some(Zeroizing::new([0u8; 32])), + ) + .await?; + + let client_options = ClientOptions::new(); + let wallet = Wallet::builder() + .with_storage_options( + StorageOptions::new( + "migrate_chrysalis_db_encrypted_encrypt_new".into(), + StorageKind::Rocksdb, + ) + .with_encryption_key([0u8; 32]), + ) + .with_client_options(client_options) + .finish() + .await?; + + let accounts = wallet.get_accounts().await?; + assert_eq!(accounts.len(), 2); + assert_eq!(accounts[0].alias().await, "Alice"); + assert_eq!(accounts[1].alias().await, "Bob"); + + let alice_acc_details = accounts[0].details().await; + assert_eq!(alice_acc_details.public_addresses().len(), 2); + assert_eq!( + alice_acc_details.public_addresses()[0].address().try_to_bech32("rms")?, + "rms1qqqu7qry22f6v7d2d9aesny9vjtf56unpevkfzfudddlcq5ja9clv44sef6" + ); + assert_eq!(alice_acc_details.internal_addresses().len(), 1); + assert_eq!( + alice_acc_details.internal_addresses()[0] + .address() + .try_to_bech32("rms")?, + "rms1qz4tac74vympq4hqqz8g9egrkhscn9743svd9xxh2w99qf5cd8vcxrmspmw" + ); + + let bob_acc_details = accounts[1].details().await; + assert_eq!(bob_acc_details.public_addresses().len(), 1); + assert_eq!( + bob_acc_details.public_addresses()[0].address().try_to_bech32("rms")?, + "rms1qql3h5vxh2sxa93yadh7f4rkr7f9g9e65wlytazeu688mpcvhvmd2xvfq8y" + ); + assert_eq!(bob_acc_details.internal_addresses().len(), 1); + assert_eq!( + bob_acc_details.internal_addresses()[0].address().try_to_bech32("rms")?, + "rms1qq4c9kl7vz0yssjw02w7jda56lec4ss3anfq03gwzdxzl92hcfjz7daxdfg" + ); + + let chrysalis_data = wallet.get_chrysalis_data().await?.unwrap(); + let accounts_indexation = chrysalis_data.get("iota-wallet-account-indexation").unwrap(); + assert_eq!( + accounts_indexation, + "[{\"key\":\"wallet-account://b5e020ec9a67eb7ce07be742116bd27ae722e1159098c89dd7e50d972a7b13fc\"},{\"key\":\"wallet-account://e59975e320b8433916b4946bb1e21107e8d3f36d1e587782cbd35acf59c90d1a\"}]" + ); + + // Tests if setting stronghold password still works + wallet.set_stronghold_password("password".to_owned()).await?; + // Wallet was created with mnemonic: "extra dinosaur float same hockey cheese motor divert cry misery response + // hawk gift hero pool clerk hill mask man code dragon jacket dog soup" + assert_eq!( + wallet + .generate_ed25519_address(0, 0, None) + .await? + .to_bech32(Hrp::from_str_unchecked("rms")), + "rms1qqqu7qry22f6v7d2d9aesny9vjtf56unpevkfzfudddlcq5ja9clv44sef6" + ); + + tear_down("migrate_chrysalis_db_encrypted_encrypt_new") +} + +#[tokio::test] +async fn migrate_chrysalis_stronghold() -> Result<()> { + iota_stronghold::engine::snapshot::try_set_encrypt_work_factor(0).unwrap(); + let storage_path = "migrate_chrysalis_stronghold"; + setup(storage_path)?; + + let client_options = ClientOptions::new(); + let wallet = Wallet::builder() + .with_storage_path(storage_path) + .with_coin_type(IOTA_COIN_TYPE) + .with_client_options(client_options) + .with_secret_manager(SecretManager::Placeholder) + .finish() + .await?; + + wallet + .restore_backup( + "./tests/wallet/fixtures/chrysalis-backup-work-factor-0.stronghold".into(), + Password::from("password".to_string()), + None, + None, + ) + .await?; + + let accounts = wallet.get_accounts().await?; + assert_eq!(accounts.len(), 2); + assert_eq!(accounts[0].alias().await, "Alice"); + assert_eq!(accounts[1].alias().await, "Bob"); + + let alice_acc_details = accounts[0].details().await; + assert_eq!(alice_acc_details.public_addresses().len(), 2); + assert_eq!( + alice_acc_details.public_addresses()[0].address().try_to_bech32("rms")?, + "rms1qqqu7qry22f6v7d2d9aesny9vjtf56unpevkfzfudddlcq5ja9clv44sef6" + ); + assert_eq!(alice_acc_details.internal_addresses().len(), 1); + assert_eq!( + alice_acc_details.internal_addresses()[0] + .address() + .try_to_bech32("rms")?, + "rms1qz4tac74vympq4hqqz8g9egrkhscn9743svd9xxh2w99qf5cd8vcxrmspmw" + ); + + let bob_acc_details = accounts[1].details().await; + assert_eq!(bob_acc_details.public_addresses().len(), 1); + assert_eq!( + bob_acc_details.public_addresses()[0].address().try_to_bech32("rms")?, + "rms1qql3h5vxh2sxa93yadh7f4rkr7f9g9e65wlytazeu688mpcvhvmd2xvfq8y" + ); + assert_eq!(bob_acc_details.internal_addresses().len(), 1); + assert_eq!( + bob_acc_details.internal_addresses()[0].address().try_to_bech32("rms")?, + "rms1qq4c9kl7vz0yssjw02w7jda56lec4ss3anfq03gwzdxzl92hcfjz7daxdfg" + ); + + let chrysalis_data = wallet.get_chrysalis_data().await?.unwrap(); + // "iota-wallet-account-indexation" + let accounts_indexation = chrysalis_data + .get("0xddc058ad3b93b5a575b0051aafbc8ff17ad0415d7aa1c54d") + .unwrap(); + assert_eq!( + accounts_indexation, + "[{\"key\":\"wallet-account://b5e020ec9a67eb7ce07be742116bd27ae722e1159098c89dd7e50d972a7b13fc\"},{\"key\":\"wallet-account://e59975e320b8433916b4946bb1e21107e8d3f36d1e587782cbd35acf59c90d1a\"}]" + ); + + // Tests if setting stronghold password still works, commented because age encryption is very slow in CI + wallet.set_stronghold_password("password".to_owned()).await?; + // Wallet was created with mnemonic: "extra dinosaur float same hockey cheese motor divert cry misery response + // hawk gift hero pool clerk hill mask man code dragon jacket dog soup" + assert_eq!( + wallet + .generate_ed25519_address(0, 0, None) + .await? + .to_bech32(iota_sdk::types::block::address::Hrp::from_str_unchecked("rms")), + "rms1qqqu7qry22f6v7d2d9aesny9vjtf56unpevkfzfudddlcq5ja9clv44sef6" + ); + + tear_down(storage_path) +} + +fn copy_folder(src: impl AsRef, dest: impl AsRef) -> io::Result<()> { + fs::create_dir_all(&dest)?; + for entry in fs::read_dir(src)? { + let entry = entry?; + fs::copy(entry.path(), dest.as_ref().join(entry.file_name()))?; + } + Ok(()) +} diff --git a/sdk/tests/wallet/fixtures/chrysalis-backup-work-factor-0.stronghold b/sdk/tests/wallet/fixtures/chrysalis-backup-work-factor-0.stronghold new file mode 100644 index 0000000000..853b8c4f83 Binary files /dev/null and b/sdk/tests/wallet/fixtures/chrysalis-backup-work-factor-0.stronghold differ diff --git a/sdk/tests/wallet/fixtures/chrysalis-db-encrypted/db/000097.sst b/sdk/tests/wallet/fixtures/chrysalis-db-encrypted/db/000097.sst new file mode 100644 index 0000000000..d23a3e86bb Binary files /dev/null and b/sdk/tests/wallet/fixtures/chrysalis-db-encrypted/db/000097.sst differ diff --git a/sdk/tests/wallet/fixtures/chrysalis-db-encrypted/db/000100.sst b/sdk/tests/wallet/fixtures/chrysalis-db-encrypted/db/000100.sst new file mode 100644 index 0000000000..995cc33cb3 Binary files /dev/null and b/sdk/tests/wallet/fixtures/chrysalis-db-encrypted/db/000100.sst differ diff --git a/sdk/tests/wallet/fixtures/chrysalis-db-encrypted/db/000105.sst b/sdk/tests/wallet/fixtures/chrysalis-db-encrypted/db/000105.sst new file mode 100644 index 0000000000..5f62034841 Binary files /dev/null and b/sdk/tests/wallet/fixtures/chrysalis-db-encrypted/db/000105.sst differ diff --git a/sdk/tests/wallet/fixtures/chrysalis-db-encrypted/db/000110.sst b/sdk/tests/wallet/fixtures/chrysalis-db-encrypted/db/000110.sst new file mode 100644 index 0000000000..12ed25b2ed Binary files /dev/null and b/sdk/tests/wallet/fixtures/chrysalis-db-encrypted/db/000110.sst differ diff --git a/sdk/tests/wallet/fixtures/chrysalis-db-encrypted/db/CURRENT b/sdk/tests/wallet/fixtures/chrysalis-db-encrypted/db/CURRENT new file mode 100644 index 0000000000..b59a6ba248 --- /dev/null +++ b/sdk/tests/wallet/fixtures/chrysalis-db-encrypted/db/CURRENT @@ -0,0 +1 @@ +MANIFEST-000112 diff --git a/sdk/tests/wallet/fixtures/chrysalis-db-encrypted/db/IDENTITY b/sdk/tests/wallet/fixtures/chrysalis-db-encrypted/db/IDENTITY new file mode 100644 index 0000000000..8c632075e5 --- /dev/null +++ b/sdk/tests/wallet/fixtures/chrysalis-db-encrypted/db/IDENTITY @@ -0,0 +1 @@ +42953950-2698-4d2b-b2a1-9e405c72280e \ No newline at end of file diff --git a/sdk/tests/wallet/fixtures/chrysalis-db-encrypted/db/MANIFEST-000112 b/sdk/tests/wallet/fixtures/chrysalis-db-encrypted/db/MANIFEST-000112 new file mode 100644 index 0000000000..3eee97e456 Binary files /dev/null and b/sdk/tests/wallet/fixtures/chrysalis-db-encrypted/db/MANIFEST-000112 differ diff --git a/sdk/tests/wallet/fixtures/chrysalis-db-encrypted/wallet.stronghold b/sdk/tests/wallet/fixtures/chrysalis-db-encrypted/wallet.stronghold new file mode 100644 index 0000000000..853b8c4f83 Binary files /dev/null and b/sdk/tests/wallet/fixtures/chrysalis-db-encrypted/wallet.stronghold differ diff --git a/sdk/tests/wallet/fixtures/chrysalis-db/db/000051.sst b/sdk/tests/wallet/fixtures/chrysalis-db/db/000051.sst new file mode 100644 index 0000000000..c857084023 Binary files /dev/null and b/sdk/tests/wallet/fixtures/chrysalis-db/db/000051.sst differ diff --git a/sdk/tests/wallet/fixtures/chrysalis-db/db/000054.sst b/sdk/tests/wallet/fixtures/chrysalis-db/db/000054.sst new file mode 100644 index 0000000000..1cab214b18 Binary files /dev/null and b/sdk/tests/wallet/fixtures/chrysalis-db/db/000054.sst differ diff --git a/sdk/tests/wallet/fixtures/chrysalis-db/db/CURRENT b/sdk/tests/wallet/fixtures/chrysalis-db/db/CURRENT new file mode 100644 index 0000000000..80d9de0bbd --- /dev/null +++ b/sdk/tests/wallet/fixtures/chrysalis-db/db/CURRENT @@ -0,0 +1 @@ +MANIFEST-000056 diff --git a/sdk/tests/wallet/fixtures/chrysalis-db/db/MANIFEST-000056 b/sdk/tests/wallet/fixtures/chrysalis-db/db/MANIFEST-000056 new file mode 100644 index 0000000000..5b32996545 Binary files /dev/null and b/sdk/tests/wallet/fixtures/chrysalis-db/db/MANIFEST-000056 differ diff --git a/sdk/tests/wallet/fixtures/chrysalis-db/wallet.stronghold b/sdk/tests/wallet/fixtures/chrysalis-db/wallet.stronghold new file mode 100644 index 0000000000..853b8c4f83 Binary files /dev/null and b/sdk/tests/wallet/fixtures/chrysalis-db/wallet.stronghold differ diff --git a/sdk/tests/wallet/mod.rs b/sdk/tests/wallet/mod.rs index 1e08211d68..68693dcd05 100644 --- a/sdk/tests/wallet/mod.rs +++ b/sdk/tests/wallet/mod.rs @@ -9,6 +9,9 @@ mod backup_restore; mod balance; mod bech32_hrp_validation; mod burn_outputs; +#[cfg(not(target_os = "windows"))] +#[cfg(all(feature = "stronghold", feature = "storage"))] +mod chrysalis_migration; mod claim_outputs; mod common; mod consolidation;