From ce77da17379858c4ea2c3dcf9d6d06c48811d0d3 Mon Sep 17 00:00:00 2001 From: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> Date: Wed, 30 Aug 2023 16:08:29 +0200 Subject: [PATCH 01/14] Storage migration (#1057) * Brainstorm * Basic idea * Add encrypted db * Seems to work * Define types in migration, refactor * where in stronghold is the data? * Make stronghold snapshot migration work * Add migration to bindings, add get_chrysalis_data() * Save chrysalis data from snapshot in db * Address some TODOs * Address todos * Add chrysalis-backup-work-factor-0.stronghold * Fix wasm * Don't error on ClientDataNotPresent * Clippy * Address review comments * Remove extra function * Update bindings/nodejs/CHANGELOG.md Co-authored-by: Thibault Martinez * Remove `from` * Clippy * Changelog * Update sdk/src/wallet/storage/constants.rs * Move function to storage * Address review comments * Address review comments * Review suggestions * Address review comments * Add db_encryption_key to Rust, export migrateDbChrysalisToStardust and add nodejs example * Single import, lint * Address review comments --------- Co-authored-by: Thibault Martinez Co-authored-by: Thibault Martinez --- bindings/core/src/method/wallet.rs | 5 + bindings/core/src/method_handler/wallet.rs | 1 + bindings/core/src/response.rs | 5 +- bindings/nodejs/CHANGELOG.md | 5 + .../migrate-db-chrysalis-to-stardust.ts | 53 +++ bindings/nodejs/lib/bindings.ts | 2 + .../nodejs/lib/types/wallet/bridge/index.ts | 2 + .../nodejs/lib/types/wallet/bridge/wallet.ts | 4 + bindings/nodejs/lib/wallet/index.ts | 1 + bindings/nodejs/lib/wallet/wallet.ts | 11 + bindings/nodejs/src/lib.rs | 1 + bindings/nodejs/src/wallet.rs | 25 ++ bindings/wasm/lib/bindings.ts | 3 +- bindings/wasm/src/wallet.rs | 11 + sdk/CHANGELOG.md | 5 + sdk/src/client/stronghold/common.rs | 2 +- sdk/src/client/stronghold/mod.rs | 2 +- sdk/src/wallet/core/operations/storage.rs | 12 +- .../core/operations/stronghold_backup/mod.rs | 21 +- .../stronghold_backup/stronghold_snapshot.rs | 136 ++++++- sdk/src/wallet/migration/chrysalis.rs | 315 +++++++++++++++ sdk/src/wallet/migration/mod.rs | 6 +- sdk/src/wallet/storage/adapter/rocksdb.rs | 4 +- sdk/src/wallet/storage/constants.rs | 2 + sdk/src/wallet/storage/mod.rs | 8 +- sdk/tests/wallet/chrysalis_migration.rs | 358 ++++++++++++++++++ .../chrysalis-backup-work-factor-0.stronghold | Bin 0 -> 4541 bytes .../chrysalis-db-encrypted/db/000097.sst | Bin 0 -> 5365 bytes .../chrysalis-db-encrypted/db/000100.sst | Bin 0 -> 2701 bytes .../chrysalis-db-encrypted/db/000105.sst | Bin 0 -> 2691 bytes .../chrysalis-db-encrypted/db/000110.sst | Bin 0 -> 20128 bytes .../chrysalis-db-encrypted/db/CURRENT | 1 + .../chrysalis-db-encrypted/db/IDENTITY | 1 + .../chrysalis-db-encrypted/db/MANIFEST-000112 | Bin 0 -> 927 bytes .../chrysalis-db-encrypted/wallet.stronghold | Bin 0 -> 4541 bytes .../fixtures/chrysalis-db/db/000051.sst | Bin 0 -> 5243 bytes .../fixtures/chrysalis-db/db/000054.sst | Bin 0 -> 2706 bytes .../wallet/fixtures/chrysalis-db/db/CURRENT | 1 + .../fixtures/chrysalis-db/db/MANIFEST-000056 | Bin 0 -> 516 bytes .../fixtures/chrysalis-db/wallet.stronghold | Bin 0 -> 4541 bytes sdk/tests/wallet/mod.rs | 3 + 41 files changed, 982 insertions(+), 24 deletions(-) create mode 100644 bindings/nodejs/examples/wallet/migrate-db-chrysalis-to-stardust.ts create mode 100644 sdk/src/wallet/migration/chrysalis.rs create mode 100644 sdk/tests/wallet/chrysalis_migration.rs create mode 100644 sdk/tests/wallet/fixtures/chrysalis-backup-work-factor-0.stronghold create mode 100644 sdk/tests/wallet/fixtures/chrysalis-db-encrypted/db/000097.sst create mode 100644 sdk/tests/wallet/fixtures/chrysalis-db-encrypted/db/000100.sst create mode 100644 sdk/tests/wallet/fixtures/chrysalis-db-encrypted/db/000105.sst create mode 100644 sdk/tests/wallet/fixtures/chrysalis-db-encrypted/db/000110.sst create mode 100644 sdk/tests/wallet/fixtures/chrysalis-db-encrypted/db/CURRENT create mode 100644 sdk/tests/wallet/fixtures/chrysalis-db-encrypted/db/IDENTITY create mode 100644 sdk/tests/wallet/fixtures/chrysalis-db-encrypted/db/MANIFEST-000112 create mode 100644 sdk/tests/wallet/fixtures/chrysalis-db-encrypted/wallet.stronghold create mode 100644 sdk/tests/wallet/fixtures/chrysalis-db/db/000051.sst create mode 100644 sdk/tests/wallet/fixtures/chrysalis-db/db/000054.sst create mode 100644 sdk/tests/wallet/fixtures/chrysalis-db/db/CURRENT create mode 100644 sdk/tests/wallet/fixtures/chrysalis-db/db/MANIFEST-000056 create mode 100644 sdk/tests/wallet/fixtures/chrysalis-db/wallet.stronghold 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/CHANGELOG.md b/bindings/nodejs/CHANGELOG.md index a14b72f45b..6c23fa0530 100644 --- a/bindings/nodejs/CHANGELOG.md +++ b/bindings/nodejs/CHANGELOG.md @@ -27,6 +27,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## 1.0.6 - 2023-08-25 +### Added + +- `migrateDbChrysalisToStardust` function; +- `Wallet::getChrysalisData` method; + ### Fixed - `Account::prepareBurn()` return type; 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/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/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/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 adc85c6f02..2c08b5ade5 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, @@ -232,3 +233,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/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/sdk/CHANGELOG.md b/sdk/CHANGELOG.md index 1b80ee77f0..9523b58f98 100644 --- a/sdk/CHANGELOG.md +++ b/sdk/CHANGELOG.md @@ -21,6 +21,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## 1.0.3 - 2023-MM-DD +### Added + +- `migrate_db_chrysalis_to_stardust()` function; +- `Wallet::get_chrysalis_data()` method; + ### Fixed - `Clients` returning the default protocol parameters when multiple `Client` instances are used; 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 9e4d3f02bf..19d9b269b8 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/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/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 0000000000000000000000000000000000000000..853b8c4f83e040824ab97ea6286c98a41a1633c1 GIT binary patch literal 4541 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 b7Za7&Sw@o^p@3LZkmrEADo_1T|5x{AkBhFa literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d23a3e86bb607d6ff3b084ac7a6f4a0be153dc6e GIT binary patch literal 5365 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5f62034841712035b2c35ac0b72d2aa2a1ec9c8e GIT binary patch literal 2691 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 b7Za7&Sw@o^p@3LZkmrEADo_1T|5x{AkBhFa literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c8570840232c8d0a5442c7081423137c1e18fbcf GIT binary patch literal 5243 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 au{VzPT-*QstD*bdAKrNB&maD$&;Jk6g!1VC literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1cab214b18aec312fd15e203da4a15aee8009ec0 GIT binary patch literal 2706 zcmbtWU5^`A89p;!d!1x=on+g9q5|mJ2xH zWJ`X}yx-6B9N#*8{}B4rZMJ1XB{2R>-s) zYhOW)e|y65ec}iU8-ZtAmJjZ>Z@WQY3W#M8;c+XpTy6@-BgDfj;Fd#K==jVxxJk7e z*O!9d)&7cFZz1EC?L-s>?TJE>A6FWmP*=k`6cTeBVCg11fE_f5~orf1$o`z}sjHLc z)b4fk<=6Llwa;iu*+mz&wTSwqt0VO~Fq~uwesRZNqCD)@9S|*j~r7b=NR#*RpK?xPd!{ z0Uj|7aATS>2cdteP6rzuhv?YD(5vTr4tkGro&y8~q`9`ef_6?rXezVF9F0aL8Rb)q zLw5{o<~|igI`zk!GIZQB9g6WFoP{OlJZ2+jllm+kTjO*_V#piNn5Iy3$A7-|67b;c zeYE_o+U`bH%3&!X*|sLY(JwfWx+jxhs4azv0<{ zdM|`EVFRA|1aq1elvT_-3rqhJ(k&?9PEEOhTJO}1MMb@uR1DRXGAcHP0FYnQT5l+w z{~R0uXQABsdRCe zSVL0w#+xI13mWJd`FJEtHzCuRKO0%Hz?hK)TZt%dqDHG-SDLQ@Sgr#O;t1NfFsypM zrJhMMF7DL-1jNgugyz$W+0`!`LKc{>)OOa&97v}1r5qL@Lv$GFanLsOzRvpjv;c?| zZ7#;Cka{I+I-{XjhXK-3_8Iynj|v*Zf-k8c9Iv9C#>Fg)ujFxs7pMf>CRskP(T|gn zlxOm6Ao5?MdlmBld>Nzh#cYx*J4#h!1;n!z?}5CZMhhQBpL{9(8d7g5=PKt_bT1d9 zQWWxXln4OIDda%42hsbDZP$dV540NxUVhLx)o8CPTX*XF zTCbvZ>rVafA@ffCvuCp4c75enj_!JnXP5+=x9Y9i^@F8l5Mp`6LAB!xwQs1k71X$0 z-#r#veP;~2EVig`O)2k_(9Q=r34PeP>rCRQq%%>vo|g>SxIa!fp4m7lhA-9{tv^EY zcWUbH@sZ8^K}^OoV_FoO69Z4kCZ~=Y3uYIEMbmI(SiQ-NZ_Q#K_gT0(BjMB^5dIok zRo_%wzpw4`uOoOA_$>7PzWg(E>BF}TEb{-o2hh&a;}pmc3lGrIg&go-@IVh>aRWV| zh2XlZwy)1GH(i2eRX&IJE5lrb(Uu-YF{~2^V;o*pCZJzJ2eLmko6Y?S%i=Og^^hh} zJk=wP4lOLE6jWSp^def7}i6aT!>hg%mZUE7X$}%e? z?^b4bD@vc*MLI|Phn>nW6#3J4%0hJK{_~LY03ttWo;b2+^=Kv1FcKX)i8$~yl2aS| zj^#T>8!kbwZF3y715C}fFDk`@o8L3Uf#pU&D}aTMe1_SuofQZ6KLg@ib^hew%EtMN z*12<+$O|uD#aF)g;w$Fo&aN!h;SCAIdGFmff6{2~Q8@)N7tu_7u(W5NBH*tIY!jAK zpyod1;oj!QXzz-$_3%iKMcR{nv}ak#@`Or#Eb=NdYCfwxL_oFNsL-MNyo#l)EGSh9 zwsJ_BU&z%V7cpcOaGisyE6o)p5qVGO*-(H`^8&5b7Gc+wLJVczY$<7(=<{Oduq%|M zMFobIN1~`Ntvsr<`^bWpS_(wz36L!42x^0NS_<5oARBY&RU<%qR+T*JMez5cf}+qN zMIdsw`Rw8&+=?w&8G|zP3W<+u*?T~(ij-3wAPZ!LRYv?z3RhW ztDmobqY{?(p>6U7#2c)@RVAgf8$3JbnpqahWR`NH20U~+l?m#eD}A<|Ge<_ bGtYmp^wrAtzn?lBzyF7wn{S`~+qeG<0zORs literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5b3299654551213cda4f42874feb921fdb9fce1c GIT binary patch literal 516 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 b7Za7&Sw@o^p@3LZkmrEADo_1T|5x{AkBhFa literal 0 HcmV?d00001 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; From 711f85c241abb93a83ed43e169da42706bfc1940 Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Thu, 31 Aug 2023 12:16:58 +0200 Subject: [PATCH 02/14] Fix readmes (#1110) --- README.md | 2 +- cli/README.md | 16 ++-------------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index f99bb41652..2a556c463d 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ 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 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 From a8b53902905912dbd7675b584cb7f60312c05396 Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Thu, 31 Aug 2023 12:27:12 +0200 Subject: [PATCH 03/14] Add PrivateKeySecretManager (#937) * Add PrivateKeySecretManager * try_from_b58/try_from_hex * zeroize bytes * Nits * Optional bs58 * Changelog * Secret manager tests folder * Add tests and fix * Changelog * Nit * Nit * Update sdk/tests/client/secret_manager/mod.rs Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> * Replace panic with errors * Remove unwrap * Under private_key_secret_manager feature --------- Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> --- Cargo.lock | 11 +- sdk/CHANGELOG.md | 2 + sdk/Cargo.toml | 2 + sdk/src/client/secret/mod.rs | 89 +++++++++++- sdk/src/client/secret/private_key.rs | 132 ++++++++++++++++++ .../core/operations/address_generation.rs | 11 ++ sdk/tests/client/secret_manager/mnemonic.rs | 29 ++++ sdk/tests/client/secret_manager/mod.rs | 8 ++ .../client/secret_manager/private_key.rs | 84 +++++++++++ .../stronghold.rs} | 31 +--- 10 files changed, 367 insertions(+), 32 deletions(-) create mode 100644 sdk/src/client/secret/private_key.rs create mode 100644 sdk/tests/client/secret_manager/mnemonic.rs create mode 100644 sdk/tests/client/secret_manager/mod.rs create mode 100644 sdk/tests/client/secret_manager/private_key.rs rename sdk/tests/client/{secret_manager.rs => secret_manager/stronghold.rs} (77%) diff --git a/Cargo.lock b/Cargo.lock index 21332b840a..1808274fc6 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", ] @@ -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" @@ -1633,6 +1639,7 @@ dependencies = [ "async-trait", "bech32 0.9.1", "bitflags 2.4.0", + "bs58", "bytemuck", "derive_builder", "derive_more", diff --git a/sdk/CHANGELOG.md b/sdk/CHANGELOG.md index 9523b58f98..6f6a2d0eff 100644 --- a/sdk/CHANGELOG.md +++ b/sdk/CHANGELOG.md @@ -25,6 +25,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `migrate_db_chrysalis_to_stardust()` function; - `Wallet::get_chrysalis_data()` method; +- `PrivateKeySecretManager` and `SecretManager::PrivateKey`; +- `SecretManager::from` impl for variants; ### Fixed diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index 0e9f3a34e5..791550ea69 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -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 } derive_builder = { version = "0.12.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 = [ @@ -185,6 +186,7 @@ stronghold = [ "dep:heck", ] tls = ["reqwest?/rustls-tls", "rumqttc?/use-rustls"] +private_key_secret_manager = ["bs58"] client = [ "pow", 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/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/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(); From f16a283b33b36a3099a9facff575598a9ce8d6ae Mon Sep 17 00:00:00 2001 From: Alexandcoats Date: Fri, 1 Sep 2023 03:29:07 -0400 Subject: [PATCH 04/14] Use `ci-test` alias (#1112) * use ci-test alias * Add more aliases * fix coverage * try renaming coverage file * allow pow tests to take longer * adjustment --- .cargo/config.toml | 2 ++ .config/nextest.toml | 4 ++++ .github/workflows/build-and-test.yml | 4 +++- .github/workflows/coverage.yml | 13 ++----------- .github/workflows/private-tangle-tests.yml | 4 +++- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index add62220f6..399881c7e3 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" 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/build-and-test.yml b/.github/workflows/build-and-test.yml index f7bc3ccc45..64e934f4b0 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -4,6 +4,7 @@ on: push: branches: [develop, production] 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] 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/coverage.yml b/.github/workflows/coverage.yml index e691992458..b60ee9b935 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -4,6 +4,7 @@ on: push: branches: [develop, production] 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 7b0eec51a3..5db264192b 100644 --- a/.github/workflows/private-tangle-tests.yml +++ b/.github/workflows/private-tangle-tests.yml @@ -4,6 +4,7 @@ on: push: branches: [develop, production] paths: + - ".cargo/config.toml" - ".github/workflows/private-tangle-tests.yml" - ".github/actions/**" - "**.rs" @@ -15,6 +16,7 @@ on: pull_request: branches: [develop, production] paths: + - ".cargo/config.toml" - ".github/workflows/private-tangle-tests.yml" - ".github/actions/**" - "**.rs" @@ -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() From b1c1c13e33e8d90f27d9cc81b62c8d6210aa658b Mon Sep 17 00:00:00 2001 From: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> Date: Fri, 1 Sep 2023 11:41:16 +0200 Subject: [PATCH 05/14] Update readme examples (#1114) --- README.md | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 2a556c463d..b9ed9063cc 100644 --- a/README.md +++ b/README.md @@ -124,9 +124,7 @@ calling [`Client.get_info()`](https://docs.rs/iota-sdk/latest/iota_sdk/client/co and then print the node's information. ```rust -use iota_sdk::client::{ - Client, -}; +use iota_sdk::client::{Client, Result}; #[tokio::main] async fn main() -> Result<()> { @@ -134,10 +132,10 @@ async fn main() -> Result<()> { .with_node("https://api.testnet.shimmer.network")? // Insert your node URL here .finish() .await?; - + let info = client.get_info().await?; - println!("Node Info: {info:?}") - + println!("Node Info: {info:?}"); + Ok(()) } ``` @@ -148,7 +146,7 @@ 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. +to store a mnemonic. For this `features = ["stronghold"]` is needed in the Cargo.toml import. To persist the wallet in a database, `"rocksdb"` can be added. ```rust use iota_sdk::{ @@ -158,15 +156,14 @@ use iota_sdk::{ }, 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. + .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("https://api.testnet.shimmer.network")?; @@ -190,7 +187,6 @@ async fn main() -> Result<()> { .finish() .await?; - Ok(()) } ``` From cd78e6ea46df430c8f7cee255a1cd1eb709b2778 Mon Sep 17 00:00:00 2001 From: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> Date: Tue, 5 Sep 2023 11:57:28 +0200 Subject: [PATCH 06/14] Fix changelog and bump version (#1131) --- bindings/nodejs/CHANGELOG.md | 12 +++++++----- bindings/nodejs/package.json | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/bindings/nodejs/CHANGELOG.md b/bindings/nodejs/CHANGELOG.md index 6c23fa0530..ee0e431029 100644 --- a/bindings/nodejs/CHANGELOG.md +++ b/bindings/nodejs/CHANGELOG.md @@ -19,6 +19,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security --> +## 1.0.8 - 2023-09-05 + +### Added + +- `migrateDbChrysalisToStardust` function; +- `Wallet::getChrysalisData` method; + ## 1.0.7 - 2023-08-29 ### Fixed @@ -27,11 +34,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## 1.0.6 - 2023-08-25 -### Added - -- `migrateDbChrysalisToStardust` function; -- `Wallet::getChrysalisData` method; - ### Fixed - `Account::prepareBurn()` return type; diff --git a/bindings/nodejs/package.json b/bindings/nodejs/package.json index 34c546be14..e70cb1eddf 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.8", "description": "Node.js binding to the IOTA SDK library", "main": "out/index.js", "types": "out/index.d.ts", From 9f4fbbf01f2a1a1efe1b68ff02d77682b2569d70 Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Wed, 6 Sep 2023 11:28:54 +0200 Subject: [PATCH 07/14] CI: switch all rust toolchains to setup-rust (#1149) --- .github/workflows/bindings-nodejs-publish.yml | 4 ++-- .github/workflows/bindings-wallet-covector-publish.yml | 4 ++-- .github/workflows/bindings-wasm-publish.yml | 4 ++-- .github/workflows/cli-publish.yml | 5 +++-- .github/workflows/private-tangle-tests.yml | 4 ++-- 5 files changed, 11 insertions(+), 10 deletions(-) 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/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/private-tangle-tests.yml b/.github/workflows/private-tangle-tests.yml index 5db264192b..678a3a683e 100644 --- a/.github/workflows/private-tangle-tests.yml +++ b/.github/workflows/private-tangle-tests.yml @@ -44,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: | From 007c41755b54bebb1d1870e0cf3a9d4bc410527b Mon Sep 17 00:00:00 2001 From: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> Date: Wed, 6 Sep 2023 11:38:01 +0200 Subject: [PATCH 08/14] Update 'getting started' examples and link to them (#1130) * Update getting started started examples and link to them * Update wallet getting started * snake_case * Update sdk/examples/client/getting_started.rs Co-authored-by: Thibault Martinez * consistency * Fix comment * Clean readmes --------- Co-authored-by: Thibault Martinez Co-authored-by: Thibault Martinez --- README.md | 76 ++----------------- .../nodejs-old/examples/getting-started.js | 3 +- .../nodejs/examples/client/getting-started.ts | 23 ++++++ .../nodejs/examples/wallet/getting-started.ts | 60 +++++++++++++++ bindings/python/README.md | 44 +---------- .../python/examples/client/getting_started.py | 8 ++ ...{getting-started.py => getting_started.py} | 3 +- sdk/Cargo.toml | 5 ++ sdk/examples/client/getting_started.rs | 23 ++++++ sdk/examples/wallet/getting_started.rs | 47 +++++------- 10 files changed, 152 insertions(+), 140 deletions(-) create mode 100644 bindings/nodejs/examples/client/getting-started.ts create mode 100644 bindings/nodejs/examples/wallet/getting-started.ts create mode 100644 bindings/python/examples/client/getting_started.py rename bindings/python/examples/wallet/{getting-started.py => getting_started.py} (93%) create mode 100644 sdk/examples/client/getting_started.rs diff --git a/README.md b/README.md index b9ed9063cc..c0df14bca9 100644 --- a/README.md +++ b/README.md @@ -117,79 +117,15 @@ 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, 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(()) -} -``` +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. For this `features = ["stronghold"]` is needed in the Cargo.toml import. To persist the wallet in a database, `"rocksdb"` can be added. - -```rust -use iota_sdk::{ - client::{ - constants::SHIMMER_COIN_TYPE, - secret::{stronghold::StrongholdSecretManager, SecretManager}, - }, - wallet::{ClientOptions, Result, Wallet}, -}; - -#[tokio::main] -async fn main() -> Result<()> { - // Setup Stronghold secret manager. - // WARNING: Never hardcode passwords in production code. - let secret_manager = StrongholdSecretManager::builder() - .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("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/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/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/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 1bdd4e056d..92dcc06ac3 100644 --- a/bindings/python/examples/wallet/getting-started.py +++ b/bindings/python/examples/wallet/getting_started.py @@ -34,9 +34,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/sdk/Cargo.toml b/sdk/Cargo.toml index 791550ea69..20af48245b 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -624,6 +624,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(()) } From a877f283d587bbb97fddac9d5363e3b789a65bd1 Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab <31316147+abdulmth@users.noreply.github.com> Date: Wed, 6 Sep 2023 18:23:59 +0200 Subject: [PATCH 09/14] Fix Blocking behavior in Node.js bindings (#1156) * remove mpsc channel * changelog * Update bindings/nodejs/src/client.rs * remove test --------- Co-authored-by: Thibault Martinez --- bindings/nodejs/CHANGELOG.md | 6 +++ bindings/nodejs/src/client.rs | 11 +---- bindings/nodejs/src/wallet.rs | 11 +---- bindings/nodejs/tests/wallet/wallet.spec.ts | 47 --------------------- 4 files changed, 8 insertions(+), 67 deletions(-) diff --git a/bindings/nodejs/CHANGELOG.md b/bindings/nodejs/CHANGELOG.md index ee0e431029..990d60264f 100644 --- a/bindings/nodejs/CHANGELOG.md +++ b/bindings/nodejs/CHANGELOG.md @@ -19,6 +19,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security --> +## 1.0.9 - 2023-09-06 + +### Fixed + +- The main thread gets blocked when calling client or wallet methods; + ## 1.0.8 - 2023-09-05 ### Added diff --git a/bindings/nodejs/src/client.rs b/bindings/nodejs/src/client.rs index 643fead784..09fa6abc5b 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/wallet.rs b/bindings/nodejs/src/wallet.rs index 2c08b5ade5..42a4cbbfb7 100644 --- a/bindings/nodejs/src/wallet.rs +++ b/bindings/nodejs/src/wallet.rs @@ -103,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; @@ -125,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()) } 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) { From a3260a4ed7073167b20376077b2b90ecefa5b4e9 Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Thu, 7 Sep 2023 09:47:51 +0200 Subject: [PATCH 10/14] Remove ignore RUSTSEC-2023-0052 (#1159) --- .cargo/config.toml | 4 +- Cargo.lock | 109 ++++++++++++++++++------------------- bindings/core/Cargo.toml | 8 +-- bindings/nodejs/Cargo.toml | 2 +- bindings/python/Cargo.toml | 2 +- bindings/wasm/Cargo.toml | 2 +- cli/Cargo.toml | 8 +-- sdk/Cargo.toml | 20 +++---- 8 files changed, 76 insertions(+), 79 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 399881c7e3..1a9c1dcd47 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -23,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/Cargo.lock b/Cargo.lock index 1808274fc6..bca1ce9962 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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]] @@ -358,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" @@ -436,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", ] @@ -467,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", @@ -490,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]] @@ -637,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", @@ -704,7 +703,7 @@ checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.31", ] [[package]] @@ -1178,7 +1177,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.31", ] [[package]] @@ -1995,9 +1994,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" @@ -2157,9 +2156,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", ] @@ -2209,9 +2208,9 @@ dependencies = [ [[package]] name = "parity-scale-codec" -version = "3.6.5" +version = "3.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dec8a8073036902368c2cdc0387e85ff9a37054d7e7c98e592145e0c92cd4fb" +checksum = "1b4a26fb934017f2e774ad9a16b40cca8faec288e0233496c6a47f266d49f024" dependencies = [ "arrayvec", "bitvec", @@ -2223,9 +2222,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", @@ -2320,7 +2319,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.31", ] [[package]] @@ -2399,12 +2398,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]] @@ -2604,9 +2603,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", @@ -2616,9 +2615,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", @@ -2767,9 +2766,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", @@ -2967,7 +2966,7 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.31", ] [[package]] @@ -2989,7 +2988,7 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.31", ] [[package]] @@ -3044,9 +3043,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" @@ -3229,9 +3228,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", @@ -3263,22 +3262,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]] @@ -3369,7 +3368,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.31", ] [[package]] @@ -3582,9 +3581,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", @@ -3628,7 +3627,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.31", "wasm-bindgen-shared", ] @@ -3662,7 +3661,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.31", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3696,9 +3695,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", @@ -3993,5 +3992,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.31", ] 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/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/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/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/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/sdk/Cargo.toml b/sdk/Cargo.toml index 20af48245b..b9bf3bcdd5 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -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", ] } @@ -74,10 +74,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 = [ @@ -87,12 +87,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 = [ @@ -100,7 +100,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", @@ -116,7 +116,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", @@ -129,7 +129,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", From 7d89459ac61b3cd57cf23246c0b3fed5d96d3373 Mon Sep 17 00:00:00 2001 From: Alexandcoats Date: Thu, 7 Sep 2023 05:23:24 -0400 Subject: [PATCH 11/14] Add a request pool (#1137) * initial changes * fix wasm * add rand dep to client * Rewrite to be a much simpler request pool * cleanup * Add builder fn and changelog * timeout cleanup * fix wasm * refactor * add config to bindings * changelogs * Update sdk/CHANGELOG.md * Update release date * SDK release date --------- Co-authored-by: Thibault Martinez --- bindings/nodejs/CHANGELOG.md | 6 +- .../nodejs/lib/types/client/client-options.ts | 2 + bindings/nodejs/package.json | 2 +- bindings/python/CHANGELOG.md | 6 + .../python/iota_sdk/types/client_options.py | 3 + sdk/CHANGELOG.md | 3 +- sdk/src/client/builder.rs | 21 +++ sdk/src/client/core.rs | 13 +- sdk/src/client/mod.rs | 2 + sdk/src/client/node_api/core/mod.rs | 90 ++--------- sdk/src/client/node_api/core/routes.rs | 141 +++--------------- sdk/src/client/node_api/indexer/mod.rs | 4 - sdk/src/client/node_api/participation.rs | 48 ++---- sdk/src/client/node_api/plugin/mod.rs | 10 +- sdk/src/client/node_manager/mod.rs | 50 ++++++- sdk/src/client/request_pool.rs | 93 ++++++++++++ sdk/src/wallet/core/builder.rs | 2 +- sdk/src/wallet/core/operations/client.rs | 4 + 18 files changed, 247 insertions(+), 253 deletions(-) create mode 100644 sdk/src/client/request_pool.rs diff --git a/bindings/nodejs/CHANGELOG.md b/bindings/nodejs/CHANGELOG.md index 990d60264f..d783102ee0 100644 --- a/bindings/nodejs/CHANGELOG.md +++ b/bindings/nodejs/CHANGELOG.md @@ -19,7 +19,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security --> -## 1.0.9 - 2023-09-06 +## 1.0.9 - 2023-09-07 + +### Added + +- `IClientOptions::maxParallelApiRequests`; ### Fixed 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/package.json b/bindings/nodejs/package.json index e70cb1eddf..17e1c54c2d 100644 --- a/bindings/nodejs/package.json +++ b/bindings/nodejs/package.json @@ -1,6 +1,6 @@ { "name": "@iota/sdk", - "version": "1.0.8", + "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/python/CHANGELOG.md b/bindings/python/CHANGELOG.md index d121c23629..2f3c0ac01d 100644 --- a/bindings/python/CHANGELOG.md +++ b/bindings/python/CHANGELOG.md @@ -19,6 +19,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security --> +## 1.0.2 - 2023-MM-DD + +### Added + +- `ClientOptions::maxParallelApiRequests`; + ## 1.0.1 - 2023-08-23 ### Fixed 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/sdk/CHANGELOG.md b/sdk/CHANGELOG.md index 6f6a2d0eff..bda9c242be 100644 --- a/sdk/CHANGELOG.md +++ b/sdk/CHANGELOG.md @@ -19,7 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security --> -## 1.0.3 - 2023-MM-DD +## 1.0.3 - 2023-09-07 ### Added @@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `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/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/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/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; } From e3219a0f9d4d92697f81ac6132bc891ea4144ad1 Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Thu, 7 Sep 2023 11:36:09 +0200 Subject: [PATCH 12/14] Fix parity-scale-codec audit (#1167) --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bca1ce9962..f5c3d69ec9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -370,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" @@ -2208,9 +2208,9 @@ dependencies = [ [[package]] name = "parity-scale-codec" -version = "3.6.6" +version = "3.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b4a26fb934017f2e774ad9a16b40cca8faec288e0233496c6a47f266d49f024" +checksum = "0dec8a8073036902368c2cdc0387e85ff9a37054d7e7c98e592145e0c92cd4fb" dependencies = [ "arrayvec", "bitvec", From 3f254ea90bf4ca078192b22a2a83f306ffd57d42 Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Thu, 7 Sep 2023 11:58:16 +0200 Subject: [PATCH 13/14] Bump iota-sdk to 1.0.3 (#1168) --- Cargo.lock | 2 +- sdk/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f5c3d69ec9..6cfa08b051 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1632,7 +1632,7 @@ dependencies = [ [[package]] name = "iota-sdk" -version = "1.0.2" +version = "1.0.3" dependencies = [ "anymap", "async-trait", diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index b9bf3bcdd5..106bac4b0d 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." From 735dac6522a13c37a006e8e5afa733d53df05cfb Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab <31316147+abdulmth@users.noreply.github.com> Date: Thu, 7 Sep 2023 16:41:01 +0200 Subject: [PATCH 14/14] Fix type of `value` property in `CustomAddress` (Node.js) (#1171) * fix `CustomAddress` value property type * changelog --- bindings/nodejs/CHANGELOG.md | 6 ++++++ bindings/nodejs/lib/types/wallet/transaction-options.ts | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/bindings/nodejs/CHANGELOG.md b/bindings/nodejs/CHANGELOG.md index d783102ee0..4e7d379907 100644 --- a/bindings/nodejs/CHANGELOG.md +++ b/bindings/nodejs/CHANGELOG.md @@ -19,6 +19,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security --> +## 1.0.10 - 2023-mm-dd + +### Fixed + +- Type of `value` property in `CustomAddress`; + ## 1.0.9 - 2023-09-07 ### Added 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. */