diff --git a/Cargo.lock b/Cargo.lock index 5ec1f91fb8..ea1f4dc391 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -457,7 +457,7 @@ checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" dependencies = [ "glob", "libc", - "libloading 0.7.4", + "libloading", ] [[package]] @@ -574,16 +574,6 @@ dependencies = [ "windows-sys 0.45.0", ] -[[package]] -name = "console_error_panic_hook" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" -dependencies = [ - "cfg-if", - "wasm-bindgen", -] - [[package]] name = "const-oid" version = "0.9.5" @@ -1564,12 +1554,6 @@ dependencies = [ "hashbrown 0.14.2", ] -[[package]] -name = "indoc" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" - [[package]] name = "inout" version = "0.1.3" @@ -1720,47 +1704,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "iota-sdk-nodejs" -version = "0.1.0" -dependencies = [ - "iota-sdk-bindings-core", - "log", - "neon", - "once_cell", - "serde_json", - "tokio", -] - -[[package]] -name = "iota-sdk-python" -version = "1.1.0" -dependencies = [ - "futures", - "iota-sdk-bindings-core", - "once_cell", - "pyo3", - "serde_json", - "tokio", -] - -[[package]] -name = "iota-sdk-wasm" -version = "0.1.0" -dependencies = [ - "console_error_panic_hook", - "getrandom", - "instant", - "iota-sdk-bindings-core", - "js-sys", - "log", - "serde_json", - "tokio", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-logger", -] - [[package]] name = "iota_stronghold" version = "2.0.0" @@ -1895,16 +1838,6 @@ version = "0.2.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" -[[package]] -name = "libloading" -version = "0.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "351a32417a12d5f7e82c368a66781e307834dae04c6ce0cd4456d52989229883" -dependencies = [ - "cfg-if", - "winapi", -] - [[package]] name = "libloading" version = "0.7.4" @@ -2044,47 +1977,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "neon" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28e15415261d880aed48122e917a45e87bb82cf0260bb6db48bbab44b7464373" -dependencies = [ - "neon-build", - "neon-macros", - "neon-runtime", - "semver 0.9.0", - "smallvec", -] - -[[package]] -name = "neon-build" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bac98a702e71804af3dacfde41edde4a16076a7bbe889ae61e56e18c5b1c811" - -[[package]] -name = "neon-macros" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7288eac8b54af7913c60e0eb0e2a7683020dffa342ab3fd15e28f035ba897cf" -dependencies = [ - "quote", - "syn 1.0.109", - "syn-mid", -] - -[[package]] -name = "neon-runtime" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4676720fa8bb32c64c3d9f49c47a47289239ec46b4bdb66d0913cc512cb0daca" -dependencies = [ - "cfg-if", - "libloading 0.6.7", - "smallvec", -] - [[package]] name = "nibble_vec" version = "0.1.0" @@ -2239,29 +2131,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "parking_lot" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.4.1", - "smallvec", - "windows-targets 0.48.5", -] - [[package]] name = "paste" version = "1.0.14" @@ -2464,66 +2333,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "pyo3" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e681a6cfdc4adcc93b4d3cf993749a4552018ee0a9b65fc0ccfad74352c72a38" -dependencies = [ - "cfg-if", - "indoc", - "libc", - "memoffset 0.9.0", - "parking_lot", - "pyo3-build-config", - "pyo3-ffi", - "pyo3-macros", - "unindent", -] - -[[package]] -name = "pyo3-build-config" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "076c73d0bc438f7a4ef6fdd0c3bb4732149136abd952b110ac93e4edb13a6ba5" -dependencies = [ - "once_cell", - "target-lexicon", -] - -[[package]] -name = "pyo3-ffi" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e53cee42e77ebe256066ba8aa77eff722b3bb91f3419177cf4cd0f304d3284d9" -dependencies = [ - "libc", - "pyo3-build-config", -] - -[[package]] -name = "pyo3-macros" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfeb4c99597e136528c6dd7d5e3de5434d1ceaf487436a3f03b2d56b6fc9efd1" -dependencies = [ - "proc-macro2", - "pyo3-macros-backend", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "pyo3-macros-backend" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "947dc12175c254889edc0c02e399476c2f652b4b9ebd123aa655c224de259536" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "quote" version = "1.0.33" @@ -2594,15 +2403,6 @@ dependencies = [ "bitflags 1.3.2", ] -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "redox_users" version = "0.4.3" @@ -2610,7 +2410,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ "getrandom", - "redox_syscall 0.2.16", + "redox_syscall", "thiserror", ] @@ -2789,7 +2589,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.20", + "semver", ] [[package]] @@ -2981,27 +2781,12 @@ dependencies = [ "libc", ] -[[package]] -name = "semver" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" -dependencies = [ - "semver-parser", -] - [[package]] name = "semver" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" -[[package]] -name = "semver-parser" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" - [[package]] name = "serde" version = "1.0.189" @@ -3296,17 +3081,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "syn-mid" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea305d57546cc8cd04feb14b62ec84bf17f50e3f7b12560d7bfa9265f39d9ed" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "system-configuration" version = "0.5.1" @@ -3334,12 +3108,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" -[[package]] -name = "target-lexicon" -version = "0.12.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a" - [[package]] name = "thiserror" version = "1.0.50" @@ -3618,12 +3386,6 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" -[[package]] -name = "unindent" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1766d682d402817b5ac4490b3c3002d91dfa0d22812f341609f97b08757359c" - [[package]] name = "universal-hash" version = "0.5.1" @@ -3714,8 +3476,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ "cfg-if", - "serde", - "serde_json", "wasm-bindgen-macro", ] @@ -3775,17 +3535,6 @@ version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" -[[package]] -name = "wasm-logger" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "074649a66bb306c8f2068c9016395fa65d8e08d2affcbf95acf3c24c3ab19718" -dependencies = [ - "log", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "web-sys" version = "0.3.64" diff --git a/Cargo.toml b/Cargo.toml index 99647d0d92..fafb3f78f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,9 +2,12 @@ resolver = "2" members = [ "bindings/core", - "bindings/nodejs", - "bindings/python", - "bindings/wasm", + # TODO: issue #1424 + #"bindings/nodejs", + # TODO: issue #1423 + #"bindings/python", + # TODO: issue #1425 + #"bindings/wasm", "cli", "sdk", ] diff --git a/bindings/core/src/error.rs b/bindings/core/src/error.rs index d4ed3b0a14..441130e0a9 100644 --- a/bindings/core/src/error.rs +++ b/bindings/core/src/error.rs @@ -4,8 +4,6 @@ use packable::error::UnexpectedEOF; use serde::{ser::SerializeMap, Serialize, Serializer}; -pub use super::{method::AccountMethod, response::Response}; - /// Result type of the bindings core crate. pub type Result = std::result::Result; diff --git a/bindings/core/src/lib.rs b/bindings/core/src/lib.rs index 75ab8c4288..ea1bac6fdd 100644 --- a/bindings/core/src/lib.rs +++ b/bindings/core/src/lib.rs @@ -11,6 +11,7 @@ mod response; use std::fmt::{Formatter, Result as FmtResult}; +use crypto::keys::bip44::Bip44; use derivative::Derivative; use fern_logger::{logger_init, LoggerConfig, LoggerOutputConfigBuilder}; pub use iota_sdk; @@ -26,7 +27,7 @@ pub use self::method_handler::listen_mqtt; pub use self::method_handler::CallMethod; pub use self::{ error::{Error, Result}, - method::{AccountMethod, ClientMethod, SecretManagerMethod, UtilsMethod, WalletMethod}, + method::{ClientMethod, SecretManagerMethod, UtilsMethod, WalletCommandMethod, WalletMethod}, method_handler::{call_client_method, call_secret_manager_method, call_utils_method, call_wallet_method}, response::Response, }; @@ -43,7 +44,7 @@ pub fn init_logger(config: String) -> std::result::Result<(), fern_logger::Error pub struct WalletOptions { pub storage_path: Option, pub client_options: Option, - pub coin_type: Option, + pub bip_path: Option, #[derivative(Debug(format_with = "OmittedDebug::omitted_fmt"))] pub secret_manager: Option, } @@ -59,8 +60,8 @@ impl WalletOptions { self } - pub fn with_coin_type(mut self, coin_type: impl Into>) -> Self { - self.coin_type = coin_type.into(); + pub fn with_bip_path(mut self, bip_path: impl Into>) -> Self { + self.bip_path = bip_path.into(); self } @@ -73,7 +74,7 @@ impl WalletOptions { log::debug!("wallet options: {self:?}"); let mut builder = Wallet::builder() .with_client_options(self.client_options) - .with_coin_type(self.coin_type); + .with_bip_path(self.bip_path); #[cfg(feature = "storage")] if let Some(storage_path) = &self.storage_path { diff --git a/bindings/core/src/method/mod.rs b/bindings/core/src/method/mod.rs index 966b583013..7aaf0baa5a 100644 --- a/bindings/core/src/method/mod.rs +++ b/bindings/core/src/method/mod.rs @@ -1,13 +1,13 @@ // Copyright 2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -mod account; mod client; mod secret_manager; mod utils; mod wallet; +mod wallet_command; pub use self::{ - account::AccountMethod, client::ClientMethod, secret_manager::SecretManagerMethod, utils::UtilsMethod, - wallet::WalletMethod, + client::ClientMethod, secret_manager::SecretManagerMethod, utils::UtilsMethod, wallet::WalletMethod, + wallet_command::WalletCommandMethod, }; diff --git a/bindings/core/src/method/wallet.rs b/bindings/core/src/method/wallet.rs index 9c48819fd4..e99dfa1831 100644 --- a/bindings/core/src/method/wallet.rs +++ b/bindings/core/src/method/wallet.rs @@ -10,18 +10,12 @@ use iota_sdk::wallet::events::types::{WalletEvent, WalletEventType}; use iota_sdk::{ client::{node_manager::node::NodeAuth, secret::GenerateAddressOptions}, types::block::address::Hrp, - wallet::{ - account::{ - types::{AccountIdentifier, Bip44Address}, - SyncOptions, - }, - ClientOptions, - }, + wallet::{ClientOptions, SyncOptions}, }; use serde::{Deserialize, Serialize}; use url::Url; -use crate::method::account::AccountMethod; +use crate::method::WalletCommandMethod; #[cfg(feature = "stronghold")] use crate::OmittedDebug; @@ -31,35 +25,12 @@ use crate::OmittedDebug; #[serde(tag = "name", content = "data", rename_all = "camelCase")] #[non_exhaustive] pub enum WalletMethod { - /// Creates an account. - /// Expected response: [`Account`](crate::Response::Account) - #[serde(rename_all = "camelCase")] - CreateAccount { - /// The account alias. - alias: Option, - /// The bech32 HRP. - bech32_hrp: Option, - /// BIP44 addresses. - addresses: Option>, - }, - /// Read account. - /// Expected response: [`Account`](crate::Response::Account) - #[serde(rename_all = "camelCase")] - GetAccount { account_id: AccountIdentifier }, - /// Return the account indexes. - /// Expected response: [`AccountIndexes`](crate::Response::AccountIndexes) - GetAccountIndexes, - /// Read accounts. - /// Expected response: [`Accounts`](crate::Response::Accounts) - GetAccounts, - /// Consume an account method. + /// Consume a wallet command method. /// Returns [`Response`](crate::Response) #[serde(rename_all = "camelCase")] - CallAccountMethod { - /// The account identifier. - account_id: AccountIdentifier, - /// The account method to call. - method: AccountMethod, + CallMethod { + /// The wallet command method to call. + method: WalletCommandMethod, }, /// Backup storage. Password must be the current one, when Stronghold is used as SecretManager. /// Expected response: [`Ok`](crate::Response::Ok) @@ -94,31 +65,15 @@ pub enum WalletMethod { #[cfg(feature = "stronghold")] #[cfg_attr(docsrs, doc(cfg(feature = "stronghold")))] IsStrongholdPasswordAvailable, - /// Find accounts with unspent outputs - /// Expected response: [`Accounts`](crate::Response::Accounts) - #[serde(rename_all = "camelCase")] - RecoverAccounts { - /// The index of the first account to search for. - account_start_index: u32, - /// The number of accounts to search for, after the last account with unspent outputs. - account_gap_limit: u32, - /// The number of addresses to search for, after the last address with unspent outputs, in - /// each account. - address_gap_limit: u32, - /// Optional parameter to specify the sync options. The `address_start_index` and `force_syncing` - /// fields will be overwritten to skip existing addresses. - sync_options: Option, - }, /// Restore a backup from a Stronghold file - /// Replaces client_options, coin_type, secret_manager and accounts. Returns an error if accounts were already + /// Replaces client_options, coin_type, secret_manager and wallet. Returns an error if the wallet was already /// created If Stronghold is used as secret_manager, the existing Stronghold file will be overwritten. If a /// mnemonic was stored, it will be gone. /// if ignore_if_coin_type_mismatch.is_some(), client options will not be restored - /// if ignore_if_coin_type_mismatch == Some(true), client options coin type and accounts will not be restored if + /// if ignore_if_coin_type_mismatch == Some(true), client options coin type and the wallet will not be restored if /// the cointype doesn't match - /// if ignore_if_bech32_hrp_mismatch == Some("rms"), but addresses have something different like "smr", no accounts - /// will be restored. - /// Expected response: [`Ok`](crate::Response::Ok) + /// If a bech32 hrp is provided to ignore_if_bech32_hrp_mismatch, that doesn't match the one of the current + /// address, the wallet will not be restored. Expected response: [`Ok`](crate::Response::Ok) #[cfg(feature = "stronghold")] #[cfg_attr(docsrs, doc(cfg(feature = "stronghold")))] #[serde(rename_all = "camelCase")] @@ -129,17 +84,14 @@ pub enum WalletMethod { #[derivative(Debug(format_with = "OmittedDebug::omitted_fmt"))] password: String, /// If ignore_if_coin_type_mismatch.is_some(), client options will not be restored. - /// If ignore_if_coin_type_mismatch == Some(true), client options coin type and accounts will not be restored + /// If ignore_if_coin_type_mismatch == Some(true), client options coin type and wallet will not be restored /// if the cointype doesn't match. ignore_if_coin_type_mismatch: Option, - /// If ignore_if_bech32_hrp_mismatch == Some("rms"), but addresses have something different like "smr", no - /// accounts will be restored. + /// If a bech32 hrp is provided to ignore_if_bech32_hrp_mismatch, that doesn't match the one of the current + /// address, the wallet will not be restored. ignore_if_bech32_mismatch: Option, }, - /// Removes the latest account (account with the largest account index). - /// Expected response: [`Ok`](crate::Response::Ok) - RemoveLatestAccount, - /// Updates the client options for all accounts. + /// Updates the client options for the wallet. /// Expected response: [`Ok`](crate::Response::Ok) #[serde(rename_all = "camelCase")] SetClientOptions { client_options: Box }, diff --git a/bindings/core/src/method/account.rs b/bindings/core/src/method/wallet_command.rs similarity index 84% rename from bindings/core/src/method/account.rs rename to bindings/core/src/method/wallet_command.rs index 8ee185ca87..72aa622eaf 100644 --- a/bindings/core/src/method/account.rs +++ b/bindings/core/src/method/wallet_command.rs @@ -1,45 +1,32 @@ // Copyright 2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -#[cfg(feature = "participation")] use iota_sdk::{ - client::node_manager::node::Node, - types::api::plugins::participation::types::{ParticipationEventId, ParticipationEventType}, - wallet::account::types::participation::ParticipationEventRegistrationOptions, -}; -use iota_sdk::{ - client::{ - api::{input_selection::Burn, PreparedTransactionDataDto, SignedTransactionDataDto}, - secret::GenerateAddressOptions, - }, + client::api::{input_selection::Burn, PreparedTransactionDataDto, SignedTransactionDataDto}, types::block::{ address::Bech32Address, output::{dto::OutputDto, OutputId, TokenId}, payload::signed_transaction::TransactionId, }, wallet::{ - account::{ - ConsolidationParams, CreateAccountParams, CreateNativeTokenParams, FilterOptions, MintNftParams, - OutputParams, OutputsToClaim, SyncOptions, TransactionOptions, - }, - SendNativeTokensParams, SendNftParams, SendParams, + ConsolidationParams, CreateAccountParams, CreateNativeTokenParams, FilterOptions, MintNftParams, OutputParams, + OutputsToClaim, SendNativeTokensParams, SendNftParams, SendParams, SyncOptions, TransactionOptions, }, U256, }; +#[cfg(feature = "participation")] +use iota_sdk::{ + client::node_manager::node::Node, + types::api::plugins::participation::types::{ParticipationEventId, ParticipationEventType}, + wallet::types::participation::ParticipationEventRegistrationOptions, +}; use serde::{Deserialize, Serialize}; -/// Each public account method. +/// Each public wallet command method. #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(tag = "name", content = "data", rename_all = "camelCase")] #[non_exhaustive] -pub enum AccountMethod { - /// List addresses. - /// Expected response: [`Addresses`](crate::Response::Addresses) - Addresses, - /// Returns only addresses of the account with unspent outputs - /// Expected response: - /// [`AddressesWithUnspentOutputs`](crate::Response::AddressesWithUnspentOutputs) - AddressesWithUnspentOutputs, +pub enum WalletCommandMethod { /// Get outputs with additional unlock conditions /// Expected response: [`OutputIds`](crate::Response::OutputIds) #[serde(rename_all = "camelCase")] @@ -54,25 +41,22 @@ pub enum AccountMethod { #[cfg_attr(docsrs, doc(cfg(feature = "participation")))] #[serde(rename_all = "camelCase")] DeregisterParticipationEvent { event_id: ParticipationEventId }, - /// Generate new Ed25519 addresses. - /// Expected response: [`GeneratedEd25519Addresses`](crate::Response::GeneratedEd25519Addresses) - GenerateEd25519Addresses { - amount: u32, - options: Option, - }, - /// Get account balance information. + /// Get the wallet address. + /// Expected response: [`Address`](crate::Response::Address) + GetAddress, + /// Get wallet balance information. /// Expected response: [`Balance`](crate::Response::Balance) GetBalance, /// Get the [`Output`](iota_sdk::types::block::output::Output) that minted a native token by its TokenId /// Expected response: [`Output`](crate::Response::Output) #[serde(rename_all = "camelCase")] GetFoundryOutput { token_id: TokenId }, - /// Get the transaction with inputs of an incoming transaction stored in the account + /// Get the transaction with inputs of an incoming transaction stored in the wallet /// List might not be complete, if the node pruned the data already /// Expected response: [`Transaction`](crate::Response::Transaction) #[serde(rename_all = "camelCase")] GetIncomingTransaction { transaction_id: TransactionId }, - /// Get the [`OutputData`](iota_sdk::wallet::account::types::OutputData) of an output stored in the account + /// Get the [`OutputData`](iota_sdk::wallet::types::OutputData) of an output stored in the wallet /// Expected response: [`OutputData`](crate::Response::OutputData) #[serde(rename_all = "camelCase")] GetOutput { output_id: OutputId }, @@ -99,34 +83,34 @@ pub enum AccountMethod { #[cfg(feature = "participation")] #[cfg_attr(docsrs, doc(cfg(feature = "participation")))] GetParticipationEvents, - /// Calculates a participation overview for an account. If event_ids are provided, only return outputs and tracked + /// Calculates a participation overview for the wallet. If event_ids are provided, only return outputs and tracked /// participations for them. /// Expected response: - /// [`AccountParticipationOverview`](crate::Response::AccountParticipationOverview) + /// [`ParticipationOverview`](crate::Response::ParticipationOverview) #[cfg(feature = "participation")] #[cfg_attr(docsrs, doc(cfg(feature = "participation")))] #[serde(rename_all = "camelCase")] GetParticipationOverview { event_ids: Option>, }, - /// Get the [`Transaction`](iota_sdk::wallet::account::types::Transaction) of a transaction stored in the account + /// Get the [`Transaction`](iota_sdk::wallet::types::Transaction) of a transaction stored in the wallet /// Expected response: [`Transaction`](crate::Response::Transaction) #[serde(rename_all = "camelCase")] GetTransaction { transaction_id: TransactionId }, - /// Get the account's total voting power (voting or NOT voting). + /// Get the wallet's total voting power (voting or NOT voting). /// Expected response: [`VotingPower`](crate::Response::VotingPower) #[cfg(feature = "participation")] #[cfg_attr(docsrs, doc(cfg(feature = "participation")))] GetVotingPower, - /// Returns all incoming transactions of the account + /// Returns all incoming transactions of the wallet /// Expected response: /// [`Transactions`](crate::Response::Transactions) IncomingTransactions, - /// Returns all outputs of the account + /// Returns all outputs of the wallet /// Expected response: [`OutputsData`](crate::Response::OutputsData) #[serde(rename_all = "camelCase")] Outputs { filter_options: Option }, - /// Returns all pending transactions of the account + /// Returns all pending transactions of the wallet /// Expected response: [`Transactions`](crate::Response::Transactions) PendingTransactions, /// A generic function that can be used to burn native tokens, nfts, foundries and accounts. @@ -156,7 +140,7 @@ pub enum AccountMethod { params: CreateNativeTokenParams, options: Option, }, - /// Reduces an account's "voting power" by a given amount. + /// Reduces a wallet's "voting power" by a given amount. /// This will stop voting, but the voting data isn't lost and calling `Vote` without parameters will revote. /// Expected response: [`PreparedTransaction`](crate::Response::PreparedTransaction) #[cfg(feature = "participation")] @@ -165,7 +149,7 @@ pub enum AccountMethod { #[serde(with = "iota_sdk::utils::serde::string")] amount: u64, }, - /// Designates a given amount of tokens towards an account's "voting power" by creating a + /// Designates a given amount of tokens towards a wallet's "voting power" by creating a /// special output, which is really a basic one with some metadata. /// This will stop voting in most cases (if there is a remainder output), but the voting data isn't lost and /// calling `Vote` without parameters will revote. Expected response: @@ -258,7 +242,7 @@ pub enum AccountMethod { RegisterParticipationEvents { options: ParticipationEventRegistrationOptions, }, - /// Reissues a transaction sent from the account for a provided transaction id until it's + /// Reissues a transaction sent from the wallet for a provided transaction id until it's /// included (referenced by a milestone). Returns the included block id. /// Expected response: [`BlockId`](crate::Response::BlockId) #[serde(rename_all = "camelCase")] @@ -290,14 +274,14 @@ pub enum AccountMethod { outputs: Vec, options: Option, }, - /// Set the alias of the account. + /// Set the alias of the wallet. /// Expected response: [`Ok`](crate::Response::Ok) SetAlias { alias: String }, - /// Set the fallback SyncOptions for account syncing. + /// Set the fallback SyncOptions for wallet syncing. /// If storage is enabled, will persist during restarts. /// Expected response: [`Ok`](crate::Response::Ok) SetDefaultSyncOptions { options: SyncOptions }, - /// Validate the transaction, sign it, submit it to a node and store it in the account. + /// Validate the transaction, sign it, submit it to a node and store it in the wallet. /// Expected response: [`SentTransaction`](crate::Response::SentTransaction) #[serde(rename_all = "camelCase")] SignAndSubmitTransaction { @@ -309,23 +293,23 @@ pub enum AccountMethod { SignTransaction { prepared_transaction_data: PreparedTransactionDataDto, }, - /// Validate the transaction, submit it to a node and store it in the account. + /// Validate the transaction, submit it to a node and store it in the wallet. /// Expected response: [`SentTransaction`](crate::Response::SentTransaction) #[serde(rename_all = "camelCase")] SubmitAndStoreTransaction { signed_transaction_data: SignedTransactionDataDto, }, - /// Sync the account by fetching new information from the nodes. Will also reissue pending transactions + /// Sync the wallet by fetching new information from the nodes. Will also reissue pending transactions /// if necessary. A custom default can be set using SetDefaultSyncOptions. /// Expected response: [`Balance`](crate::Response::Balance) Sync { /// Sync options options: Option, }, - /// Returns all transaction of the account + /// Returns all transactions of the wallet /// Expected response: [`Transactions`](crate::Response::Transactions) Transactions, - /// Returns all unspent outputs of the account + /// Returns all unspent outputs of the wallet /// Expected response: [`OutputsData`](crate::Response::OutputsData) #[serde(rename_all = "camelCase")] UnspentOutputs { filter_options: Option }, diff --git a/bindings/core/src/method_handler/account.rs b/bindings/core/src/method_handler/account.rs deleted file mode 100644 index ff4acf83d8..0000000000 --- a/bindings/core/src/method_handler/account.rs +++ /dev/null @@ -1,294 +0,0 @@ -// Copyright 2023 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use iota_sdk::{ - client::api::{ - PreparedTransactionData, PreparedTransactionDataDto, SignedTransactionData, SignedTransactionDataDto, - }, - types::{ - block::output::{dto::OutputDto, Output}, - TryFromDto, - }, - wallet::account::{ - types::TransactionWithMetadataDto, Account, OutputDataDto, PreparedCreateNativeTokenTransactionDto, - }, -}; - -use crate::{method::AccountMethod, Response, Result}; - -pub(crate) async fn call_account_method_internal(account: &Account, method: AccountMethod) -> Result { - let response = match method { - AccountMethod::Addresses => { - let addresses = account.addresses().await; - Response::Addresses(addresses) - } - AccountMethod::AddressesWithUnspentOutputs => { - let addresses = account.addresses_with_unspent_outputs().await?; - Response::AddressesWithUnspentOutputs(addresses) - } - AccountMethod::ClaimableOutputs { outputs_to_claim } => { - let output_ids = account.claimable_outputs(outputs_to_claim).await?; - Response::OutputIds(output_ids) - } - AccountMethod::ClaimOutputs { output_ids_to_claim } => { - let transaction = account.claim_outputs(output_ids_to_claim.to_vec()).await?; - Response::SentTransaction(TransactionWithMetadataDto::from(&transaction)) - } - #[cfg(feature = "participation")] - AccountMethod::DeregisterParticipationEvent { event_id } => { - account.deregister_participation_event(&event_id).await?; - Response::Ok - } - AccountMethod::GenerateEd25519Addresses { amount, options } => { - let address = account.generate_ed25519_addresses(amount, options).await?; - Response::GeneratedAccountAddresses(address) - } - AccountMethod::GetBalance => Response::Balance(account.balance().await?), - AccountMethod::GetFoundryOutput { token_id } => { - let output = account.get_foundry_output(token_id).await?; - Response::Output(OutputDto::from(&output)) - } - AccountMethod::GetIncomingTransaction { transaction_id } => { - let transaction = account.get_incoming_transaction(&transaction_id).await; - - transaction.map_or_else( - || Response::Transaction(None), - |transaction| Response::Transaction(Some(Box::new(TransactionWithMetadataDto::from(&transaction)))), - ) - } - AccountMethod::GetOutput { output_id } => { - let output_data = account.get_output(&output_id).await; - Response::OutputData(output_data.as_ref().map(OutputDataDto::from).map(Box::new)) - } - #[cfg(feature = "participation")] - AccountMethod::GetParticipationEvent { event_id } => { - let event_and_nodes = account.get_participation_event(event_id).await?; - Response::ParticipationEvent(event_and_nodes) - } - #[cfg(feature = "participation")] - AccountMethod::GetParticipationEventIds { node, event_type } => { - let event_ids = account.get_participation_event_ids(&node, event_type).await?; - Response::ParticipationEventIds(event_ids) - } - #[cfg(feature = "participation")] - AccountMethod::GetParticipationEventStatus { event_id } => { - let event_status = account.get_participation_event_status(&event_id).await?; - Response::ParticipationEventStatus(event_status) - } - #[cfg(feature = "participation")] - AccountMethod::GetParticipationEvents => { - let events = account.get_participation_events().await?; - Response::ParticipationEvents(events) - } - #[cfg(feature = "participation")] - AccountMethod::GetParticipationOverview { event_ids } => { - let overview = account.get_participation_overview(event_ids).await?; - Response::AccountParticipationOverview(overview) - } - AccountMethod::GetTransaction { transaction_id } => { - let transaction = account.get_transaction(&transaction_id).await; - Response::Transaction(transaction.as_ref().map(TransactionWithMetadataDto::from).map(Box::new)) - } - #[cfg(feature = "participation")] - AccountMethod::GetVotingPower => { - let voting_power = account.get_voting_power().await?; - Response::VotingPower(voting_power.to_string()) - } - AccountMethod::IncomingTransactions => { - let transactions = account.incoming_transactions().await; - Response::Transactions(transactions.iter().map(TransactionWithMetadataDto::from).collect()) - } - AccountMethod::Outputs { filter_options } => { - let outputs = account.outputs(filter_options).await?; - Response::OutputsData(outputs.iter().map(OutputDataDto::from).collect()) - } - AccountMethod::PendingTransactions => { - let transactions = account.pending_transactions().await; - Response::Transactions(transactions.iter().map(TransactionWithMetadataDto::from).collect()) - } - AccountMethod::PrepareBurn { burn, options } => { - let data = account.prepare_burn(burn, options).await?; - Response::PreparedTransaction(PreparedTransactionDataDto::from(&data)) - } - AccountMethod::PrepareConsolidateOutputs { params } => { - let data = account.prepare_consolidate_outputs(params).await?; - Response::PreparedTransaction(PreparedTransactionDataDto::from(&data)) - } - AccountMethod::PrepareCreateAccountOutput { params, options } => { - let data = account.prepare_create_account_output(params, options).await?; - Response::PreparedTransaction(PreparedTransactionDataDto::from(&data)) - } - AccountMethod::PrepareMeltNativeToken { - token_id, - melt_amount, - options, - } => { - let data = account - .prepare_melt_native_token(token_id, melt_amount, options) - .await?; - Response::PreparedTransaction(PreparedTransactionDataDto::from(&data)) - } - #[cfg(feature = "participation")] - AccountMethod::PrepareDecreaseVotingPower { amount } => { - let data = account.prepare_decrease_voting_power(amount).await?; - Response::PreparedTransaction(PreparedTransactionDataDto::from(&data)) - } - AccountMethod::PrepareMintNativeToken { - token_id, - mint_amount, - options, - } => { - let data = account - .prepare_mint_native_token(token_id, mint_amount, options) - .await?; - Response::PreparedTransaction(PreparedTransactionDataDto::from(&data)) - } - #[cfg(feature = "participation")] - AccountMethod::PrepareIncreaseVotingPower { amount } => { - let data = account.prepare_increase_voting_power(amount).await?; - Response::PreparedTransaction(PreparedTransactionDataDto::from(&data)) - } - AccountMethod::PrepareMintNfts { params, options } => { - let data = account.prepare_mint_nfts(params, options).await?; - Response::PreparedTransaction(PreparedTransactionDataDto::from(&data)) - } - AccountMethod::PrepareCreateNativeToken { params, options } => { - let data = account.prepare_create_native_token(params, options).await?; - Response::PreparedCreateNativeTokenTransaction(PreparedCreateNativeTokenTransactionDto::from(&data)) - } - AccountMethod::PrepareOutput { - params, - transaction_options, - } => { - let output = account.prepare_output(*params, transaction_options).await?; - Response::Output(OutputDto::from(&output)) - } - AccountMethod::PrepareSend { params, options } => { - let data = account.prepare_send(params, options).await?; - Response::PreparedTransaction(PreparedTransactionDataDto::from(&data)) - } - AccountMethod::PrepareSendNativeTokens { params, options } => { - let data = account.prepare_send_native_tokens(params.clone(), options).await?; - Response::PreparedTransaction(PreparedTransactionDataDto::from(&data)) - } - AccountMethod::PrepareSendNft { params, options } => { - let data = account.prepare_send_nft(params.clone(), options).await?; - Response::PreparedTransaction(PreparedTransactionDataDto::from(&data)) - } - #[cfg(feature = "participation")] - AccountMethod::PrepareStopParticipating { event_id } => { - let data = account.prepare_stop_participating(event_id).await?; - Response::PreparedTransaction(PreparedTransactionDataDto::from(&data)) - } - AccountMethod::PrepareTransaction { outputs, options } => { - let token_supply = account.client().get_token_supply().await?; - let data = account - .prepare_transaction( - outputs - .into_iter() - .map(|o| Ok(Output::try_from_dto_with_params(o, token_supply)?)) - .collect::>>()?, - options, - ) - .await?; - Response::PreparedTransaction(PreparedTransactionDataDto::from(&data)) - } - #[cfg(feature = "participation")] - AccountMethod::PrepareVote { event_id, answers } => { - let data = account.prepare_vote(event_id, answers).await?; - Response::PreparedTransaction(PreparedTransactionDataDto::from(&data)) - } - #[cfg(feature = "participation")] - AccountMethod::RegisterParticipationEvents { options } => { - let events = account.register_participation_events(&options).await?; - Response::ParticipationEvents(events) - } - AccountMethod::ReissueTransactionUntilIncluded { - transaction_id, - interval, - max_attempts, - } => { - let block_id = account - .reissue_transaction_until_included(&transaction_id, interval, max_attempts) - .await?; - Response::BlockId(block_id) - } - AccountMethod::Send { - amount, - address, - options, - } => { - let transaction = account.send(amount, address, options).await?; - Response::SentTransaction(TransactionWithMetadataDto::from(&transaction)) - } - AccountMethod::SendWithParams { params, options } => { - let transaction = account.send_with_params(params, options).await?; - Response::SentTransaction(TransactionWithMetadataDto::from(&transaction)) - } - AccountMethod::SendOutputs { outputs, options } => { - let token_supply = account.client().get_token_supply().await?; - let transaction = account - .send_outputs( - outputs - .into_iter() - .map(|o| Ok(Output::try_from_dto_with_params(o, token_supply)?)) - .collect::>>()?, - options, - ) - .await?; - Response::SentTransaction(TransactionWithMetadataDto::from(&transaction)) - } - AccountMethod::SetAlias { alias } => { - account.set_alias(&alias).await?; - Response::Ok - } - AccountMethod::SetDefaultSyncOptions { options } => { - account.set_default_sync_options(options).await?; - Response::Ok - } - AccountMethod::SignAndSubmitTransaction { - prepared_transaction_data, - } => { - let transaction = account - .sign_and_submit_transaction( - PreparedTransactionData::try_from_dto_with_params( - prepared_transaction_data, - account.client().get_protocol_parameters().await?, - )?, - None, - ) - .await?; - Response::SentTransaction(TransactionWithMetadataDto::from(&transaction)) - } - AccountMethod::SignTransaction { - prepared_transaction_data, - } => { - let signed_transaction_data = account - .sign_transaction(&PreparedTransactionData::try_from_dto(prepared_transaction_data)?) - .await?; - Response::SignedTransactionData(SignedTransactionDataDto::from(&signed_transaction_data)) - } - AccountMethod::SubmitAndStoreTransaction { - signed_transaction_data, - } => { - let signed_transaction_data = SignedTransactionData::try_from_dto_with_params( - signed_transaction_data, - account.client().get_protocol_parameters().await?, - )?; - let transaction = account - .submit_and_store_transaction(signed_transaction_data, None) - .await?; - Response::SentTransaction(TransactionWithMetadataDto::from(&transaction)) - } - AccountMethod::Sync { options } => Response::Balance(account.sync(options).await?), - AccountMethod::Transactions => { - let transactions = account.transactions().await; - Response::Transactions(transactions.iter().map(TransactionWithMetadataDto::from).collect()) - } - AccountMethod::UnspentOutputs { filter_options } => { - let outputs = account.unspent_outputs(filter_options).await?; - Response::OutputsData(outputs.iter().map(OutputDataDto::from).collect()) - } - }; - Ok(response) -} diff --git a/bindings/core/src/method_handler/mod.rs b/bindings/core/src/method_handler/mod.rs index 49be54cf5e..35bccfbbc6 100644 --- a/bindings/core/src/method_handler/mod.rs +++ b/bindings/core/src/method_handler/mod.rs @@ -1,12 +1,12 @@ // Copyright 2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -mod account; mod call_method; mod client; mod secret_manager; mod utils; mod wallet; +mod wallet_command; pub use call_method::{ call_client_method, call_secret_manager_method, call_utils_method, call_wallet_method, CallMethod, diff --git a/bindings/core/src/method_handler/wallet.rs b/bindings/core/src/method_handler/wallet.rs index 01510cace4..b24ac505d7 100644 --- a/bindings/core/src/method_handler/wallet.rs +++ b/bindings/core/src/method_handler/wallet.rs @@ -3,70 +3,15 @@ use std::time::Duration; -use iota_sdk::{ - types::block::address::ToBech32Ext, - wallet::{account::AccountDetailsDto, Wallet}, -}; +use iota_sdk::{types::block::address::ToBech32Ext, wallet::Wallet}; -use super::account::call_account_method_internal; +use super::wallet_command::call_wallet_command_method_internal; use crate::{method::WalletMethod, response::Response, Result}; /// Call a wallet method. pub(crate) async fn call_wallet_method_internal(wallet: &Wallet, method: WalletMethod) -> Result { let response = match method { - WalletMethod::CreateAccount { - alias, - bech32_hrp, - addresses, - } => { - let mut builder = wallet.create_account(); - - if let Some(alias) = alias { - builder = builder.with_alias(alias); - } - - if let Some(bech32_hrp) = bech32_hrp { - builder = builder.with_bech32_hrp(bech32_hrp); - } - - if let Some(addresses) = addresses { - builder = builder.with_addresses(addresses); - } - - match builder.finish().await { - Ok(account) => { - let account = account.details().await; - Response::Account(AccountDetailsDto::from(&*account)) - } - Err(e) => return Err(e.into()), - } - } - WalletMethod::GetAccount { account_id } => { - let account = wallet.get_account(account_id.clone()).await?; - let account = account.details().await; - Response::Account(AccountDetailsDto::from(&*account)) - } - WalletMethod::GetAccountIndexes => { - let accounts = wallet.get_accounts().await?; - let mut account_indexes = Vec::with_capacity(accounts.len()); - for account in accounts.iter() { - account_indexes.push(*account.details().await.index()); - } - Response::AccountIndexes(account_indexes) - } - WalletMethod::GetAccounts => { - let accounts = wallet.get_accounts().await?; - let mut account_dtos = Vec::with_capacity(accounts.len()); - for account in accounts { - let account = account.details().await; - account_dtos.push(AccountDetailsDto::from(&*account)); - } - Response::Accounts(account_dtos) - } - WalletMethod::CallAccountMethod { account_id, method } => { - let account = wallet.get_account(account_id).await?; - call_account_method_internal(&account, method).await? - } + WalletMethod::CallMethod { method } => call_wallet_command_method_internal(&wallet, method).await?, #[cfg(feature = "stronghold")] WalletMethod::Backup { destination, password } => { wallet.backup(destination, password).await?; @@ -92,26 +37,6 @@ pub(crate) async fn call_wallet_method_internal(wallet: &Wallet, method: WalletM let is_available = wallet.is_stronghold_password_available().await?; Response::Bool(is_available) } - WalletMethod::RecoverAccounts { - account_start_index, - account_gap_limit, - address_gap_limit, - sync_options, - } => { - let accounts = wallet - .recover_accounts(account_start_index, account_gap_limit, address_gap_limit, sync_options) - .await?; - let mut account_dtos = Vec::with_capacity(accounts.len()); - for account in accounts { - let account = account.details().await; - account_dtos.push(AccountDetailsDto::from(&*account)); - } - Response::Accounts(account_dtos) - } - WalletMethod::RemoveLatestAccount => { - wallet.remove_latest_account().await?; - Response::Ok - } #[cfg(feature = "stronghold")] WalletMethod::RestoreBackup { source, @@ -150,7 +75,7 @@ pub(crate) async fn call_wallet_method_internal(wallet: &Wallet, method: WalletM let bech32_hrp = match bech32_hrp { Some(bech32_hrp) => bech32_hrp, - None => wallet.get_bech32_hrp().await?, + None => *wallet.address().await.hrp(), }; Response::Bech32Address(address.to_bech32(bech32_hrp)) diff --git a/bindings/core/src/method_handler/wallet_command.rs b/bindings/core/src/method_handler/wallet_command.rs new file mode 100644 index 0000000000..562cdd370f --- /dev/null +++ b/bindings/core/src/method_handler/wallet_command.rs @@ -0,0 +1,283 @@ +// Copyright 2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use iota_sdk::{ + client::api::{ + PreparedTransactionData, PreparedTransactionDataDto, SignedTransactionData, SignedTransactionDataDto, + }, + types::{ + block::output::{dto::OutputDto, Output}, + TryFromDto, + }, + wallet::{types::TransactionWithMetadataDto, OutputDataDto, PreparedCreateNativeTokenTransactionDto, Wallet}, +}; + +use crate::{method::WalletCommandMethod, Response, Result}; + +pub(crate) async fn call_wallet_command_method_internal( + wallet: &Wallet, + method: WalletCommandMethod, +) -> Result { + let response = match method { + WalletCommandMethod::ClaimableOutputs { outputs_to_claim } => { + let output_ids = wallet.claimable_outputs(outputs_to_claim).await?; + Response::OutputIds(output_ids) + } + WalletCommandMethod::ClaimOutputs { output_ids_to_claim } => { + let transaction = wallet.claim_outputs(output_ids_to_claim.to_vec()).await?; + Response::SentTransaction(TransactionWithMetadataDto::from(&transaction)) + } + #[cfg(feature = "participation")] + WalletCommandMethod::DeregisterParticipationEvent { event_id } => { + wallet.deregister_participation_event(&event_id).await?; + Response::Ok + } + WalletCommandMethod::GetAddress => { + let address = wallet.address().await; + Response::Address(address) + } + WalletCommandMethod::GetBalance => Response::Balance(wallet.balance().await?), + WalletCommandMethod::GetFoundryOutput { token_id } => { + let output = wallet.get_foundry_output(token_id).await?; + Response::Output(OutputDto::from(&output)) + } + WalletCommandMethod::GetIncomingTransaction { transaction_id } => { + let transaction = wallet.get_incoming_transaction(&transaction_id).await; + + transaction.map_or_else( + || Response::Transaction(None), + |transaction| Response::Transaction(Some(Box::new(TransactionWithMetadataDto::from(&transaction)))), + ) + } + WalletCommandMethod::GetOutput { output_id } => { + let output_data = wallet.get_output(&output_id).await; + Response::OutputData(output_data.as_ref().map(OutputDataDto::from).map(Box::new)) + } + #[cfg(feature = "participation")] + WalletCommandMethod::GetParticipationEvent { event_id } => { + let event_and_nodes = wallet.get_participation_event(event_id).await?; + Response::ParticipationEvent(event_and_nodes) + } + #[cfg(feature = "participation")] + WalletCommandMethod::GetParticipationEventIds { node, event_type } => { + let event_ids = wallet.get_participation_event_ids(&node, event_type).await?; + Response::ParticipationEventIds(event_ids) + } + #[cfg(feature = "participation")] + WalletCommandMethod::GetParticipationEventStatus { event_id } => { + let event_status = wallet.get_participation_event_status(&event_id).await?; + Response::ParticipationEventStatus(event_status) + } + #[cfg(feature = "participation")] + WalletCommandMethod::GetParticipationEvents => { + let events = wallet.get_participation_events().await?; + Response::ParticipationEvents(events) + } + #[cfg(feature = "participation")] + WalletCommandMethod::GetParticipationOverview { event_ids } => { + let overview = wallet.get_participation_overview(event_ids).await?; + Response::ParticipationOverview(overview) + } + WalletCommandMethod::GetTransaction { transaction_id } => { + let transaction = wallet.get_transaction(&transaction_id).await; + Response::Transaction(transaction.as_ref().map(TransactionWithMetadataDto::from).map(Box::new)) + } + #[cfg(feature = "participation")] + WalletCommandMethod::GetVotingPower => { + let voting_power = wallet.get_voting_power().await?; + Response::VotingPower(voting_power.to_string()) + } + WalletCommandMethod::IncomingTransactions => { + let transactions = wallet.incoming_transactions().await; + Response::Transactions(transactions.iter().map(TransactionWithMetadataDto::from).collect()) + } + WalletCommandMethod::Outputs { filter_options } => { + let outputs = wallet.outputs(filter_options).await?; + Response::OutputsData(outputs.iter().map(OutputDataDto::from).collect()) + } + WalletCommandMethod::PendingTransactions => { + let transactions = wallet.pending_transactions().await; + Response::Transactions(transactions.iter().map(TransactionWithMetadataDto::from).collect()) + } + WalletCommandMethod::PrepareBurn { burn, options } => { + let data = wallet.prepare_burn(burn, options).await?; + Response::PreparedTransaction(PreparedTransactionDataDto::from(&data)) + } + WalletCommandMethod::PrepareConsolidateOutputs { params } => { + let data = wallet.prepare_consolidate_outputs(params).await?; + Response::PreparedTransaction(PreparedTransactionDataDto::from(&data)) + } + WalletCommandMethod::PrepareCreateAccountOutput { params, options } => { + let data = wallet.prepare_create_account_output(params, options).await?; + Response::PreparedTransaction(PreparedTransactionDataDto::from(&data)) + } + WalletCommandMethod::PrepareMeltNativeToken { + token_id, + melt_amount, + options, + } => { + let data = wallet.prepare_melt_native_token(token_id, melt_amount, options).await?; + Response::PreparedTransaction(PreparedTransactionDataDto::from(&data)) + } + #[cfg(feature = "participation")] + WalletCommandMethod::PrepareDecreaseVotingPower { amount } => { + let data = wallet.prepare_decrease_voting_power(amount).await?; + Response::PreparedTransaction(PreparedTransactionDataDto::from(&data)) + } + WalletCommandMethod::PrepareMintNativeToken { + token_id, + mint_amount, + options, + } => { + let data = wallet.prepare_mint_native_token(token_id, mint_amount, options).await?; + Response::PreparedTransaction(PreparedTransactionDataDto::from(&data)) + } + #[cfg(feature = "participation")] + WalletCommandMethod::PrepareIncreaseVotingPower { amount } => { + let data = wallet.prepare_increase_voting_power(amount).await?; + Response::PreparedTransaction(PreparedTransactionDataDto::from(&data)) + } + WalletCommandMethod::PrepareMintNfts { params, options } => { + let data = wallet.prepare_mint_nfts(params, options).await?; + Response::PreparedTransaction(PreparedTransactionDataDto::from(&data)) + } + WalletCommandMethod::PrepareCreateNativeToken { params, options } => { + let data = wallet.prepare_create_native_token(params, options).await?; + Response::PreparedCreateNativeTokenTransaction(PreparedCreateNativeTokenTransactionDto::from(&data)) + } + WalletCommandMethod::PrepareOutput { + params, + transaction_options, + } => { + let output = wallet.prepare_output(*params, transaction_options).await?; + Response::Output(OutputDto::from(&output)) + } + WalletCommandMethod::PrepareSend { params, options } => { + let data = wallet.prepare_send(params, options).await?; + Response::PreparedTransaction(PreparedTransactionDataDto::from(&data)) + } + WalletCommandMethod::PrepareSendNativeTokens { params, options } => { + let data = wallet.prepare_send_native_tokens(params.clone(), options).await?; + Response::PreparedTransaction(PreparedTransactionDataDto::from(&data)) + } + WalletCommandMethod::PrepareSendNft { params, options } => { + let data = wallet.prepare_send_nft(params.clone(), options).await?; + Response::PreparedTransaction(PreparedTransactionDataDto::from(&data)) + } + #[cfg(feature = "participation")] + WalletCommandMethod::PrepareStopParticipating { event_id } => { + let data = wallet.prepare_stop_participating(event_id).await?; + Response::PreparedTransaction(PreparedTransactionDataDto::from(&data)) + } + WalletCommandMethod::PrepareTransaction { outputs, options } => { + let token_supply = wallet.client().get_token_supply().await?; + let data = wallet + .prepare_transaction( + outputs + .into_iter() + .map(|o| Ok(Output::try_from_dto_with_params(o, token_supply)?)) + .collect::>>()?, + options, + ) + .await?; + Response::PreparedTransaction(PreparedTransactionDataDto::from(&data)) + } + #[cfg(feature = "participation")] + WalletCommandMethod::PrepareVote { event_id, answers } => { + let data = wallet.prepare_vote(event_id, answers).await?; + Response::PreparedTransaction(PreparedTransactionDataDto::from(&data)) + } + #[cfg(feature = "participation")] + WalletCommandMethod::RegisterParticipationEvents { options } => { + let events = wallet.register_participation_events(&options).await?; + Response::ParticipationEvents(events) + } + WalletCommandMethod::ReissueTransactionUntilIncluded { + transaction_id, + interval, + max_attempts, + } => { + let block_id = wallet + .reissue_transaction_until_included(&transaction_id, interval, max_attempts) + .await?; + Response::BlockId(block_id) + } + WalletCommandMethod::Send { + amount, + address, + options, + } => { + let transaction = wallet.send(amount, address, options).await?; + Response::SentTransaction(TransactionWithMetadataDto::from(&transaction)) + } + WalletCommandMethod::SendWithParams { params, options } => { + let transaction = wallet.send_with_params(params, options).await?; + Response::SentTransaction(TransactionWithMetadataDto::from(&transaction)) + } + WalletCommandMethod::SendOutputs { outputs, options } => { + let token_supply = wallet.client().get_token_supply().await?; + let transaction = wallet + .send_outputs( + outputs + .into_iter() + .map(|o| Ok(Output::try_from_dto_with_params(o, token_supply)?)) + .collect::>>()?, + options, + ) + .await?; + Response::SentTransaction(TransactionWithMetadataDto::from(&transaction)) + } + WalletCommandMethod::SetAlias { alias } => { + wallet.set_alias(&alias).await?; + Response::Ok + } + WalletCommandMethod::SetDefaultSyncOptions { options } => { + wallet.set_default_sync_options(options).await?; + Response::Ok + } + WalletCommandMethod::SignAndSubmitTransaction { + prepared_transaction_data, + } => { + let transaction = wallet + .sign_and_submit_transaction( + PreparedTransactionData::try_from_dto_with_params( + prepared_transaction_data, + wallet.client().get_protocol_parameters().await?, + )?, + None, + ) + .await?; + Response::SentTransaction(TransactionWithMetadataDto::from(&transaction)) + } + WalletCommandMethod::SignTransaction { + prepared_transaction_data, + } => { + let signed_transaction_data = wallet + .sign_transaction(&PreparedTransactionData::try_from_dto(prepared_transaction_data)?) + .await?; + Response::SignedTransactionData(SignedTransactionDataDto::from(&signed_transaction_data)) + } + WalletCommandMethod::SubmitAndStoreTransaction { + signed_transaction_data, + } => { + let signed_transaction_data = SignedTransactionData::try_from_dto_with_params( + signed_transaction_data, + wallet.client().get_protocol_parameters().await?, + )?; + let transaction = wallet + .submit_and_store_transaction(signed_transaction_data, None) + .await?; + Response::SentTransaction(TransactionWithMetadataDto::from(&transaction)) + } + WalletCommandMethod::Sync { options } => Response::Balance(wallet.sync(options).await?), + WalletCommandMethod::Transactions => { + let transactions = wallet.transactions().await; + Response::Transactions(transactions.iter().map(TransactionWithMetadataDto::from).collect()) + } + WalletCommandMethod::UnspentOutputs { filter_options } => { + let outputs = wallet.unspent_outputs(filter_options).await?; + Response::OutputsData(outputs.iter().map(OutputDataDto::from).collect()) + } + }; + Ok(response) +} diff --git a/bindings/core/src/response.rs b/bindings/core/src/response.rs index 9493c4f956..7a67f1076d 100644 --- a/bindings/core/src/response.rs +++ b/bindings/core/src/response.rs @@ -33,16 +33,16 @@ use iota_sdk::{ BlockId, SignedBlockDto, UnsignedBlockDto, }, }, - wallet::account::{ - types::{AddressWithUnspentOutputs, Balance, Bip44Address, OutputDataDto, TransactionWithMetadataDto}, - AccountDetailsDto, PreparedCreateNativeTokenTransactionDto, + wallet::{ + types::{Balance, OutputDataDto, TransactionWithMetadataDto}, + PreparedCreateNativeTokenTransactionDto, }, }; use serde::Serialize; #[cfg(feature = "participation")] use { iota_sdk::types::api::plugins::participation::types::{ParticipationEventId, ParticipationEventStatus}, - iota_sdk::wallet::account::{AccountParticipationOverview, ParticipationEventWithNodes}, + iota_sdk::wallet::{ParticipationEventWithNodes, ParticipationOverview}, std::collections::HashMap, }; @@ -198,8 +198,8 @@ pub enum Response { /// - [`BuildBasicOutput`](crate::method::ClientMethod::BuildBasicOutput) /// - [`BuildFoundryOutput`](crate::method::ClientMethod::BuildFoundryOutput) /// - [`BuildNftOutput`](crate::method::ClientMethod::BuildNftOutput) - /// - [`GetFoundryOutput`](crate::method::AccountMethod::GetFoundryOutput) - /// - [`PrepareOutput`](crate::method::AccountMethod::PrepareOutput) + /// - [`GetFoundryOutput`](crate::method::WalletCommandMethod::GetFoundryOutput) + /// - [`PrepareOutput`](crate::method::WalletCommandMethod::PrepareOutput) Output(OutputDto), /// Response for: /// - [`AccountIdToBech32`](crate::method::ClientMethod::AccountIdToBech32) @@ -221,7 +221,7 @@ pub enum Response { /// - [`BlockId`](crate::method::UtilsMethod::BlockId) /// - [`PostBlock`](crate::method::ClientMethod::PostBlock) /// - [`PostBlockRaw`](crate::method::ClientMethod::PostBlockRaw) - /// - [`ReissueTransactionUntilIncluded`](crate::method::AccountMethod::ReissueTransactionUntilIncluded) + /// - [`ReissueTransactionUntilIncluded`](crate::method::WalletCommandMethod::ReissueTransactionUntilIncluded) BlockId(BlockId), /// Response for: /// - [`GetHealth`](crate::method::ClientMethod::GetHealth) @@ -233,12 +233,12 @@ pub enum Response { /// - [`Backup`](crate::method::WalletMethod::Backup), /// - [`ClearListeners`](crate::method::WalletMethod::ClearListeners) /// - [`ClearStrongholdPassword`](crate::method::WalletMethod::ClearStrongholdPassword), - /// - [`DeregisterParticipationEvent`](crate::method::AccountMethod::DeregisterParticipationEvent), + /// - [`DeregisterParticipationEvent`](crate::method::WalletCommandMethod::DeregisterParticipationEvent), /// - [`EmitTestEvent`](crate::method::WalletMethod::EmitTestEvent), /// - [`RestoreBackup`](crate::method::WalletMethod::RestoreBackup), - /// - [`SetAlias`](crate::method::AccountMethod::SetAlias), + /// - [`SetAlias`](crate::method::WalletCommandMethod::SetAlias), /// - [`SetClientOptions`](crate::method::WalletMethod::SetClientOptions), - /// - [`SetDefaultSyncOptions`](crate::method::AccountMethod::SetDefaultSyncOptions), + /// - [`SetDefaultSyncOptions`](crate::method::WalletCommandMethod::SetDefaultSyncOptions), /// - [`SetStrongholdPassword`](crate::method::WalletMethod::SetStrongholdPassword), /// - [`SetStrongholdPasswordClearInterval`](crate::method::WalletMethod::SetStrongholdPasswordClearInterval), /// - [`StartBackgroundSync`](crate::method::WalletMethod::StartBackgroundSync), @@ -252,109 +252,93 @@ pub enum Response { // wallet responses /// Response for: - /// - [`CreateAccount`](crate::method::WalletMethod::CreateAccount), - /// - [`GetAccount`](crate::method::WalletMethod::GetAccount) - Account(AccountDetailsDto), - /// Response for: - /// - [`GetAccountIndexes`](crate::method::WalletMethod::GetAccountIndexes) - AccountIndexes(Vec), - /// Response for: - /// - [`GetAccounts`](crate::method::WalletMethod::GetAccounts) - Accounts(Vec), - /// Response for: - /// - [`Addresses`](crate::method::AccountMethod::Addresses) - Addresses(Vec), - /// Response for: - /// - [`AddressesWithUnspentOutputs`](crate::method::AccountMethod::AddressesWithUnspentOutputs) - AddressesWithUnspentOutputs(Vec), + /// - [`GetAddress`](crate::method::WalletCommandMethod::GetAddress) + Address(Bech32Address), /// Response for: /// - [`MinimumRequiredStorageDeposit`](crate::method::ClientMethod::MinimumRequiredStorageDeposit) /// - [`ComputeStorageDeposit`](crate::method::UtilsMethod::ComputeStorageDeposit) MinimumRequiredStorageDeposit(String), /// Response for: - /// - [`ClaimableOutputs`](crate::method::AccountMethod::ClaimableOutputs) + /// - [`ClaimableOutputs`](crate::method::WalletCommandMethod::ClaimableOutputs) OutputIds(Vec), /// Response for: - /// - [`GetOutput`](crate::method::AccountMethod::GetOutput) + /// - [`GetOutput`](crate::method::WalletCommandMethod::GetOutput) OutputData(Option>), /// Response for: - /// - [`Outputs`](crate::method::AccountMethod::Outputs), - /// - [`UnspentOutputs`](crate::method::AccountMethod::UnspentOutputs) + /// - [`Outputs`](crate::method::WalletCommandMethod::Outputs), + /// - [`UnspentOutputs`](crate::method::WalletCommandMethod::UnspentOutputs) OutputsData(Vec), /// Response for: - /// - [`PrepareBurn`](crate::method::AccountMethod::PrepareBurn), - /// - [`PrepareConsolidateOutputs`](crate::method::AccountMethod::PrepareConsolidateOutputs) - /// - [`PrepareCreateAccountOutput`](crate::method::AccountMethod::PrepareCreateAccountOutput) - /// - [`PrepareDecreaseVotingPower`](crate::method::AccountMethod::PrepareDecreaseVotingPower) - /// - [`PrepareIncreaseVotingPower`](crate::method::AccountMethod::PrepareIncreaseVotingPower) - /// - [`PrepareMeltNativeToken`](crate::method::AccountMethod::PrepareMeltNativeToken) - /// - [`PrepareMintNativeToken`](crate::method::AccountMethod::PrepareMintNativeToken), - /// - [`PrepareMintNfts`](crate::method::AccountMethod::PrepareMintNfts), - /// - [`PrepareSend`](crate::method::AccountMethod::PrepareSend), - /// - [`PrepareSendNativeTokens`](crate::method::AccountMethod::PrepareSendNativeTokens), - /// - [`PrepareSendNft`](crate::method::AccountMethod::PrepareSendNft), - /// - [`PrepareStopParticipating`](crate::method::AccountMethod::PrepareStopParticipating) - /// - [`PrepareTransaction`](crate::method::AccountMethod::PrepareTransaction) - /// - [`PrepareVote`](crate::method::AccountMethod::PrepareVote) + /// - [`PrepareBurn`](crate::method::WalletCommandMethod::PrepareBurn), + /// - [`PrepareConsolidateOutputs`](crate::method::WalletCommandMethod::PrepareConsolidateOutputs) + /// - [`PrepareCreateAccountOutput`](crate::method::WalletCommandMethod::PrepareCreateAccountOutput) + /// - [`PrepareDecreaseVotingPower`](crate::method::WalletCommandMethod::PrepareDecreaseVotingPower) + /// - [`PrepareIncreaseVotingPower`](crate::method::WalletCommandMethod::PrepareIncreaseVotingPower) + /// - [`PrepareMeltNativeToken`](crate::method::WalletCommandMethod::PrepareMeltNativeToken) + /// - [`PrepareMintNativeToken`](crate::method::WalletCommandMethod::PrepareMintNativeToken), + /// - [`PrepareMintNfts`](crate::method::WalletCommandMethod::PrepareMintNfts), + /// - [`PrepareSend`](crate::method::WalletCommandMethod::PrepareSend), + /// - [`PrepareSendNativeTokens`](crate::method::WalletCommandMethod::PrepareSendNativeTokens), + /// - [`PrepareSendNft`](crate::method::WalletCommandMethod::PrepareSendNft), + /// - [`PrepareStopParticipating`](crate::method::WalletCommandMethod::PrepareStopParticipating) + /// - [`PrepareTransaction`](crate::method::WalletCommandMethod::PrepareTransaction) + /// - [`PrepareVote`](crate::method::WalletCommandMethod::PrepareVote) PreparedTransaction(PreparedTransactionDataDto), /// Response for: - /// - [`PrepareCreateNativeToken`](crate::method::AccountMethod::PrepareCreateNativeToken), + /// - [`PrepareCreateNativeToken`](crate::method::WalletCommandMethod::PrepareCreateNativeToken), PreparedCreateNativeTokenTransaction(PreparedCreateNativeTokenTransactionDto), /// Response for: - /// - [`GetIncomingTransaction`](crate::method::AccountMethod::GetIncomingTransaction) - /// - [`GetTransaction`](crate::method::AccountMethod::GetTransaction), + /// - [`GetIncomingTransaction`](crate::method::WalletCommandMethod::GetIncomingTransaction) + /// - [`GetTransaction`](crate::method::WalletCommandMethod::GetTransaction), Transaction(Option>), /// Response for: - /// - [`IncomingTransactions`](crate::method::AccountMethod::IncomingTransactions) - /// - [`PendingTransactions`](crate::method::AccountMethod::PendingTransactions), - /// - [`Transactions`](crate::method::AccountMethod::Transactions), + /// - [`IncomingTransactions`](crate::method::WalletCommandMethod::IncomingTransactions) + /// - [`PendingTransactions`](crate::method::WalletCommandMethod::PendingTransactions), + /// - [`Transactions`](crate::method::WalletCommandMethod::Transactions), Transactions(Vec), /// Response for: - /// - [`SignTransaction`](crate::method::AccountMethod::SignTransaction) + /// - [`SignTransaction`](crate::method::WalletCommandMethod::SignTransaction) SignedTransactionData(SignedTransactionDataDto), /// Response for: - /// - [`GenerateEd25519Addresses`](crate::method::AccountMethod::GenerateEd25519Addresses) - GeneratedAccountAddresses(Vec), - /// Response for: - /// - [`GetBalance`](crate::method::AccountMethod::GetBalance), - /// - [`Sync`](crate::method::AccountMethod::Sync) + /// - [`GetBalance`](crate::method::WalletCommandMethod::GetBalance), + /// - [`Sync`](crate::method::WalletCommandMethod::Sync) Balance(Balance), /// Response for: - /// - [`ClaimOutputs`](crate::method::AccountMethod::ClaimOutputs) - /// - [`Send`](crate::method::AccountMethod::Send) - /// - [`SendOutputs`](crate::method::AccountMethod::SendOutputs) - /// - [`SignAndSubmitTransaction`](crate::method::AccountMethod::SignAndSubmitTransaction) - /// - [`SubmitAndStoreTransaction`](crate::method::AccountMethod::SubmitAndStoreTransaction) + /// - [`ClaimOutputs`](crate::method::WalletCommandMethod::ClaimOutputs) + /// - [`Send`](crate::method::WalletCommandMethod::Send) + /// - [`SendOutputs`](crate::method::WalletCommandMethod::SendOutputs) + /// - [`SignAndSubmitTransaction`](crate::method::WalletCommandMethod::SignAndSubmitTransaction) + /// - [`SubmitAndStoreTransaction`](crate::method::WalletCommandMethod::SubmitAndStoreTransaction) SentTransaction(TransactionWithMetadataDto), /// Response for: - /// - [`GetParticipationEvent`](crate::method::AccountMethod::GetParticipationEvent) + /// - [`GetParticipationEvent`](crate::method::WalletCommandMethod::GetParticipationEvent) #[cfg(feature = "participation")] #[cfg_attr(docsrs, doc(cfg(feature = "participation")))] ParticipationEvent(Option), /// Response for: - /// - [`GetParticipationEventIds`](crate::method::AccountMethod::GetParticipationEventIds) + /// - [`GetParticipationEventIds`](crate::method::WalletCommandMethod::GetParticipationEventIds) #[cfg(feature = "participation")] #[cfg_attr(docsrs, doc(cfg(feature = "participation")))] ParticipationEventIds(Vec), /// Response for: - /// - [`GetParticipationEventStatus`](crate::method::AccountMethod::GetParticipationEventStatus) + /// - [`GetParticipationEventStatus`](crate::method::WalletCommandMethod::GetParticipationEventStatus) #[cfg(feature = "participation")] #[cfg_attr(docsrs, doc(cfg(feature = "participation")))] ParticipationEventStatus(ParticipationEventStatus), /// Response for: - /// - [`GetParticipationEvents`](crate::method::AccountMethod::GetParticipationEvents) - /// - [`RegisterParticipationEvents`](crate::method::AccountMethod::RegisterParticipationEvents) + /// - [`GetParticipationEvents`](crate::method::WalletCommandMethod::GetParticipationEvents) + /// - [`RegisterParticipationEvents`](crate::method::WalletCommandMethod::RegisterParticipationEvents) #[cfg(feature = "participation")] #[cfg_attr(docsrs, doc(cfg(feature = "participation")))] ParticipationEvents(HashMap), /// Response for: - /// - [`GetVotingPower`](crate::method::AccountMethod::GetVotingPower) + /// - [`GetVotingPower`](crate::method::WalletCommandMethod::GetVotingPower) #[cfg(feature = "participation")] #[cfg_attr(docsrs, doc(cfg(feature = "participation")))] VotingPower(String), /// Response for: - /// - [`GetParticipationOverview`](crate::method::AccountMethod::GetParticipationOverview) + /// - [`GetParticipationOverview`](crate::method::WalletCommandMethod::GetParticipationOverview) #[cfg(feature = "participation")] #[cfg_attr(docsrs, doc(cfg(feature = "participation")))] - AccountParticipationOverview(AccountParticipationOverview), + ParticipationOverview(ParticipationOverview), } diff --git a/bindings/core/tests/combined.rs b/bindings/core/tests/combined.rs index 7ad2e68b48..7025bf713e 100644 --- a/bindings/core/tests/combined.rs +++ b/bindings/core/tests/combined.rs @@ -17,17 +17,16 @@ use iota_sdk::{ }, TryFromDto, }, - wallet::account::types::AccountIdentifier, }; use iota_sdk_bindings_core::{ - call_client_method, call_secret_manager_method, AccountMethod, CallMethod, ClientMethod, Response, Result, - SecretManagerMethod, WalletMethod, WalletOptions, + call_client_method, call_secret_manager_method, CallMethod, ClientMethod, Response, Result, SecretManagerMethod, + WalletCommandMethod, WalletMethod, WalletOptions, }; use pretty_assertions::assert_eq; #[tokio::test] -async fn create_account() -> Result<()> { - let storage_path = "test-storage/create_account"; +async fn create_wallet() -> Result<()> { + let storage_path = "test-storage/create_wallet"; std::fs::remove_dir_all(storage_path).ok(); let secret_manager = r#"{"Mnemonic":"about solution utility exist rail budget vacuum major survey clerk pave ankle wealth gym gossip still medal expect strong rely amazing inspire lazy lunar"}"#; @@ -44,33 +43,14 @@ async fn create_account() -> Result<()> { let wallet = WalletOptions::default() .with_storage_path(storage_path.to_string()) .with_client_options(ClientBuilder::new().from_json(client_options).unwrap()) - .with_coin_type(SHIMMER_COIN_TYPE) + .with_bip_path(Bip44::new(SHIMMER_COIN_TYPE)) .with_secret_manager(serde_json::from_str::(secret_manager).unwrap()) .build() .await?; - // create an account let response = wallet - .call_method(WalletMethod::CreateAccount { - alias: None, - bech32_hrp: None, - addresses: None, - }) - .await; - - match response { - Response::Account(account) => { - assert_eq!(account.index, 0); - let id = account.index; - println!("Created account index: {id}") - } - _ => panic!("unexpected response {response:?}"), - } - - let response = wallet - .call_method(WalletMethod::CallAccountMethod { - account_id: AccountIdentifier::Index(0), - method: AccountMethod::UnspentOutputs { filter_options: None }, + .call_method(WalletMethod::CallMethod { + method: WalletCommandMethod::UnspentOutputs { filter_options: None }, }) .await; @@ -83,114 +63,6 @@ async fn create_account() -> Result<()> { Ok(()) } -#[tokio::test] -async fn verify_accounts() -> Result<()> { - let storage_path = "test-storage/verify_accounts"; - std::fs::remove_dir_all(storage_path).ok(); - - let secret_manager = r#"{"Mnemonic":"about solution utility exist rail budget vacuum major survey clerk pave ankle wealth gym gossip still medal expect strong rely amazing inspire lazy lunar"}"#; - let client_options = r#"{ - "nodes":[ - { - "url":"http://localhost:14265", - "auth":null, - "disabled":false - } - ] - }"#; - - let wallet = WalletOptions::default() - .with_storage_path(storage_path.to_string()) - .with_client_options(ClientBuilder::new().from_json(client_options).unwrap()) - .with_coin_type(SHIMMER_COIN_TYPE) - .with_secret_manager(serde_json::from_str::(secret_manager).unwrap()) - .build() - .await?; - - let mut account_details = BTreeMap::new(); - let mut handle_response = |response| match response { - Response::Account(account) => { - account_details.insert(account.index, account); - } - _ => panic!("unexpected response {response:?}"), - }; - - // Create a few accounts - for alias in ["Alice", "Bob", "Roger", "Denise", "Farquad", "Pikachu"] { - handle_response( - wallet - .call_method(WalletMethod::CreateAccount { - alias: Some(alias.to_owned()), - bech32_hrp: None, - addresses: None, - }) - .await, - ); - } - - // Remove latest account - match wallet.call_method(WalletMethod::RemoveLatestAccount).await { - Response::Ok => {} - response => panic!("unexpected response {response:?}"), - } - - account_details.pop_last(); - - // Get individual account details - for account in account_details.values() { - // By Index - match wallet - .call_method(WalletMethod::GetAccount { - account_id: account.index.into(), - }) - .await - { - Response::Account(details) => { - assert_eq!(&account_details[&details.index], &details); - } - response => panic!("unexpected response {response:?}"), - } - - // By Name - match wallet - .call_method(WalletMethod::GetAccount { - account_id: account.alias.as_str().into(), - }) - .await - { - Response::Account(details) => { - assert_eq!(&account_details[&details.index], &details); - } - response => panic!("unexpected response {response:?}"), - } - } - - // Get account details - match wallet.call_method(WalletMethod::GetAccounts).await { - Response::Accounts(details) => { - assert_eq!(account_details.len(), details.len()); - for detail in details { - assert_eq!(&account_details[&detail.index], &detail); - } - } - response => panic!("unexpected response {response:?}"), - } - - // Get account indexes - match wallet.call_method(WalletMethod::GetAccountIndexes).await { - Response::AccountIndexes(indexes) => { - assert_eq!(account_details.len(), indexes.len()); - for index in indexes { - assert!(account_details.contains_key(&index)); - } - } - response => panic!("unexpected response {response:?}"), - } - - std::fs::remove_dir_all(storage_path).ok(); - Ok(()) -} - #[tokio::test] async fn client_from_wallet() -> Result<()> { let storage_path = "test-storage/client_from_wallet"; @@ -210,32 +82,14 @@ async fn client_from_wallet() -> Result<()> { let wallet = WalletOptions::default() .with_storage_path(storage_path.to_string()) .with_client_options(ClientBuilder::new().from_json(client_options).unwrap()) - .with_coin_type(SHIMMER_COIN_TYPE) + .with_bip_path(Bip44::new(SHIMMER_COIN_TYPE)) .with_secret_manager(serde_json::from_str::(secret_manager).unwrap()) .build() .await?; - // create an account - let response = wallet - .call_method(WalletMethod::CreateAccount { - alias: None, - bech32_hrp: None, - addresses: None, - }) - .await; - - match response { - Response::Account(account) => { - assert_eq!(account.index, 0); - let id = account.index; - println!("Created account index: {id}") - } - _ => panic!("unexpected response {response:?}"), - } - // TODO reenable // // Send ClientMethod via the client from the wallet - // let response = wallet.get_accounts().await?[0] + // let response = wallet // .client() // .call_method(ClientMethod::GetHealth) // .await; diff --git a/bindings/core/tests/secrets_debug.rs b/bindings/core/tests/secrets_debug.rs index 79e35d6052..25fdc1438c 100644 --- a/bindings/core/tests/secrets_debug.rs +++ b/bindings/core/tests/secrets_debug.rs @@ -26,6 +26,6 @@ fn method_interface_secrets_debug() { let wallet_options = WalletOptions::default().with_secret_manager(SecretManagerDto::Placeholder); assert_eq!( format!("{:?}", wallet_options), - "WalletOptions { storage_path: None, client_options: None, coin_type: None, secret_manager: Some() }" + "WalletOptions { storage_path: None, client_options: None, bip_path: None, secret_manager: Some() }" ); } diff --git a/bindings/core/tests/serialize_error.rs b/bindings/core/tests/serialize_error.rs index 472044f86b..67c599b53f 100644 --- a/bindings/core/tests/serialize_error.rs +++ b/bindings/core/tests/serialize_error.rs @@ -1,20 +1,37 @@ // Copyright 2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use iota_sdk::{client::Error as ClientError, wallet::Error as WalletError}; +use crypto::keys::bip44::Bip44; +use iota_sdk::{ + client::{constants::SHIMMER_COIN_TYPE, Error as ClientError}, + wallet::Error as WalletError, +}; use iota_sdk_bindings_core::Error; use pretty_assertions::assert_eq; #[test] fn custom_error_serialization() { + // testing a unit-type-like error let error = Error::Client(ClientError::HealthyNodePoolEmpty); assert_eq!( serde_json::to_string(&error).unwrap(), "{\"type\":\"client\",\"error\":\"no healthy node available\"}" ); - let error = Error::Wallet(WalletError::AccountNotFound("Alice".to_string())); + + // testing a tuple-like error + let error = Error::Wallet(WalletError::InvalidMnemonic("nilly willy".to_string())); + assert_eq!( + serde_json::to_string(&error).unwrap(), + "{\"type\":\"wallet\",\"error\":\"invalid mnemonic: nilly willy\"}" + ); + + // testing a struct-like error + let error = Error::Wallet(WalletError::BipPathMismatch { + old_bip_path: None, + new_bip_path: Some(Bip44::new(SHIMMER_COIN_TYPE)), + }); assert_eq!( serde_json::to_string(&error).unwrap(), - "{\"type\":\"wallet\",\"error\":\"account Alice not found\"}" + "{\"type\":\"wallet\",\"error\":\"BIP44 mismatch: Some(Bip44 { coin_type: 4219, account: 0, change: 0, address_index: 0 }), existing bip path is: None\"}" ); } diff --git a/bindings/nodejs/examples/.env.example b/bindings/nodejs/examples/.env.example index cacb4f080e..ef36979f75 100644 --- a/bindings/nodejs/examples/.env.example +++ b/bindings/nodejs/examples/.env.example @@ -6,7 +6,7 @@ # Mnemonics (Don't ever use them to manage real funds!) MNEMONIC="endorse answer radar about source reunion marriage tag sausage weekend frost daring base attack because joke dream slender leisure group reason prepare broken river" MNEMONIC_2="width scatter jaguar sponsor erosion enable cave since ancient first garden royal luggage exchange ritual exotic play wall clinic ride autumn divert spin exchange" -# The Wallet database folder used to store account data +# The Wallet database folder used to store wallet data WALLET_DB_PATH="./example-walletdb" # The Stronghold snapshot file location used to store secrets STRONGHOLD_SNAPSHOT_PATH="./example.stronghold" diff --git a/bindings/nodejs/lib/wallet/wallet.ts b/bindings/nodejs/lib/wallet/wallet.ts index be7951086f..2e4605768e 100644 --- a/bindings/nodejs/lib/wallet/wallet.ts +++ b/bindings/nodejs/lib/wallet/wallet.ts @@ -259,8 +259,7 @@ export class Wallet { * stored, it will be gone. * if ignore_if_coin_type_mismatch is provided client options will not be restored * if ignore_if_coin_type_mismatch == true, client options coin type and accounts will not be restored if the cointype doesn't match - * if ignore_if_bech32_hrp_mismatch == Some("rms"), but addresses have something different like "smr", no accounts - * will be restored. + * If a bech32 hrp is provided to ignore_if_bech32_hrp_mismatch, that doesn't match the one of the current address, the wallet will not be restored. */ async restoreBackup( source: string, diff --git a/bindings/python/examples/.env.example b/bindings/python/examples/.env.example index cacb4f080e..ef36979f75 100644 --- a/bindings/python/examples/.env.example +++ b/bindings/python/examples/.env.example @@ -6,7 +6,7 @@ # Mnemonics (Don't ever use them to manage real funds!) MNEMONIC="endorse answer radar about source reunion marriage tag sausage weekend frost daring base attack because joke dream slender leisure group reason prepare broken river" MNEMONIC_2="width scatter jaguar sponsor erosion enable cave since ancient first garden royal luggage exchange ritual exotic play wall clinic ride autumn divert spin exchange" -# The Wallet database folder used to store account data +# The Wallet database folder used to store wallet data WALLET_DB_PATH="./example-walletdb" # The Stronghold snapshot file location used to store secrets STRONGHOLD_SNAPSHOT_PATH="./example.stronghold" diff --git a/cli/src/account.rs b/cli/src/account.rs deleted file mode 100644 index 70c2bff6a8..0000000000 --- a/cli/src/account.rs +++ /dev/null @@ -1,235 +0,0 @@ -// Copyright 2020-2022 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use clap::Parser; -use colored::Colorize; -use iota_sdk::wallet::{Account, Wallet}; -use rustyline::{error::ReadlineError, history::MemHistory, Config, Editor}; - -use crate::{ - command::{ - account::{ - addresses_command, balance_command, burn_native_token_command, burn_nft_command, claim_command, - claimable_outputs_command, consolidate_command, create_account_output_command, create_native_token_command, - decrease_voting_power_command, destroy_account_command, destroy_foundry_command, faucet_command, - increase_voting_power_command, melt_native_token_command, mint_native_token, mint_nft_command, - new_address_command, node_info_command, output_command, outputs_command, participation_overview_command, - send_command, send_native_token_command, send_nft_command, stop_participating_command, sync_command, - transaction_command, transactions_command, unspent_outputs_command, vote_command, voting_output_command, - voting_power_command, AccountCli, AccountCommand, - }, - account_completion::AccountPromptHelper, - }, - error::Error, - helper::bytes_from_hex_or_file, - println_log_error, -}; - -// loop on the account prompt -pub async fn account_prompt(wallet: &Wallet, mut account: Account) -> Result<(), Error> { - let config = Config::builder() - .auto_add_history(true) - .history_ignore_space(true) - .completion_type(rustyline::CompletionType::List) - .edit_mode(rustyline::EditMode::Emacs) - .build(); - - let mut rl = Editor::with_history(config, MemHistory::with_config(config))?; - rl.set_helper(Some(AccountPromptHelper::default())); - - loop { - match account_prompt_internal(wallet, &account, &mut rl).await { - Ok(res) => match res { - AccountPromptResponse::Reprompt => (), - AccountPromptResponse::Done => { - return Ok(()); - } - AccountPromptResponse::Switch(new_account) => { - account = new_account; - } - }, - Err(e) => { - println_log_error!("{e}"); - } - } - } -} - -pub enum AccountPromptResponse { - Reprompt, - Done, - Switch(Account), -} - -// loop on the account prompt -pub async fn account_prompt_internal( - wallet: &Wallet, - account: &Account, - rl: &mut Editor, -) -> Result { - let alias = account.details().await.alias().clone(); - let prompt = format!("Account \"{alias}\": "); - - if let Some(helper) = rl.helper_mut() { - helper.set_prompt(prompt.green().to_string()); - } - - let input = rl.readline(&prompt); - - match input { - Ok(command) => { - match command.trim() { - "" => {} - "h" | "help" => AccountCli::print_help()?, - "c" | "clear" => { - // Clear console - let _ = std::process::Command::new("clear").status(); - } - "accounts" => { - // List all accounts - let accounts = wallet.get_accounts().await?; - println!("INDEX\tALIAS"); - for account in accounts { - let details = &*account.details().await; - println!("{}\t{}", details.index(), details.alias()); - } - } - _ => { - // Prepend `Account: ` so the parsing will be correct - let command = format!("Account: {command}"); - let account_cli = match AccountCli::try_parse_from(command.split_whitespace()) { - Ok(account_cli) => account_cli, - Err(err) => { - println!("{err}"); - return Ok(AccountPromptResponse::Reprompt); - } - }; - match account_cli.command { - AccountCommand::Addresses => addresses_command(account).await, - AccountCommand::Balance { addresses } => balance_command(account, addresses).await, - AccountCommand::BurnNativeToken { token_id, amount } => { - burn_native_token_command(account, token_id, amount).await - } - AccountCommand::BurnNft { nft_id } => burn_nft_command(account, nft_id).await, - AccountCommand::Claim { output_id } => claim_command(account, output_id).await, - AccountCommand::ClaimableOutputs => claimable_outputs_command(account).await, - AccountCommand::Consolidate => consolidate_command(account).await, - AccountCommand::CreateAccountOutput => create_account_output_command(account).await, - AccountCommand::CreateNativeToken { - circulating_supply, - maximum_supply, - foundry_metadata_hex, - foundry_metadata_file, - } => { - create_native_token_command( - account, - circulating_supply, - maximum_supply, - bytes_from_hex_or_file(foundry_metadata_hex, foundry_metadata_file).await?, - ) - .await - } - AccountCommand::DestroyAccount { account_id } => { - destroy_account_command(account, account_id).await - } - AccountCommand::DestroyFoundry { foundry_id } => { - destroy_foundry_command(account, foundry_id).await - } - AccountCommand::Exit => { - return Ok(AccountPromptResponse::Done); - } - AccountCommand::Faucet { address, url } => faucet_command(account, address, url).await, - AccountCommand::MeltNativeToken { token_id, amount } => { - melt_native_token_command(account, token_id, amount).await - } - AccountCommand::MintNativeToken { token_id, amount } => { - mint_native_token(account, token_id, amount).await - } - AccountCommand::MintNft { - address, - immutable_metadata_hex, - immutable_metadata_file, - metadata_hex, - metadata_file, - tag, - sender, - issuer, - } => { - mint_nft_command( - account, - address, - bytes_from_hex_or_file(immutable_metadata_hex, immutable_metadata_file).await?, - bytes_from_hex_or_file(metadata_hex, metadata_file).await?, - tag, - sender, - issuer, - ) - .await - } - AccountCommand::NewAddress => new_address_command(account).await, - AccountCommand::NodeInfo => node_info_command(account).await, - AccountCommand::Output { selector } => output_command(account, selector).await, - AccountCommand::Outputs => outputs_command(account).await, - AccountCommand::Send { - address, - amount, - return_address, - expiration, - allow_micro_amount, - } => { - let allow_micro_amount = if return_address.is_some() || expiration.is_some() { - true - } else { - allow_micro_amount - }; - send_command(account, address, amount, return_address, expiration, allow_micro_amount).await - } - AccountCommand::SendNativeToken { - address, - token_id, - amount, - gift_storage_deposit, - } => send_native_token_command(account, address, token_id, amount, gift_storage_deposit).await, - AccountCommand::SendNft { address, nft_id } => send_nft_command(account, address, nft_id).await, - AccountCommand::Switch { account_id } => { - return Ok(AccountPromptResponse::Switch(wallet.get_account(account_id).await?)); - } - AccountCommand::Sync => sync_command(account).await, - AccountCommand::Transaction { selector } => transaction_command(account, selector).await, - AccountCommand::Transactions { show_details } => { - transactions_command(account, show_details).await - } - AccountCommand::UnspentOutputs => unspent_outputs_command(account).await, - AccountCommand::Vote { event_id, answers } => vote_command(account, event_id, answers).await, - AccountCommand::StopParticipating { event_id } => { - stop_participating_command(account, event_id).await - } - AccountCommand::ParticipationOverview { event_ids } => { - let event_ids = (!event_ids.is_empty()).then_some(event_ids); - participation_overview_command(account, event_ids).await - } - AccountCommand::VotingPower => voting_power_command(account).await, - AccountCommand::IncreaseVotingPower { amount } => { - increase_voting_power_command(account, amount).await - } - AccountCommand::DecreaseVotingPower { amount } => { - decrease_voting_power_command(account, amount).await - } - AccountCommand::VotingOutput => voting_output_command(account).await, - } - .unwrap_or_else(|err| { - println_log_error!("{err}"); - }); - } - } - } - Err(ReadlineError::Interrupted) => { - return Ok(AccountPromptResponse::Done); - } - Err(err) => { - println_log_error!("{err}"); - } - } - - Ok(AccountPromptResponse::Reprompt) -} diff --git a/cli/src/command/wallet.rs b/cli/src/cli.rs similarity index 64% rename from cli/src/command/wallet.rs rename to cli/src/cli.rs index 0ba4410799..92aa7b4764 100644 --- a/cli/src/command/wallet.rs +++ b/cli/src/cli.rs @@ -1,7 +1,7 @@ // Copyright 2020-2022 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use std::path::Path; +use std::{path::Path, str::FromStr}; use clap::{builder::BoolishValueParser, Args, CommandFactory, Parser, Subcommand}; use iota_sdk::{ @@ -11,13 +11,18 @@ use iota_sdk::{ stronghold::StrongholdAdapter, utils::Password, }, - wallet::{account::types::AccountIdentifier, ClientOptions, Wallet}, + crypto::keys::bip44::Bip44, + types::block::address::Bech32Address, + wallet::{ClientOptions, Wallet}, }; use log::LevelFilter; use crate::{ error::Error, - helper::{check_file_exists, enter_or_generate_mnemonic, generate_mnemonic, get_password, import_mnemonic}, + helper::{ + check_file_exists, enter_or_generate_mnemonic, generate_mnemonic, get_alias, get_decision, get_password, + import_mnemonic, + }, println_log_error, println_log_info, }; @@ -28,33 +33,82 @@ const DEFAULT_WALLET_DATABASE_PATH: &str = "./stardust-cli-wallet-db"; #[derive(Debug, Clone, Parser)] #[command(author, version, about, long_about = None, propagate_version = true)] -pub struct WalletCli { +pub struct Cli { /// Set the path to the wallet database. #[arg(long, value_name = "PATH", env = "WALLET_DATABASE_PATH", default_value = DEFAULT_WALLET_DATABASE_PATH)] pub wallet_db_path: String, /// Set the path to the stronghold snapshot file. #[arg(long, value_name = "PATH", env = "STRONGHOLD_SNAPSHOT_PATH", default_value = DEFAULT_STRONGHOLD_SNAPSHOT_PATH)] pub stronghold_snapshot_path: String, - /// Set the account to enter. - pub account: Option, /// Set the log level. #[arg(short, long, default_value = DEFAULT_LOG_LEVEL)] pub log_level: LevelFilter, #[command(subcommand)] - pub command: Option, + pub command: Option, } -impl WalletCli { +impl Cli { pub fn print_help() -> Result<(), Error> { Self::command().bin_name("wallet").print_help()?; Ok(()) } } +#[derive(Debug, Clone, Args)] +pub struct InitParameters { + /// Set the path to a file containing mnemonics. If empty, a mnemonic has to be entered or will be randomly + /// generated. + #[arg(short, long, value_name = "PATH")] + pub mnemonic_file_path: Option, + /// Set the node to connect to with this wallet. + #[arg(short, long, value_name = "URL", env = "NODE_URL", default_value = DEFAULT_NODE_URL)] + pub node_url: String, + /// Set the BIP path, `4219/0/0/0` if not provided. + #[arg(short, long, value_parser = parse_bip_path)] + pub bip_path: Option, + /// Set the Bech32-encoded wallet address. + #[arg(short, long)] + pub address: Option, +} + +impl Default for InitParameters { + fn default() -> Self { + Self { + mnemonic_file_path: None, + node_url: DEFAULT_NODE_URL.to_string(), + bip_path: Some(Bip44::new(SHIMMER_COIN_TYPE)), + address: None, + } + } +} + +fn parse_bip_path(arg: &str) -> Result { + let mut bip_path_enc = Vec::with_capacity(4); + for p in arg.split_terminator('/').map(|p| p.trim()) { + match p.parse::() { + Ok(value) => bip_path_enc.push(value), + Err(_) => { + return Err(format!("cannot parse BIP path: {p}")); + } + } + } + + if bip_path_enc.len() != 4 { + return Err( + "invalid BIP path format. Expected: `coin_type/account_index/change_address/address_index`".to_string(), + ); + } + + let bip_path = Bip44::new(bip_path_enc[0]) + .with_account(bip_path_enc[1]) + .with_change(bip_path_enc[2]) + .with_address_index(bip_path_enc[3]); + + Ok(bip_path) +} + #[derive(Debug, Clone, Subcommand)] -pub enum WalletCommand { - /// List all accounts. - Accounts, +pub enum CliCommand { /// Create a stronghold backup file. Backup { /// Path of the created stronghold backup file. @@ -78,11 +132,6 @@ pub enum WalletCommand { #[arg(long, num_args = 0..=1, default_missing_value = Some("true"), value_parser = BoolishValueParser::new())] output_stdout: Option, }, - /// Create a new account. - NewAccount { - /// Account alias, next available account index if not provided. - alias: Option, - }, /// Get information about currently set node. NodeInfo, /// Restore a stronghold backup file. @@ -95,46 +144,84 @@ pub enum WalletCommand { /// Node URL to use for all future operations. url: String, }, - /// Synchronize all accounts. + /// Synchronize wallet. Sync, } -#[derive(Debug, Clone, Args)] -pub struct InitParameters { - /// Set the path to a file containing mnemonics. If empty, a mnemonic has to be entered or will be randomly - /// generated. - #[arg(short, long, value_name = "PATH")] - pub mnemonic_file_path: Option, - /// Set the node to connect to with this wallet. - #[arg(short, long, value_name = "URL", env = "NODE_URL", default_value = DEFAULT_NODE_URL)] - pub node_url: String, - /// Coin type, SHIMMER_COIN_TYPE (4219) if not provided. - #[arg(short, long, default_value_t = SHIMMER_COIN_TYPE)] - pub coin_type: u32, -} - -impl Default for InitParameters { - fn default() -> Self { - Self { - mnemonic_file_path: None, - node_url: DEFAULT_NODE_URL.to_string(), - coin_type: SHIMMER_COIN_TYPE, +pub async fn new_wallet(cli: Cli) -> Result, Error> { + let storage_path = Path::new(&cli.wallet_db_path); + let snapshot_path = Path::new(&cli.stronghold_snapshot_path); + + Ok(if let Some(command) = cli.command { + match command { + CliCommand::Backup { backup_path } => { + backup_command(storage_path, snapshot_path, std::path::Path::new(&backup_path)).await?; + None + } + CliCommand::ChangePassword => { + let wallet = change_password_command(storage_path, snapshot_path).await?; + Some(wallet) + } + CliCommand::Init(init_parameters) => { + let wallet = init_command(storage_path, snapshot_path, init_parameters).await?; + Some(wallet) + } + CliCommand::MigrateStrongholdSnapshotV2ToV3 { path } => { + migrate_stronghold_snapshot_v2_to_v3_command(path).await?; + None + } + CliCommand::Mnemonic { + output_file_name, + output_stdout, + } => { + mnemonic_command(output_file_name, output_stdout).await?; + None + } + CliCommand::NodeInfo => { + node_info_command(storage_path).await?; + None + } + CliCommand::Restore { backup_path } => { + let wallet = restore_command(storage_path, snapshot_path, std::path::Path::new(&backup_path)).await?; + Some(wallet) + } + CliCommand::SetNodeUrl { url } => { + let wallet = set_node_url_command(storage_path, snapshot_path, url).await?; + Some(wallet) + } + CliCommand::Sync => { + let wallet = sync_command(storage_path, snapshot_path).await?; + Some(wallet) + } } - } -} - -pub async fn accounts_command(storage_path: &Path, snapshot_path: &Path) -> Result<(), Error> { - let password = get_password("Stronghold password", false)?; - let wallet = unlock_wallet(storage_path, snapshot_path, password).await?; - let accounts = wallet.get_accounts().await?; - - println!("INDEX\tALIAS"); - for account in accounts { - let details = &*account.details().await; - println!("{}\t{}", details.index(), details.alias()); - } - - Ok(()) + } else { + // no command provided, i.e. `> ./wallet` + match (storage_path.exists(), snapshot_path.exists()) { + (true, true) => { + let password = get_password("Stronghold password", false)?; + let wallet = unlock_wallet(storage_path, snapshot_path, password).await?; + Some(wallet) + } + (false, false) => { + if get_decision("Create a new wallet with default parameters?")? { + let wallet = init_command(storage_path, snapshot_path, InitParameters::default()).await?; + println_log_info!("Created new wallet."); + Some(wallet) + } else { + Cli::print_help()?; + None + } + } + (true, false) => { + println_log_error!("Stronghold snapshot not found at '{}'.", snapshot_path.display()); + None + } + (false, true) => { + println_log_error!("Wallet database not found at '{}'.", storage_path.display()); + None + } + } + }) } pub async fn backup_command(storage_path: &Path, snapshot_path: &Path, backup_path: &Path) -> Result<(), Error> { @@ -161,7 +248,7 @@ pub async fn change_password_command(storage_path: &Path, snapshot_path: &Path) pub async fn init_command( storage_path: &Path, snapshot_path: &Path, - parameters: InitParameters, + init_params: InitParameters, ) -> Result { if storage_path.exists() { return Err(Error::Miscellaneous(format!( @@ -176,7 +263,7 @@ pub async fn init_command( ))); } let password = get_password("Stronghold password", true)?; - let mnemonic = match parameters.mnemonic_file_path { + let mnemonic = match init_params.mnemonic_file_path { Some(path) => import_mnemonic(&path).await?, None => enter_or_generate_mnemonic().await?, }; @@ -187,11 +274,24 @@ pub async fn init_command( secret_manager.store_mnemonic(mnemonic).await?; let secret_manager = SecretManager::Stronghold(secret_manager); + let alias = if get_decision("Do you want to assign an alias to your wallet?")? { + Some(get_alias("New wallet alias").await?) + } else { + None + }; + + let address = init_params + .address + .map(|addr| Bech32Address::from_str(&addr)) + .transpose()?; + Ok(Wallet::builder() .with_secret_manager(secret_manager) - .with_client_options(ClientOptions::new().with_node(parameters.node_url.as_str())?) + .with_client_options(ClientOptions::new().with_node(init_params.node_url.as_str())?) .with_storage_path(storage_path.to_str().expect("invalid unicode")) - .with_coin_type(parameters.coin_type) + .with_bip_path(init_params.bip_path) + .with_address(address) + .with_alias(alias) .finish() .await?) } @@ -213,19 +313,6 @@ pub async fn mnemonic_command(output_file_name: Option, output_stdout: O Ok(()) } -pub async fn new_account_command( - storage_path: &Path, - snapshot_path: &Path, - alias: Option, -) -> Result<(Wallet, AccountIdentifier), Error> { - let password = get_password("Stronghold password", !snapshot_path.exists())?; - let wallet = unlock_wallet(storage_path, snapshot_path, password).await?; - - let alias = add_account(&wallet, alias).await?; - - Ok((wallet, alias)) -} - pub async fn node_info_command(storage_path: &Path) -> Result { let wallet = unlock_wallet(storage_path, None, None).await?; let node_info = wallet.client().get_info().await?; @@ -258,7 +345,7 @@ pub async fn restore_command(storage_path: &Path, snapshot_path: &Path, backup_p .with_client_options(ClientOptions::new().with_node(DEFAULT_NODE_URL)?) .with_storage_path(storage_path.to_str().expect("invalid unicode")) // Will be overwritten by the backup's value. - .with_coin_type(SHIMMER_COIN_TYPE) + .with_bip_path(Bip44::new(SHIMMER_COIN_TYPE)) .finish() .await?; @@ -286,7 +373,7 @@ pub async fn sync_command(storage_path: &Path, snapshot_path: &Path) -> Result) -> Result { - let mut account_builder = wallet.create_account(); - - if let Some(alias) = alias { - account_builder = account_builder.with_alias(alias); - } - - let account = account_builder.finish().await?; - let alias = AccountIdentifier::Alias(account.details().await.alias().clone()); - - println_log_info!("Created account \"{alias}\""); - - Ok(alias) -} diff --git a/cli/src/command/mod.rs b/cli/src/command/mod.rs deleted file mode 100644 index 503b26a487..0000000000 --- a/cli/src/command/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright 2020-2022 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -pub mod account; -pub mod account_completion; -pub mod wallet; diff --git a/cli/src/error.rs b/cli/src/error.rs index 76e9ef309a..ce716ff35b 100644 --- a/cli/src/error.rs +++ b/cli/src/error.rs @@ -24,8 +24,6 @@ pub enum Error { Logger(#[from] LoggerError), #[error("{0}")] Miscellaneous(String), - #[error("generate at least one address before using the faucet")] - NoAddressForFaucet, #[error("prompt error: {0}")] Prompt(#[from] ReadlineError), #[error("serde_json error: {0}")] diff --git a/cli/src/helper.rs b/cli/src/helper.rs index 5be5970080..bba612103c 100644 --- a/cli/src/helper.rs +++ b/cli/src/helper.rs @@ -8,7 +8,6 @@ use dialoguer::{console::Term, theme::ColorfulTheme, Input, Select}; use iota_sdk::{ client::{utils::Password, verify_mnemonic}, crypto::keys::bip39::Mnemonic, - wallet::{Account, Wallet}, }; use tokio::{ fs::{self, OpenOptions}, @@ -49,41 +48,17 @@ pub fn get_decision(prompt: &str) -> Result { } } -pub async fn get_account_alias(prompt: &str, wallet: &Wallet) -> Result { - let account_aliases = wallet.get_account_aliases().await?; +pub async fn get_alias(prompt: &str) -> Result { loop { let input = Input::::new().with_prompt(prompt).interact_text()?; if input.is_empty() || !input.is_ascii() { println_log_error!("Invalid input, please choose a non-empty alias consisting of ASCII characters."); - } else if account_aliases.iter().any(|alias| alias == &input) { - println_log_error!("Account '{input}' already exists, please choose another alias."); } else { return Ok(input); } } } -pub async fn pick_account(wallet: &Wallet) -> Result, Error> { - let mut accounts = wallet.get_accounts().await?; - - match accounts.len() { - 0 => Ok(None), - 1 => Ok(Some(accounts.swap_remove(0))), - _ => { - // fetch all available account aliases to display to the user - let account_aliases = wallet.get_account_aliases().await?; - - let index = Select::with_theme(&ColorfulTheme::default()) - .with_prompt("Select an account:") - .items(&account_aliases) - .default(0) - .interact_on(&Term::stderr())?; - - Ok(Some(accounts.swap_remove(index))) - } - } -} - pub async fn bytes_from_hex_or_file(hex: Option, file: Option) -> Result>, Error> { Ok(if let Some(hex) = hex { Some(prefix_hex::decode(hex).map_err(|e| Error::Miscellaneous(e.to_string()))?) @@ -152,10 +127,10 @@ pub async fn generate_mnemonic( println_log_info!("Mnemonic has been written to '{file_path}'."); } - println_log_info!("IMPORTANT:"); - println_log_info!("Store this mnemonic in a secure location!"); - println_log_info!( - "It is the only way to recover your account if you ever forget your password and/or lose the stronghold file." + println!("IMPORTANT:"); + println!("Store this mnemonic in a secure location!"); + println!( + "It is the only way to recover your wallet if you ever forget your password and/or lose the stronghold file." ); Ok(mnemonic) diff --git a/cli/src/main.rs b/cli/src/main.rs index ea0336e05b..df31b1d5c8 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,16 +1,18 @@ // Copyright 2020-2022 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -mod account; -mod command; +mod cli; mod error; mod helper; -mod wallet; +mod wallet_cli; use clap::Parser; use fern_logger::{LoggerConfigBuilder, LoggerOutputConfigBuilder}; -use self::{command::wallet::WalletCli, error::Error, wallet::new_wallet}; +use self::{ + cli::{new_wallet, Cli}, + error::Error, +}; #[macro_export] macro_rules! println_log_info { @@ -28,7 +30,7 @@ macro_rules! println_log_error { }; } -fn logger_init(cli: &WalletCli) -> Result<(), Error> { +fn logger_init(cli: &Cli) -> Result<(), Error> { std::panic::set_hook(Box::new(move |panic_info| { println_log_error!("{panic_info}"); })); @@ -45,10 +47,9 @@ fn logger_init(cli: &WalletCli) -> Result<(), Error> { Ok(()) } -async fn run(cli: WalletCli) -> Result<(), Error> { - if let (Some(wallet), Some(account)) = new_wallet(cli).await? { - let account = wallet.get_account(account).await?; - account::account_prompt(&wallet, account).await?; +async fn run(cli: Cli) -> Result<(), Error> { + if let Some(wallet) = new_wallet(cli).await? { + wallet_cli::prompt(&wallet).await?; } Ok(()) @@ -58,7 +59,7 @@ async fn run(cli: WalletCli) -> Result<(), Error> { async fn main() { dotenvy::dotenv().ok(); - let cli = match WalletCli::try_parse() { + let cli = match Cli::try_parse() { Ok(cli) => cli, Err(e) => { println!("{e}"); diff --git a/cli/src/wallet.rs b/cli/src/wallet.rs deleted file mode 100644 index e8ce5bdf4f..0000000000 --- a/cli/src/wallet.rs +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright 2020-2022 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use std::path::Path; - -use iota_sdk::wallet::{account::types::AccountIdentifier, Wallet}; - -use crate::{ - command::wallet::{ - accounts_command, add_account, backup_command, change_password_command, init_command, - migrate_stronghold_snapshot_v2_to_v3_command, mnemonic_command, new_account_command, node_info_command, - restore_command, set_node_url_command, sync_command, unlock_wallet, InitParameters, WalletCli, WalletCommand, - }, - error::Error, - helper::{get_account_alias, get_decision, get_password, pick_account}, - println_log_error, println_log_info, -}; - -pub async fn new_wallet(cli: WalletCli) -> Result<(Option, Option), Error> { - let storage_path = Path::new(&cli.wallet_db_path); - let snapshot_path = Path::new(&cli.stronghold_snapshot_path); - - let (wallet, account_id) = if let Some(command) = cli.command { - match command { - WalletCommand::Accounts => { - accounts_command(storage_path, snapshot_path).await?; - return Ok((None, None)); - } - WalletCommand::Init(init_parameters) => { - let wallet = init_command(storage_path, snapshot_path, init_parameters).await?; - (Some(wallet), None) - } - WalletCommand::Restore { backup_path } => { - let wallet = restore_command(storage_path, snapshot_path, std::path::Path::new(&backup_path)).await?; - (Some(wallet), None) - } - WalletCommand::Backup { backup_path } => { - backup_command(storage_path, snapshot_path, std::path::Path::new(&backup_path)).await?; - return Ok((None, None)); - } - WalletCommand::ChangePassword => { - let wallet = change_password_command(storage_path, snapshot_path).await?; - (Some(wallet), None) - } - WalletCommand::MigrateStrongholdSnapshotV2ToV3 { path } => { - migrate_stronghold_snapshot_v2_to_v3_command(path).await?; - return Ok((None, None)); - } - WalletCommand::NewAccount { alias } => { - let (wallet, account) = new_account_command(storage_path, snapshot_path, alias).await?; - (Some(wallet), Some(account)) - } - WalletCommand::SetNodeUrl { url } => { - let wallet = set_node_url_command(storage_path, snapshot_path, url).await?; - (Some(wallet), None) - } - WalletCommand::Sync => { - let wallet = sync_command(storage_path, snapshot_path).await?; - (Some(wallet), None) - } - WalletCommand::Mnemonic { - output_file_name, - output_stdout, - } => { - mnemonic_command(output_file_name, output_stdout).await?; - return Ok((None, None)); - } - WalletCommand::NodeInfo => { - node_info_command(storage_path).await?; - return Ok((None, None)); - } - } - } else { - // no command provided, i.e. `> ./wallet` - match (storage_path.exists(), snapshot_path.exists()) { - (true, true) => { - let password = get_password("Stronghold password", false)?; - let wallet = unlock_wallet(storage_path, snapshot_path, password).await?; - if wallet.get_accounts().await?.is_empty() { - create_initial_account(wallet).await? - } else if let Some(alias) = cli.account { - (Some(wallet), Some(alias)) - } else if let Some(account) = pick_account(&wallet).await? { - (Some(wallet), Some(account.alias().await.into())) - } else { - (Some(wallet), None) - } - } - (false, false) => { - if get_decision("Create a new wallet with default parameters?")? { - let wallet = init_command(storage_path, snapshot_path, InitParameters::default()).await?; - println_log_info!("Created new wallet."); - create_initial_account(wallet).await? - } else { - WalletCli::print_help()?; - (None, None) - } - } - (true, false) => { - println_log_error!("Stronghold snapshot not found at '{}'.", snapshot_path.display()); - (None, None) - } - (false, true) => { - println_log_error!("Wallet database not found at '{}'.", storage_path.display()); - (None, None) - } - } - }; - Ok((wallet, account_id)) -} - -async fn create_initial_account(wallet: Wallet) -> Result<(Option, Option), Error> { - // Ask the user whether an initial account should be created. - if get_decision("Create initial account?")? { - let alias = get_account_alias("New account alias", &wallet).await?; - let account_id = add_account(&wallet, Some(alias)).await?; - println_log_info!("Created initial account.\nType `help` to see all available account commands."); - Ok((Some(wallet), Some(account_id))) - } else { - Ok((Some(wallet), None)) - } -} diff --git a/cli/src/command/account_completion.rs b/cli/src/wallet_cli/completer.rs similarity index 83% rename from cli/src/command/account_completion.rs rename to cli/src/wallet_cli/completer.rs index 53a78c1261..ea2abb4748 100644 --- a/cli/src/command/account_completion.rs +++ b/cli/src/wallet_cli/completer.rs @@ -8,12 +8,8 @@ use rustyline::{ completion::Completer, highlight::Highlighter, hint::HistoryHinter, Completer, Context, Helper, Hinter, Validator, }; -#[derive(Default)] -pub struct AccountCompleter; - -const ACCOUNT_COMMANDS: &[&str] = &[ - "accounts", - "addresses", +const WALLET_COMMANDS: &[&str] = &[ + "address", "balance", "burn-native-token", "burn-nft", @@ -30,14 +26,12 @@ const ACCOUNT_COMMANDS: &[&str] = &[ "melt-native-token", "mint-native-token", "mint-nft", - "new-address", "node-info", "output", "outputs", "send", "send-native-token", "send-nft", - "switch", "sync", "transaction", "transactions", @@ -54,7 +48,10 @@ const ACCOUNT_COMMANDS: &[&str] = &[ "help", ]; -impl Completer for AccountCompleter { +#[derive(Default)] +pub struct WalletCommandCompleter; + +impl Completer for WalletCommandCompleter { type Candidate = &'static str; fn complete( @@ -65,7 +62,7 @@ impl Completer for AccountCompleter { ) -> rustyline::Result<(usize, Vec)> { Ok(( 0, - ACCOUNT_COMMANDS + WALLET_COMMANDS .iter() .filter_map(|cmd| cmd.starts_with(input).then_some(*cmd)) .collect(), @@ -74,21 +71,21 @@ impl Completer for AccountCompleter { } #[derive(Helper, Completer, Hinter, Validator)] -pub struct AccountPromptHelper { +pub struct WalletCommandHelper { #[rustyline(Completer)] - completer: AccountCompleter, + completer: WalletCommandCompleter, #[rustyline(Hinter)] hinter: HistoryHinter, prompt: String, } -impl AccountPromptHelper { +impl WalletCommandHelper { pub fn set_prompt(&mut self, prompt: String) { self.prompt = prompt; } } -impl Highlighter for AccountPromptHelper { +impl Highlighter for WalletCommandHelper { fn highlight_prompt<'b, 's: 'b, 'p: 'b>(&'s self, prompt: &'p str, default: bool) -> Cow<'b, str> { if default { Cow::Borrowed(&self.prompt) @@ -102,10 +99,10 @@ impl Highlighter for AccountPromptHelper { } } -impl Default for AccountPromptHelper { +impl Default for WalletCommandHelper { fn default() -> Self { Self { - completer: AccountCompleter, + completer: WalletCommandCompleter, hinter: HistoryHinter {}, prompt: String::new(), } diff --git a/cli/src/command/account.rs b/cli/src/wallet_cli/mod.rs similarity index 59% rename from cli/src/command/account.rs rename to cli/src/wallet_cli/mod.rs index e3934256d4..71b73dcb23 100644 --- a/cli/src/command/account.rs +++ b/cli/src/wallet_cli/mod.rs @@ -1,9 +1,12 @@ // Copyright 2020-2022 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +mod completer; + use std::str::FromStr; use clap::{CommandFactory, Parser, Subcommand}; +use colored::Colorize; use iota_sdk::{ client::request_funds_from_faucet, types::{ @@ -20,41 +23,43 @@ use iota_sdk::{ }, }, wallet::{ - account::{ - types::{AccountIdentifier, Bip44Address, OutputData, TransactionWithMetadata}, - Account, ConsolidationParams, OutputsToClaim, SyncOptions, TransactionOptions, - }, - CreateNativeTokenParams, MintNftParams, SendNativeTokensParams, SendNftParams, SendParams, + types::{OutputData, TransactionWithMetadata}, + ConsolidationParams, CreateNativeTokenParams, MintNftParams, OutputsToClaim, SendNativeTokensParams, + SendNftParams, SendParams, SyncOptions, TransactionOptions, Wallet, }, U256, }; +use rustyline::{error::ReadlineError, history::MemHistory, Config, Editor}; -use crate::{error::Error, helper::to_utc_date_time, println_log_info}; +use self::completer::WalletCommandHelper; +use crate::{ + error::Error, + helper::{bytes_from_hex_or_file, to_utc_date_time}, + println_log_error, println_log_info, +}; #[derive(Debug, Parser)] #[command(author, version, about, long_about = None, propagate_version = true)] -pub struct AccountCli { +pub struct WalletCli { #[command(subcommand)] - pub command: AccountCommand, + pub command: WalletCommand, } -impl AccountCli { +impl WalletCli { pub fn print_help() -> Result<(), Error> { - Self::command().bin_name("Account:").print_help()?; + Self::command().bin_name("Wallet:").print_help()?; Ok(()) } } +/// Commands #[derive(Debug, Subcommand)] #[allow(clippy::large_enum_variant)] -pub enum AccountCommand { - /// List the account addresses. - Addresses, - /// Print the account balance. - Balance { - /// Addresses to compute the balance for. - addresses: Option>, - }, +pub enum WalletCommand { + /// Print the wallet address. + Address, + /// Print the wallet balance. + Balance, /// Burn an amount of native token. BurnNativeToken { /// Token ID to be burnt, e.g. 0x087d205988b733d97fb145ae340e27a8b19554d1ceee64574d7e5ff66c45f69e7a0100000000. @@ -106,7 +111,7 @@ pub enum AccountCommand { Exit, /// Request funds from the faucet. Faucet { - /// Address the faucet sends the funds to, defaults to the latest address. + /// Address the faucet sends the funds to, defaults to the wallet address. address: Option, /// URL of the faucet, default to . url: Option, @@ -152,8 +157,6 @@ pub enum AccountCommand { /// Amount to be melted, e.g. 100. amount: String, }, - /// Generate a new address. - NewAddress, /// Get information about currently set node. NodeInfo, /// Display an output. @@ -172,7 +175,7 @@ pub enum AccountCommand { amount: u64, /// Bech32 encoded return address, to which the storage deposit will be returned if one is necessary /// given the provided amount. If a storage deposit is needed and a return address is not provided, it will - /// default to the first address of the account. + /// default to the wallet address. #[arg(long)] return_address: Option, /// Expiration in slot indices, after which the output will be available for the sender again, if not spent by @@ -205,12 +208,7 @@ pub enum AccountCommand { /// NFT ID to be sent, e.g. 0xecadf10e6545aa82da4df2dfd2a496b457c8850d2cab49b7464cb273d3dffb07. nft_id: String, }, - /// Switch to a different account. - Switch { - /// The identifier (alias or index) of the account you want to switch to. - account_id: AccountIdentifier, - }, - /// Synchronize the account. + /// Synchronize the wallet. Sync, /// Show the details of a transaction. #[clap(visible_alias = "tx")] @@ -219,14 +217,14 @@ pub enum AccountCommand { /// Either by ID (e.g. 0x84fe6b1796bddc022c9bc40206f0a692f4536b02aa8c13140264e2e01a3b7e4b) or index. selector: TransactionSelector, }, - /// List the account transactions. + /// List the wallet transactions. #[clap(visible_alias = "txs")] Transactions { - /// List account transactions with all details. + /// List wallet transactions with all details. #[arg(long, default_value_t = false)] show_details: bool, }, - /// List the account unspent outputs. + /// List the unspent outputs. UnspentOutputs, /// Cast votes for an event. Vote { @@ -241,26 +239,26 @@ pub enum AccountCommand { /// 0xdc049a721dc65ec342f836c876ec15631ed915cd55213cee39e8d1c821c751f2. event_id: ParticipationEventId, }, - /// Get the participation overview of the account. + /// Get the participation overview of the wallet. ParticipationOverview { /// Event IDs for which to get the participation overview, e.g. /// 0xdc049a721dc65ec342f836c876ec15631ed915cd55213cee39e8d1c821c751f2... #[arg(short, long, num_args = 1.., value_delimiter = ' ')] event_ids: Vec, }, - /// Get the voting power of the account. + /// Get the voting power of the wallet. VotingPower, - /// Increase the voting power of the account. + /// Increase the voting power of the wallet. IncreaseVotingPower { /// Amount to increase the voting power by, e.g. 100. amount: u64, }, - /// Decrease the voting power of the account. + /// Decrease the voting power of the wallet. DecreaseVotingPower { /// Amount to decrease the voting power by, e.g. 100. amount: u64, }, - /// Get the voting output of the account. + /// Get the voting output of the wallet. VotingOutput, } @@ -302,38 +300,26 @@ impl FromStr for OutputSelector { } } -/// `addresses` command -pub async fn addresses_command(account: &Account) -> Result<(), Error> { - let addresses = account.addresses().await; - - if addresses.is_empty() { - println_log_info!("No addresses found"); - } else { - for address in addresses { - print_address(account, &address).await?; - } - } +// `address` command +pub async fn address_command(wallet: &Wallet) -> Result<(), Error> { + print_wallet_address(wallet).await?; Ok(()) } // `balance` command -pub async fn balance_command(account: &Account, addresses: Option>) -> Result<(), Error> { - let balance = if let Some(addresses) = addresses { - account.addresses_balance(addresses).await? - } else { - account.balance().await? - }; +pub async fn balance_command(wallet: &Wallet) -> Result<(), Error> { + let balance = wallet.balance().await?; println_log_info!("{balance:#?}"); Ok(()) } // `burn-native-token` command -pub async fn burn_native_token_command(account: &Account, token_id: String, amount: String) -> Result<(), Error> { +pub async fn burn_native_token_command(wallet: &Wallet, token_id: String, amount: String) -> Result<(), Error> { println_log_info!("Burning native token {token_id} {amount}."); - let transaction = account + let transaction = wallet .burn( NativeToken::new( TokenId::from_str(&token_id)?, @@ -353,10 +339,10 @@ pub async fn burn_native_token_command(account: &Account, token_id: String, amou } // `burn-nft` command -pub async fn burn_nft_command(account: &Account, nft_id: String) -> Result<(), Error> { +pub async fn burn_nft_command(wallet: &Wallet, nft_id: String) -> Result<(), Error> { println_log_info!("Burning nft {nft_id}."); - let transaction = account.burn(NftId::from_str(&nft_id)?, None).await?; + let transaction = wallet.burn(NftId::from_str(&nft_id)?, None).await?; println_log_info!( "Burning transaction sent:\n{:?}\n{:?}", @@ -368,11 +354,11 @@ pub async fn burn_nft_command(account: &Account, nft_id: String) -> Result<(), E } // `claim` command -pub async fn claim_command(account: &Account, output_id: Option) -> Result<(), Error> { +pub async fn claim_command(wallet: &Wallet, output_id: Option) -> Result<(), Error> { if let Some(output_id) = output_id { println_log_info!("Claiming output {output_id}"); - let transaction = account.claim_outputs([OutputId::from_str(&output_id)?]).await?; + let transaction = wallet.claim_outputs([OutputId::from_str(&output_id)?]).await?; println_log_info!( "Claiming transaction sent:\n{:?}\n{:?}", @@ -382,7 +368,7 @@ pub async fn claim_command(account: &Account, output_id: Option) -> Resu } else { println_log_info!("Claiming outputs."); - let output_ids = account.claimable_outputs(OutputsToClaim::All).await?; + let output_ids = wallet.claimable_outputs(OutputsToClaim::All).await?; if output_ids.is_empty() { println_log_info!("No outputs available to claim."); @@ -391,7 +377,7 @@ pub async fn claim_command(account: &Account, output_id: Option) -> Resu // Doing chunks of only 60, because we might need to create the double amount of outputs, because of potential // storage deposit return unlock conditions and also consider the remainder output. for output_ids_chunk in output_ids.chunks(60) { - let transaction = account.claim_outputs(output_ids_chunk.to_vec()).await?; + let transaction = wallet.claim_outputs(output_ids_chunk.to_vec()).await?; println_log_info!( "Claiming transaction sent:\n{:?}\n{:?}", transaction.transaction_id, @@ -404,15 +390,15 @@ pub async fn claim_command(account: &Account, output_id: Option) -> Resu } /// `claimable-outputs` command -pub async fn claimable_outputs_command(account: &Account) -> Result<(), Error> { - let balance = account.balance().await?; +pub async fn claimable_outputs_command(wallet: &Wallet) -> Result<(), Error> { + let balance = wallet.balance().await?; for output_id in balance .potentially_locked_outputs() .iter() .filter_map(|(output_id, unlockable)| unlockable.then_some(output_id)) { // Unwrap: for the iterated `OutputId`s this call will always return `Some(...)`. - let output_data = account.get_output(output_id).await.unwrap(); + let output_data = wallet.get_output(output_id).await.unwrap(); let output = output_data.output; let kind = match output { Output::Nft(_) => "Nft", @@ -439,7 +425,7 @@ pub async fn claimable_outputs_command(account: &Account) -> Result<(), Error> { println_log_info!(" - base coin amount: {}", amount); if let Some(expiration) = unlock_conditions.expiration() { - let slot_index = account.client().get_slot_index().await?; + let slot_index = wallet.client().get_slot_index().await?; if *expiration.slot_index() > *slot_index { println_log_info!(" - expires in {} slot indices", *expiration.slot_index() - *slot_index); @@ -457,10 +443,10 @@ pub async fn claimable_outputs_command(account: &Account) -> Result<(), Error> { } // `consolidate` command -pub async fn consolidate_command(account: &Account) -> Result<(), Error> { +pub async fn consolidate_command(wallet: &Wallet) -> Result<(), Error> { println_log_info!("Consolidating outputs."); - let transaction = account + let transaction = wallet .consolidate_outputs(ConsolidationParams::new().with_force(true)) .await?; @@ -474,10 +460,10 @@ pub async fn consolidate_command(account: &Account) -> Result<(), Error> { } // `create-account-output` command -pub async fn create_account_output_command(account: &Account) -> Result<(), Error> { +pub async fn create_account_output_command(wallet: &Wallet) -> Result<(), Error> { println_log_info!("Creating account output."); - let transaction = account.create_account_output(None, None).await?; + let transaction = wallet.create_account_output(None, None).await?; println_log_info!( "Account output creation transaction sent:\n{:?}\n{:?}", @@ -490,24 +476,24 @@ pub async fn create_account_output_command(account: &Account) -> Result<(), Erro // `create-native-token` command pub async fn create_native_token_command( - account: &Account, + wallet: &Wallet, circulating_supply: String, maximum_supply: String, foundry_metadata: Option>, ) -> Result<(), Error> { // If no account output exists, create one first - if account.balance().await?.accounts().is_empty() { - let transaction = account.create_account_output(None, None).await?; + if wallet.balance().await?.accounts().is_empty() { + let transaction = wallet.create_account_output(None, None).await?; println_log_info!( "Account output minting transaction sent:\n{:?}\n{:?}", transaction.transaction_id, transaction.block_id ); - account + wallet .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; - // Sync account after the transaction got confirmed, so the account output is available - account.sync(None).await?; + // Sync wallet after the transaction got confirmed, so the account output is available + wallet.sync(None).await?; } let params = CreateNativeTokenParams { @@ -517,7 +503,7 @@ pub async fn create_native_token_command( foundry_metadata, }; - let create_transaction = account.create_native_token(params, None).await?; + let create_transaction = wallet.create_native_token(params, None).await?; println_log_info!( "Transaction to create native token sent:\n{:?}\n{:?}", @@ -529,10 +515,10 @@ pub async fn create_native_token_command( } // `destroy-account` command -pub async fn destroy_account_command(account: &Account, account_id: String) -> Result<(), Error> { +pub async fn destroy_account_command(wallet: &Wallet, account_id: String) -> Result<(), Error> { println_log_info!("Destroying account {account_id}."); - let transaction = account.burn(AccountId::from_str(&account_id)?, None).await?; + let transaction = wallet.burn(AccountId::from_str(&account_id)?, None).await?; println_log_info!( "Destroying account transaction sent:\n{:?}\n{:?}", @@ -544,10 +530,10 @@ pub async fn destroy_account_command(account: &Account, account_id: String) -> R } // `destroy-foundry` command -pub async fn destroy_foundry_command(account: &Account, foundry_id: String) -> Result<(), Error> { +pub async fn destroy_foundry_command(wallet: &Wallet, foundry_id: String) -> Result<(), Error> { println_log_info!("Destroying foundry {foundry_id}."); - let transaction = account.burn(FoundryId::from_str(&foundry_id)?, None).await?; + let transaction = wallet.burn(FoundryId::from_str(&foundry_id)?, None).await?; println_log_info!( "Destroying foundry transaction sent:\n{:?}\n{:?}", @@ -559,19 +545,13 @@ pub async fn destroy_foundry_command(account: &Account, foundry_id: String) -> R } // `faucet` command -pub async fn faucet_command( - account: &Account, - address: Option, - url: Option, -) -> Result<(), Error> { +pub async fn faucet_command(wallet: &Wallet, address: Option, url: Option) -> Result<(), Error> { let address = if let Some(address) = address { address } else { - match account.addresses().await.into_iter().rev().next() { - Some(address) => address.into_bech32(), - None => return Err(Error::NoAddressForFaucet), - } + wallet.address().await }; + let faucet_url = url .as_deref() .unwrap_or("https://faucet.testnet.shimmer.network/api/enqueue"); @@ -582,8 +562,8 @@ pub async fn faucet_command( } // `melt-native-token` command -pub async fn melt_native_token_command(account: &Account, token_id: String, amount: String) -> Result<(), Error> { - let transaction = account +pub async fn melt_native_token_command(wallet: &Wallet, token_id: String, amount: String) -> Result<(), Error> { + let transaction = wallet .melt_native_token( TokenId::from_str(&token_id)?, U256::from_dec_str(&amount).map_err(|e| Error::Miscellaneous(e.to_string()))?, @@ -601,8 +581,8 @@ pub async fn melt_native_token_command(account: &Account, token_id: String, amou } // `mint-native-token` command -pub async fn mint_native_token(account: &Account, token_id: String, amount: String) -> Result<(), Error> { - let mint_transaction = account +pub async fn mint_native_token(wallet: &Wallet, token_id: String, amount: String) -> Result<(), Error> { + let mint_transaction = wallet .mint_native_token( TokenId::from_str(&token_id)?, U256::from_dec_str(&amount).map_err(|e| Error::Miscellaneous(e.to_string()))?, @@ -621,7 +601,7 @@ pub async fn mint_native_token(account: &Account, token_id: String, amount: Stri // `mint-nft` command pub async fn mint_nft_command( - account: &Account, + wallet: &Wallet, address: Option, immutable_metadata: Option>, metadata: Option>, @@ -642,7 +622,7 @@ pub async fn mint_nft_command( .with_tag(tag) .with_sender(sender) .with_issuer(issuer); - let transaction = account.mint_nfts([nft_options], None).await?; + let transaction = wallet.mint_nfts([nft_options], None).await?; println_log_info!( "NFT minting transaction sent:\n{:?}\n{:?}", @@ -653,18 +633,9 @@ pub async fn mint_nft_command( Ok(()) } -// `new-address` command -pub async fn new_address_command(account: &Account) -> Result<(), Error> { - let address = account.generate_ed25519_addresses(1, None).await?; - - print_address(account, &address[0]).await?; - - Ok(()) -} - // `node-info` command -pub async fn node_info_command(account: &Account) -> Result<(), Error> { - let node_info = account.client().get_info().await?; +pub async fn node_info_command(wallet: &Wallet) -> Result<(), Error> { + let node_info = wallet.client().get_info().await?; println_log_info!("Current node info: {}", serde_json::to_string_pretty(&node_info)?); @@ -672,11 +643,11 @@ pub async fn node_info_command(account: &Account) -> Result<(), Error> { } /// `output` command -pub async fn output_command(account: &Account, selector: OutputSelector) -> Result<(), Error> { +pub async fn output_command(wallet: &Wallet, selector: OutputSelector) -> Result<(), Error> { let output = match selector { - OutputSelector::Id(id) => account.get_output(&id).await, + OutputSelector::Id(id) => wallet.get_output(&id).await, OutputSelector::Index(index) => { - let mut outputs = account.outputs(None).await?; + let mut outputs = wallet.outputs(None).await?; outputs.sort_unstable_by(outputs_ordering); outputs.into_iter().nth(index) } @@ -692,13 +663,13 @@ pub async fn output_command(account: &Account, selector: OutputSelector) -> Resu } /// `outputs` command -pub async fn outputs_command(account: &Account) -> Result<(), Error> { - print_outputs(account.outputs(None).await?, "Outputs:").await +pub async fn outputs_command(wallet: &Wallet) -> Result<(), Error> { + print_outputs(wallet.outputs(None).await?, "Outputs:").await } // `send` command pub async fn send_command( - account: &Account, + wallet: &Wallet, address: impl ConvertTo, amount: u64, return_address: Option>, @@ -708,7 +679,7 @@ pub async fn send_command( let params = [SendParams::new(amount, address)? .with_return_address(return_address.map(ConvertTo::convert).transpose()?) .with_expiration(expiration)]; - let transaction = account + let transaction = wallet .send_with_params( params, TransactionOptions { @@ -729,7 +700,7 @@ pub async fn send_command( // `send-native-token` command pub async fn send_native_token_command( - account: &Account, + wallet: &Wallet, address: impl ConvertTo, token_id: String, amount: String, @@ -738,10 +709,10 @@ pub async fn send_native_token_command( let address = address.convert()?; let transaction = if gift_storage_deposit.unwrap_or(false) { // Send native tokens together with the required storage deposit - let rent_structure = account.client().get_rent_structure().await?; - let token_supply = account.client().get_token_supply().await?; + let rent_structure = wallet.client().get_rent_structure().await?; + let token_supply = wallet.client().get_token_supply().await?; - account.client().bech32_hrp_matches(address.hrp()).await?; + wallet.client().bech32_hrp_matches(address.hrp()).await?; let outputs = [BasicOutputBuilder::new_with_minimum_storage_deposit(rent_structure) .add_unlock_condition(AddressUnlockCondition::new(address)) @@ -751,7 +722,7 @@ pub async fn send_native_token_command( )?]) .finish_output(token_supply)?]; - account.send_outputs(outputs, None).await? + wallet.send_outputs(outputs, None).await? } else { // Send native tokens with storage deposit return and expiration let outputs = [SendNativeTokensParams::new( @@ -761,7 +732,7 @@ pub async fn send_native_token_command( U256::from_dec_str(&amount).map_err(|e| Error::Miscellaneous(e.to_string()))?, )], )?]; - account.send_native_tokens(outputs, None).await? + wallet.send_native_tokens(outputs, None).await? }; println_log_info!( @@ -775,12 +746,12 @@ pub async fn send_native_token_command( // `send-nft` command pub async fn send_nft_command( - account: &Account, + wallet: &Wallet, address: impl ConvertTo, nft_id: String, ) -> Result<(), Error> { let outputs = [SendNftParams::new(address.convert()?, &nft_id)?]; - let transaction = account.send_nft(outputs, None).await?; + let transaction = wallet.send_nft(outputs, None).await?; println_log_info!( "Nft transaction sent:\n{:?}\n{:?}", @@ -792,8 +763,8 @@ pub async fn send_nft_command( } // `sync` command -pub async fn sync_command(account: &Account) -> Result<(), Error> { - let balance = account +pub async fn sync_command(wallet: &Wallet) -> Result<(), Error> { + let balance = wallet .sync(Some(SyncOptions { sync_native_token_foundries: true, ..Default::default() @@ -806,8 +777,8 @@ pub async fn sync_command(account: &Account) -> Result<(), Error> { } /// `transaction` command -pub async fn transaction_command(account: &Account, selector: TransactionSelector) -> Result<(), Error> { - let mut transactions = account.transactions().await; +pub async fn transaction_command(wallet: &Wallet, selector: TransactionSelector) -> Result<(), Error> { + let mut transactions = wallet.transactions().await; let transaction = match selector { TransactionSelector::Id(id) => transactions.into_iter().find(|tx| tx.transaction_id == id), TransactionSelector::Index(index) => { @@ -826,8 +797,8 @@ pub async fn transaction_command(account: &Account, selector: TransactionSelecto } /// `transactions` command -pub async fn transactions_command(account: &Account, show_details: bool) -> Result<(), Error> { - let mut transactions = account.transactions().await; +pub async fn transactions_command(wallet: &Wallet, show_details: bool) -> Result<(), Error> { + let mut transactions = wallet.transactions().await; transactions.sort_unstable_by(transactions_ordering); if transactions.is_empty() { @@ -849,12 +820,12 @@ pub async fn transactions_command(account: &Account, show_details: bool) -> Resu } /// `unspent-outputs` command -pub async fn unspent_outputs_command(account: &Account) -> Result<(), Error> { - print_outputs(account.unspent_outputs(None).await?, "Unspent outputs:").await +pub async fn unspent_outputs_command(wallet: &Wallet) -> Result<(), Error> { + print_outputs(wallet.unspent_outputs(None).await?, "Unspent outputs:").await } -pub async fn vote_command(account: &Account, event_id: ParticipationEventId, answers: Vec) -> Result<(), Error> { - let transaction = account.vote(Some(event_id), Some(answers)).await?; +pub async fn vote_command(wallet: &Wallet, event_id: ParticipationEventId, answers: Vec) -> Result<(), Error> { + let transaction = wallet.vote(Some(event_id), Some(answers)).await?; println_log_info!( "Voting transaction sent:\n{:?}\n{:?}", @@ -865,8 +836,8 @@ pub async fn vote_command(account: &Account, event_id: ParticipationEventId, ans Ok(()) } -pub async fn stop_participating_command(account: &Account, event_id: ParticipationEventId) -> Result<(), Error> { - let transaction = account.stop_participating(event_id).await?; +pub async fn stop_participating_command(wallet: &Wallet, event_id: ParticipationEventId) -> Result<(), Error> { + let transaction = wallet.stop_participating(event_id).await?; println_log_info!( "Stop participating transaction sent:\n{:?}\n{:?}", @@ -878,26 +849,26 @@ pub async fn stop_participating_command(account: &Account, event_id: Participati } pub async fn participation_overview_command( - account: &Account, + wallet: &Wallet, event_ids: Option>, ) -> Result<(), Error> { - let participation_overview = account.get_participation_overview(event_ids).await?; + let participation_overview = wallet.get_participation_overview(event_ids).await?; println_log_info!("Participation overview: {participation_overview:?}"); Ok(()) } -pub async fn voting_power_command(account: &Account) -> Result<(), Error> { - let voting_power = account.get_voting_power().await?; +pub async fn voting_power_command(wallet: &Wallet) -> Result<(), Error> { + let voting_power = wallet.get_voting_power().await?; println_log_info!("Voting power: {voting_power}"); Ok(()) } -pub async fn increase_voting_power_command(account: &Account, amount: u64) -> Result<(), Error> { - let transaction = account.increase_voting_power(amount).await?; +pub async fn increase_voting_power_command(wallet: &Wallet, amount: u64) -> Result<(), Error> { + let transaction = wallet.increase_voting_power(amount).await?; println_log_info!( "Increase voting power transaction sent:\n{:?}\n{:?}", @@ -908,8 +879,8 @@ pub async fn increase_voting_power_command(account: &Account, amount: u64) -> Re Ok(()) } -pub async fn decrease_voting_power_command(account: &Account, amount: u64) -> Result<(), Error> { - let transaction = account.decrease_voting_power(amount).await?; +pub async fn decrease_voting_power_command(wallet: &Wallet, amount: u64) -> Result<(), Error> { + let transaction = wallet.decrease_voting_power(amount).await?; println_log_info!( "Decrease voting power transaction sent:\n{:?}\n{:?}", @@ -920,32 +891,29 @@ pub async fn decrease_voting_power_command(account: &Account, amount: u64) -> Re Ok(()) } -pub async fn voting_output_command(account: &Account) -> Result<(), Error> { - let output = account.get_voting_output().await?; +pub async fn voting_output_command(wallet: &Wallet) -> Result<(), Error> { + let output = wallet.get_voting_output().await?; println_log_info!("Voting output: {output:?}"); Ok(()) } -async fn print_address(account: &Account, address: &Bip44Address) -> Result<(), Error> { +async fn print_wallet_address(wallet: &Wallet) -> Result<(), Error> { + let address = wallet.address().await; + let mut log = format!( - "Address: {}\n{:<9}{}\n{:<9}{:?}", - address.key_index(), + "Address:\n{:<9}{}\n{:<9}{:?}", "Bech32:", - address.address(), + address, "Hex:", - address.address().inner() + address.inner() ); - if *address.internal() { - log = format!("{log}\nChange address"); - } + let unspent_outputs = wallet.unspent_outputs(None).await?; + let slot_index = wallet.client().get_slot_index().await?; - let addresses = account.addresses_with_unspent_outputs().await?; - let slot_index = account.client().get_slot_index().await?; - - let mut output_ids: &[OutputId] = &[]; + let mut output_ids = Vec::new(); let mut amount = 0; let mut native_tokens = NativeTokensBuilder::new(); let mut accounts = Vec::new(); @@ -954,48 +922,43 @@ async fn print_address(account: &Account, address: &Bip44Address) -> Result<(), let mut delegations = Vec::new(); let mut anchors = Vec::new(); - if let Some(address) = addresses - .iter() - .find(|a| a.key_index() == address.key_index() && a.internal() == address.internal()) - { - output_ids = address.output_ids().as_slice(); - - for output_id in output_ids { - if let Some(output_data) = account.get_output(output_id).await { - // Output might be associated with the address, but can't be unlocked by it, so we check that here. - let (required_address, _) = output_data - .output - .required_and_unlocked_address(slot_index, output_id)?; - - if address.address().as_ref() == &required_address { - if let Some(nts) = output_data.output.native_tokens() { - native_tokens.add_native_tokens(nts.clone())?; - } - match &output_data.output { - Output::Basic(_) => {} - Output::Account(account) => accounts.push(account.account_id_non_null(output_id)), - Output::Foundry(foundry) => foundries.push(foundry.id()), - Output::Nft(nft) => nfts.push(nft.nft_id_non_null(output_id)), - Output::Delegation(delegation) => { - delegations.push(delegation.delegation_id_non_null(output_id)) - } - Output::Anchor(anchor) => anchors.push(anchor.anchor_id_non_null(output_id)), - } - let unlock_conditions = output_data - .output - .unlock_conditions() - .expect("output must have unlock conditions"); - let sdr_amount = unlock_conditions - .storage_deposit_return() - .map(|sdr| sdr.amount()) - .unwrap_or(0); - - amount += output_data.output.amount() - sdr_amount; - } + for output_data in unspent_outputs { + let output_id = output_data.output_id; + output_ids.push(output_id); + + // Output might be associated with the address, but can't be unlocked by it, so we check that here. + let (required_address, _) = &output_data + .output + .required_and_unlocked_address(slot_index, &output_id)?; + + if address.inner() == required_address { + if let Some(nts) = output_data.output.native_tokens() { + native_tokens.add_native_tokens(nts.clone())?; + } + match &output_data.output { + Output::Basic(_) => {} + Output::Account(account) => accounts.push(account.account_id_non_null(&output_id)), + Output::Foundry(foundry) => foundries.push(foundry.id()), + Output::Nft(nft) => nfts.push(nft.nft_id_non_null(&output_id)), + Output::Delegation(delegation) => delegations.push(delegation.delegation_id_non_null(&output_id)), + Output::Anchor(anchor) => anchors.push(anchor.anchor_id_non_null(&output_id)), } + let unlock_conditions = output_data + .output + .unlock_conditions() + .expect("output must have unlock conditions"); + let sdr_amount = unlock_conditions + .storage_deposit_return() + .map(|sdr| sdr.amount()) + .unwrap_or(0); + + amount += output_data.output.amount() - sdr_amount; } } + let bip_path = wallet.bip_path().await; + log = format!("{log}\nBIP path: {bip_path:?}"); + log = format!( "{log}\nOutputs: {:#?}\nBase coin amount: {}\nNative Tokens: {:?}\nAccounts: {:?}\nFoundries: {:?}\nNFTs: {:?}\nDelegations: {:?}\nAnchors: {:?}\n", output_ids, @@ -1013,6 +976,198 @@ async fn print_address(account: &Account, address: &Bip44Address) -> Result<(), Ok(()) } +// loop on the wallet prompt +pub async fn prompt(wallet: &Wallet) -> Result<(), Error> { + let config = Config::builder() + .auto_add_history(true) + .history_ignore_space(true) + .completion_type(rustyline::CompletionType::List) + .edit_mode(rustyline::EditMode::Emacs) + .build(); + + let mut rl = Editor::with_history(config, MemHistory::with_config(config))?; + rl.set_helper(Some(WalletCommandHelper::default())); + + loop { + match prompt_internal(wallet, &mut rl).await { + Ok(res) => match res { + PromptResponse::Reprompt => (), + PromptResponse::Done => { + return Ok(()); + } + }, + Err(e) => { + println_log_error!("{e}"); + } + } + } +} + +pub enum PromptResponse { + Reprompt, + Done, +} + +pub async fn prompt_internal( + wallet: &Wallet, + rl: &mut Editor, +) -> Result { + let prompt = if let Some(alias) = wallet.alias().await { + format!("Wallet \"{alias}\": ") + } else { + format!("Wallet: ") + }; + + if let Some(helper) = rl.helper_mut() { + helper.set_prompt(prompt.green().to_string()); + } + + let input = rl.readline(&prompt); + match input { + Ok(command) => { + match command.trim() { + "" => {} + "h" | "help" => WalletCli::print_help()?, + "c" | "clear" => { + // Clear console + let _ = std::process::Command::new("clear").status(); + } + _ => { + // Prepend `Wallet: ` so the parsing will be correct + let command = format!("Wallet: {command}"); + let protocol_cli = match WalletCli::try_parse_from(command.split_whitespace()) { + Ok(protocol_cli) => protocol_cli, + Err(err) => { + println!("{err}"); + return Ok(PromptResponse::Reprompt); + } + }; + match protocol_cli.command { + WalletCommand::Address => address_command(wallet).await, + WalletCommand::Balance => balance_command(wallet).await, + WalletCommand::BurnNativeToken { token_id, amount } => { + burn_native_token_command(wallet, token_id, amount).await + } + WalletCommand::BurnNft { nft_id } => burn_nft_command(wallet, nft_id).await, + WalletCommand::Claim { output_id } => claim_command(wallet, output_id).await, + WalletCommand::ClaimableOutputs => claimable_outputs_command(wallet).await, + WalletCommand::Consolidate => consolidate_command(wallet).await, + WalletCommand::CreateAccountOutput => create_account_output_command(wallet).await, + WalletCommand::CreateNativeToken { + circulating_supply, + maximum_supply, + foundry_metadata_hex, + foundry_metadata_file, + } => { + create_native_token_command( + wallet, + circulating_supply, + maximum_supply, + bytes_from_hex_or_file(foundry_metadata_hex, foundry_metadata_file).await?, + ) + .await + } + WalletCommand::DestroyAccount { account_id } => { + destroy_account_command(wallet, account_id).await + } + WalletCommand::DestroyFoundry { foundry_id } => { + destroy_foundry_command(wallet, foundry_id).await + } + WalletCommand::Exit => { + return Ok(PromptResponse::Done); + } + WalletCommand::Faucet { address, url } => faucet_command(wallet, address, url).await, + WalletCommand::MeltNativeToken { token_id, amount } => { + melt_native_token_command(wallet, token_id, amount).await + } + WalletCommand::MintNativeToken { token_id, amount } => { + mint_native_token(wallet, token_id, amount).await + } + WalletCommand::MintNft { + address, + immutable_metadata_hex, + immutable_metadata_file, + metadata_hex, + metadata_file, + tag, + sender, + issuer, + } => { + mint_nft_command( + wallet, + address, + bytes_from_hex_or_file(immutable_metadata_hex, immutable_metadata_file).await?, + bytes_from_hex_or_file(metadata_hex, metadata_file).await?, + tag, + sender, + issuer, + ) + .await + } + WalletCommand::NodeInfo => node_info_command(wallet).await, + WalletCommand::Output { selector } => output_command(wallet, selector).await, + WalletCommand::Outputs => outputs_command(wallet).await, + WalletCommand::Send { + address, + amount, + return_address, + expiration, + allow_micro_amount, + } => { + let allow_micro_amount = if return_address.is_some() || expiration.is_some() { + true + } else { + allow_micro_amount + }; + send_command(wallet, address, amount, return_address, expiration, allow_micro_amount).await + } + WalletCommand::SendNativeToken { + address, + token_id, + amount, + gift_storage_deposit, + } => send_native_token_command(wallet, address, token_id, amount, gift_storage_deposit).await, + WalletCommand::SendNft { address, nft_id } => send_nft_command(wallet, address, nft_id).await, + WalletCommand::Sync => sync_command(wallet).await, + WalletCommand::Transaction { selector } => transaction_command(wallet, selector).await, + WalletCommand::Transactions { show_details } => { + transactions_command(wallet, show_details).await + } + WalletCommand::UnspentOutputs => unspent_outputs_command(wallet).await, + WalletCommand::Vote { event_id, answers } => vote_command(wallet, event_id, answers).await, + WalletCommand::StopParticipating { event_id } => { + stop_participating_command(wallet, event_id).await + } + WalletCommand::ParticipationOverview { event_ids } => { + let event_ids = (!event_ids.is_empty()).then_some(event_ids); + participation_overview_command(wallet, event_ids).await + } + WalletCommand::VotingPower => voting_power_command(wallet).await, + WalletCommand::IncreaseVotingPower { amount } => { + increase_voting_power_command(wallet, amount).await + } + WalletCommand::DecreaseVotingPower { amount } => { + decrease_voting_power_command(wallet, amount).await + } + WalletCommand::VotingOutput => voting_output_command(wallet).await, + } + .unwrap_or_else(|err| { + println_log_error!("{err}"); + }); + } + } + } + Err(ReadlineError::Interrupted) => { + return Ok(PromptResponse::Done); + } + Err(err) => { + println_log_error!("{err}"); + } + } + + Ok(PromptResponse::Reprompt) +} + async fn print_outputs(mut outputs: Vec, title: &str) -> Result<(), Error> { if outputs.is_empty() { println_log_info!("No outputs found"); diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index 9937b02621..0b83ce34c8 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -221,13 +221,8 @@ path = "examples/how_tos/accounts_and_addresses/create_mnemonic.rs" required-features = ["client"] [[example]] -name = "create_account" -path = "examples/how_tos/accounts_and_addresses/create_account.rs" -required-features = ["rocksdb", "stronghold"] - -[[example]] -name = "list_accounts" -path = "examples/how_tos/accounts_and_addresses/list_accounts.rs" +name = "create_wallet" +path = "examples/how_tos/accounts_and_addresses/create_wallet.rs" required-features = ["rocksdb", "stronghold"] [[example]] @@ -235,16 +230,6 @@ name = "check_balance" path = "examples/how_tos/accounts_and_addresses/check_balance.rs" required-features = ["rocksdb", "stronghold"] -[[example]] -name = "list_addresses" -path = "examples/how_tos/accounts_and_addresses/list_addresses.rs" -required-features = ["rocksdb", "stronghold"] - -[[example]] -name = "create_address" -path = "examples/how_tos/accounts_and_addresses/create_address.rs" -required-features = ["rocksdb", "stronghold"] - [[example]] name = "list_transactions" path = "examples/how_tos/accounts_and_addresses/list_transactions.rs" @@ -588,8 +573,8 @@ path = "examples/wallet/getting_started.rs" required-features = ["stronghold"] [[example]] -name = "0_generate_addresses" -path = "examples/wallet/offline_signing/0_generate_addresses.rs" +name = "0_generate_address" +path = "examples/wallet/offline_signing/0_generate_address.rs" required-features = ["wallet", "storage", "stronghold"] [[example]] @@ -627,11 +612,6 @@ name = "check_unlock_conditions" path = "examples/wallet/17_check_unlock_conditions.rs" required-features = ["wallet", "storage"] -[[example]] -name = "accounts" -path = "examples/wallet/accounts.rs" -required-features = ["wallet", "storage"] - [[example]] name = "background_syncing" path = "examples/wallet/background_syncing.rs" @@ -657,21 +637,11 @@ name = "logger" path = "examples/wallet/logger.rs" required-features = ["wallet", "storage"] -[[example]] -name = "recover_accounts" -path = "examples/wallet/recover_accounts.rs" -required-features = ["wallet", "storage"] - [[example]] name = "spammer" path = "examples/wallet/spammer.rs" required-features = ["wallet"] -[[example]] -name = "split_funds" -path = "examples/wallet/split_funds.rs" -required-features = ["wallet", "storage"] - [[example]] name = "storage" path = "examples/wallet/storage.rs" diff --git a/sdk/examples/.env.example b/sdk/examples/.env.example index d51040423a..7366ed10d1 100644 --- a/sdk/examples/.env.example +++ b/sdk/examples/.env.example @@ -6,7 +6,7 @@ # Mnemonics (Don't ever use them to manage real funds!) MNEMONIC="endorse answer radar about source reunion marriage tag sausage weekend frost daring base attack because joke dream slender leisure group reason prepare broken river" MNEMONIC_2="width scatter jaguar sponsor erosion enable cave since ancient first garden royal luggage exchange ritual exotic play wall clinic ride autumn divert spin exchange" -# The Wallet database folder used to store account data +# The Wallet database folder used to store wallet data WALLET_DB_PATH="./example-walletdb" # The Stronghold snapshot file location used to store secrets STRONGHOLD_SNAPSHOT_PATH="./example.stronghold" diff --git a/sdk/examples/how_tos/account/create.rs b/sdk/examples/how_tos/account/create.rs index a0b2263b7b..78ad399c7f 100644 --- a/sdk/examples/how_tos/account/create.rs +++ b/sdk/examples/how_tos/account/create.rs @@ -4,7 +4,7 @@ //! In this example we will create an account output. //! //! Make sure that `STRONGHOLD_SNAPSHOT_PATH` and `WALLET_DB_PATH` already exist by -//! running the `./how_tos/accounts_and_addresses/create_account.rs` example and that funds are available by running +//! running the `./how_tos/accounts_and_addresses/create_wallet.rs` example and that funds are available by running //! the `get_funds` example! //! //! Rename `.env.example` to `.env` first, then run the command: @@ -23,10 +23,9 @@ async fn main() -> Result<()> { .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) .finish() .await?; - let account = wallet.get_account("Alice").await?; - // May want to ensure the account is synced before sending a transaction. - let balance = account.sync(None).await?; + // May want to ensure the wallet is synced before sending a transaction. + let balance = wallet.sync(None).await?; println!("Accounts BEFORE:\n{:#?}", balance.accounts()); // Set the stronghold password @@ -37,10 +36,10 @@ async fn main() -> Result<()> { println!("Sending the create-account transaction..."); // Create an account output - let transaction = account.create_account_output(None, None).await?; + let transaction = wallet.create_account_output(None, None).await?; println!("Transaction sent: {}", transaction.transaction_id); - let block_id = account + let block_id = wallet .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; println!( @@ -49,7 +48,7 @@ async fn main() -> Result<()> { block_id ); - let balance = account.sync(None).await?; + let balance = wallet.sync(None).await?; println!("Accounts AFTER:\n{:#?}", balance.accounts()); Ok(()) diff --git a/sdk/examples/how_tos/account/destroy.rs b/sdk/examples/how_tos/account/destroy.rs index 1c9388e361..20930776f6 100644 --- a/sdk/examples/how_tos/account/destroy.rs +++ b/sdk/examples/how_tos/account/destroy.rs @@ -1,10 +1,10 @@ // Copyright 2022 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -//! In this example we will try to destroy the first account output there is in the account. +//! In this example we will try to destroy the first account output there is in the wallet. //! //! Make sure that `STRONGHOLD_SNAPSHOT_PATH` and `WALLET_DB_PATH` already exist by -//! running the `./how_tos/accounts_and_addresses/create_account.rs` example! +//! running the `./how_tos/accounts_and_addresses/create_wallet.rs` example! //! //! Rename `.env.example` to `.env` first, then run the command: //! ```sh @@ -22,11 +22,9 @@ async fn main() -> Result<()> { .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) .finish() .await?; - let alias = "Alice"; - let account = wallet.get_account(alias).await?; - // May want to ensure the account is synced before sending a transaction. - let balance = account.sync(None).await?; + // May want to ensure the wallet is synced before sending a transaction. + let balance = wallet.sync(None).await?; // Get the first account if let Some(account_id) = balance.accounts().first() { @@ -40,10 +38,10 @@ async fn main() -> Result<()> { println!("Sending account burn transaction..."); - let transaction = account.burn(*account_id, None).await?; + let transaction = wallet.burn(*account_id, None).await?; println!("Transaction sent: {}", transaction.transaction_id); - let block_id = account + let block_id = wallet .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; @@ -55,11 +53,12 @@ async fn main() -> Result<()> { println!("Burned Account '{}'", account_id); - let balance = account.sync(None).await?; + let balance = wallet.sync(None).await?; + let accounts_after = balance.accounts(); println!("Accounts AFTER destroying:\n{accounts_after:#?}",); } else { - println!("No Account available in account '{alias}'"); + println!("No Account available"); } Ok(()) diff --git a/sdk/examples/how_tos/account_wallet/request_funds.rs b/sdk/examples/how_tos/account_wallet/request_funds.rs index 9691313432..1975a09391 100644 --- a/sdk/examples/how_tos/account_wallet/request_funds.rs +++ b/sdk/examples/how_tos/account_wallet/request_funds.rs @@ -9,10 +9,7 @@ use iota_sdk::{ client::request_funds_from_faucet, types::block::address::{AccountAddress, ToBech32Ext}, - wallet::{ - account::{AliasSyncOptions, SyncOptions}, - Result, - }, + wallet::{AccountSyncOptions, Result, SyncOptions}, Wallet, }; @@ -29,18 +26,16 @@ async fn main() -> Result<()> { .finish() .await?; - // Get the account - let account = wallet.get_account("Alice").await?; - let balance = account.sync(None).await?; + let balance = wallet.sync(None).await?; let total_base_token_balance = balance.base_coin().total(); - println!("Balance before requesting funds on account address: {total_base_token_balance:#?}"); + println!("Balance before requesting funds on wallet address: {total_base_token_balance:#?}"); let account_id = balance.accounts().first().unwrap(); println!("Account Id: {account_id}"); // Get account address - let account_address = AccountAddress::new(*account_id).to_bech32(account.client().get_bech32_hrp().await.unwrap()); + let account_address = AccountAddress::new(*account_id).to_bech32(wallet.client().get_bech32_hrp().await.unwrap()); let faucet_response = request_funds_from_faucet(&faucet_url, &account_address).await?; println!("{faucet_response}"); @@ -48,13 +43,13 @@ async fn main() -> Result<()> { tokio::time::sleep(std::time::Duration::from_secs(10)).await; let sync_options = SyncOptions { - alias: AliasSyncOptions { + account: AccountSyncOptions { basic_outputs: true, ..Default::default() }, ..Default::default() }; - let total_base_token_balance = account.sync(Some(sync_options)).await?.base_coin().total(); + let total_base_token_balance = wallet.sync(Some(sync_options)).await?.base_coin().total(); println!("Balance after requesting funds on account address: {total_base_token_balance:#?}"); Ok(()) diff --git a/sdk/examples/how_tos/account_wallet/transaction.rs b/sdk/examples/how_tos/account_wallet/transaction.rs index 946a14e297..5087b82e19 100644 --- a/sdk/examples/how_tos/account_wallet/transaction.rs +++ b/sdk/examples/how_tos/account_wallet/transaction.rs @@ -9,10 +9,7 @@ use iota_sdk::{ client::node_api::indexer::query_parameters::BasicOutputQueryParameters, types::block::address::{AccountAddress, ToBech32Ext}, - wallet::{ - account::{AliasSyncOptions, SyncOptions, TransactionOptions}, - Result, - }, + wallet::{AccountSyncOptions, Result, SyncOptions, TransactionOptions}, Wallet, }; @@ -22,7 +19,7 @@ async fn main() -> Result<()> { dotenvy::dotenv().ok(); let sync_options = SyncOptions { - alias: AliasSyncOptions { + account: AccountSyncOptions { basic_outputs: true, ..Default::default() }, @@ -38,9 +35,7 @@ async fn main() -> Result<()> { .set_stronghold_password(std::env::var("STRONGHOLD_PASSWORD").unwrap()) .await?; - // Get the account - let account = wallet.get_account("Alice").await?; - let balance = account.sync(Some(sync_options.clone())).await?; + let balance = wallet.sync(Some(sync_options.clone())).await?; let total_base_token_balance = balance.base_coin().total(); println!("Balance before sending funds from account: {total_base_token_balance:#?}"); @@ -49,10 +44,10 @@ async fn main() -> Result<()> { println!("Account Id: {account_id}"); // Get account address - let account_address = AccountAddress::new(*account_id).to_bech32(account.client().get_bech32_hrp().await.unwrap()); + let account_address = AccountAddress::new(*account_id).to_bech32(wallet.client().get_bech32_hrp().await.unwrap()); // Find first output unlockable by the account address - let input = *account + let input = *wallet .client() .basic_output_ids(BasicOutputQueryParameters::only_address_unlock_condition( account_address, @@ -62,7 +57,7 @@ async fn main() -> Result<()> { .first() .unwrap(); - let transaction = account + let transaction = wallet .send( 1_000_000, "rms1qpszqzadsym6wpppd6z037dvlejmjuke7s24hm95s9fg9vpua7vluaw60xu", @@ -72,7 +67,7 @@ async fn main() -> Result<()> { }, ) .await?; - account + wallet .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; println!( @@ -80,7 +75,7 @@ async fn main() -> Result<()> { transaction.transaction_id ); - let total_base_token_balance = account.sync(Some(sync_options)).await?.base_coin().total(); + let total_base_token_balance = wallet.sync(Some(sync_options)).await?.base_coin().total(); println!("Balance after sending funds from account: {total_base_token_balance:#?}"); Ok(()) diff --git a/sdk/examples/how_tos/accounts_and_addresses/check_balance.rs b/sdk/examples/how_tos/accounts_and_addresses/check_balance.rs index 87b28a24f0..e9ec8f0095 100644 --- a/sdk/examples/how_tos/accounts_and_addresses/check_balance.rs +++ b/sdk/examples/how_tos/accounts_and_addresses/check_balance.rs @@ -1,17 +1,17 @@ // Copyright 2021 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -//! In this example we sync the account and get the balance. +//! In this example we sync the wallet and get the balance. //! //! Make sure that `STRONGHOLD_SNAPSHOT_PATH` and `WALLET_DB_PATH` already exist by -//! running the `./how_tos/accounts_and_addresses/create_account.rs` example! +//! running the `./how_tos/accounts_and_addresses/create_wallet.rs` example! //! //! Rename `.env.example` to `.env` first, then run the command: //! ```sh //! cargo run --release --all-features --example check_balance //! ``` -use iota_sdk::{wallet::Result, Wallet}; +use iota_sdk::{types::block::address::ToBech32Ext, wallet::Result, Wallet}; #[tokio::main] async fn main() -> Result<()> { @@ -22,18 +22,14 @@ async fn main() -> Result<()> { .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) .finish() .await?; - let account = wallet.get_account("Alice").await?; // Sync and get the balance - let balance = account.sync(None).await?; + let balance = wallet.sync(None).await?; println!("{balance:#?}"); - println!("ADDRESSES:"); let explorer_url = std::env::var("EXPLORER_URL").ok(); - let prepended = explorer_url.map(|url| format!("{url}/addr/")).unwrap_or_default(); - for address in account.addresses().await { - println!(" - {prepended}{}", address.address()); - } + let addr_base_url = explorer_url.map(|url| format!("{url}/addr/")).unwrap_or_default(); + println!("{addr_base_url}{}", wallet.address().await); Ok(()) } diff --git a/sdk/examples/how_tos/accounts_and_addresses/consolidate_outputs.rs b/sdk/examples/how_tos/accounts_and_addresses/consolidate_outputs.rs index ae79dea54f..08fb4a564d 100644 --- a/sdk/examples/how_tos/accounts_and_addresses/consolidate_outputs.rs +++ b/sdk/examples/how_tos/accounts_and_addresses/consolidate_outputs.rs @@ -1,11 +1,11 @@ // Copyright 2021 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -//! In this example we will consolidate basic outputs from an account with only an AddressUnlockCondition by sending +//! In this example we will consolidate basic outputs from a wallet with only an AddressUnlockCondition by sending //! them to the same address again. //! //! Make sure that `STRONGHOLD_SNAPSHOT_PATH` and `WALLET_DB_PATH` already exist by -//! running the `./how_tos/accounts_and_addresses/create_account.rs` example! +//! running the `./how_tos/accounts_and_addresses/create_wallet.rs` example! //! //! Rename `.env.example` to `.env` first, then run the command: //! ```sh @@ -14,7 +14,7 @@ use iota_sdk::{ types::block::address::ToBech32Ext, - wallet::{account::ConsolidationParams, Result}, + wallet::{ConsolidationParams, Result}, Wallet, }; @@ -27,23 +27,22 @@ async fn main() -> Result<()> { .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) .finish() .await?; - let account = wallet.get_account("Alice").await?; // Set the stronghold password wallet .set_stronghold_password(std::env::var("STRONGHOLD_PASSWORD").unwrap()) .await?; - // Sync account to make sure account is updated with outputs from previous examples - account.sync(None).await?; - println!("Account synced"); + // Sync wallet to make sure it is updated with outputs from previous examples + wallet.sync(None).await?; + println!("Wallet synced"); // List unspent outputs before consolidation. // The output we created with example `03_get_funds` and the basic output from `09_mint_native_tokens` have only one // unlock condition and it is an `AddressUnlockCondition`, and so they are valid for consolidation. They have the - // same `AddressUnlockCondition`(the first address of the account), so they will be consolidated into one + // same `AddressUnlockCondition`(the address of the wallet), so they will be consolidated into one // output. - let outputs = account.unspent_outputs(None).await?; + let outputs = wallet.unspent_outputs(None).await?; println!("Outputs BEFORE consolidation:"); outputs.iter().enumerate().for_each(|(i, output_data)| { println!("OUTPUT #{i}"); @@ -59,13 +58,13 @@ async fn main() -> Result<()> { // Consolidate unspent outputs and print the consolidation transaction ID // Set `force` to true to force the consolidation even though the `output_threshold` isn't reached - let transaction = account + let transaction = wallet .consolidate_outputs(ConsolidationParams::new().with_force(true)) .await?; println!("Transaction sent: {}", transaction.transaction_id); // Wait for the consolidation transaction to get confirmed - let block_id = account + let block_id = wallet .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; println!( @@ -74,12 +73,12 @@ async fn main() -> Result<()> { block_id ); - // Sync account - account.sync(None).await?; - println!("Account synced"); + // Sync wallet + wallet.sync(None).await?; + println!("Wallet synced"); // Outputs after consolidation - let outputs = account.unspent_outputs(None).await?; + let outputs = wallet.unspent_outputs(None).await?; println!("Outputs AFTER consolidation:"); outputs.iter().enumerate().for_each(|(i, output_data)| { println!("OUTPUT #{i}"); diff --git a/sdk/examples/how_tos/accounts_and_addresses/create_address.rs b/sdk/examples/how_tos/accounts_and_addresses/create_address.rs deleted file mode 100644 index 3cd40dac03..0000000000 --- a/sdk/examples/how_tos/accounts_and_addresses/create_address.rs +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2021 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -//! In this example we will generate addresses for an already existing wallet. -//! -//! Make sure that `STRONGHOLD_SNAPSHOT_PATH` and `WALLET_DB_PATH` already exist by -//! running the `./how_tos/accounts_and_addresses/create_account.rs` example! -//! -//! Rename `.env.example` to `.env` first, then run the command: -//! ```sh -//! cargo run --release --all-features --example create_address` -//! ``` - -use iota_sdk::{wallet::Result, Wallet}; - -// The number of addresses to generate -const NUM_ADDRESSES_TO_GENERATE: u32 = 5; - -#[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(); - - let wallet = Wallet::builder() - .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) - .finish() - .await?; - let account = wallet.get_account("Alice").await?; - - // Provide the stronghold password - wallet - .set_stronghold_password(std::env::var("STRONGHOLD_PASSWORD").unwrap()) - .await?; - - let explorer_url = std::env::var("EXPLORER_URL").ok(); - let address_url = explorer_url.map(|url| format!("{url}/addr/")).unwrap_or_default(); - - println!("Current addresses:"); - for address in account.addresses().await { - println!(" - {address_url}{}", address.address()); - } - - // Generate some addresses - let new_addresses = account - .generate_ed25519_addresses(NUM_ADDRESSES_TO_GENERATE, None) - .await?; - println!("Generated {} new addresses:", new_addresses.len()); - let account_addresses = account.addresses().await; - for new_address in new_addresses.iter() { - assert!(account_addresses.contains(new_address)); - println!(" - {address_url}{}", new_address.address()); - } - Ok(()) -} diff --git a/sdk/examples/how_tos/accounts_and_addresses/create_account.rs b/sdk/examples/how_tos/accounts_and_addresses/create_wallet.rs similarity index 83% rename from sdk/examples/how_tos/accounts_and_addresses/create_account.rs rename to sdk/examples/how_tos/accounts_and_addresses/create_wallet.rs index af62241b55..be4d113b1d 100644 --- a/sdk/examples/how_tos/accounts_and_addresses/create_account.rs +++ b/sdk/examples/how_tos/accounts_and_addresses/create_wallet.rs @@ -7,7 +7,7 @@ //! //! Rename `.env.example` to `.env` first, then run the command: //! ```sh -//! cargo run --release --all-features --example create_account +//! cargo run --release --all-features --example create_wallet //! ``` use iota_sdk::{ @@ -15,7 +15,7 @@ use iota_sdk::{ constants::SHIMMER_COIN_TYPE, secret::{stronghold::StrongholdSecretManager, SecretManager}, }, - crypto::keys::bip39::Mnemonic, + crypto::keys::{bip39::Mnemonic, bip44::Bip44}, wallet::{ClientOptions, Result, Wallet}, }; @@ -42,14 +42,11 @@ async fn main() -> Result<()> { .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_bip_path(Bip44::new(SHIMMER_COIN_TYPE)) .finish() .await?; - // Create a new account - let account = wallet.create_account().with_alias("Alice").finish().await?; - - println!("Generated new account: '{}'", account.alias().await); + println!("Generated new wallet"); Ok(()) } diff --git a/sdk/examples/how_tos/accounts_and_addresses/list_accounts.rs b/sdk/examples/how_tos/accounts_and_addresses/list_accounts.rs deleted file mode 100644 index ffc57e6e45..0000000000 --- a/sdk/examples/how_tos/accounts_and_addresses/list_accounts.rs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2023 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -//! In this example we will list all accounts in the wallet. -//! -//! Rename `.env.example` to `.env` first, then run the command: -//! ```sh -//! cargo run --release --all-features --example list_accounts -//! ``` - -use iota_sdk::{wallet::Result, Wallet}; - -#[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(); - - let wallet = Wallet::builder() - .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) - .finish() - .await?; - - // Get the accounts and print the alias of each account - for account in wallet.get_accounts().await? { - println!("{}", account.alias().await); - } - - Ok(()) -} diff --git a/sdk/examples/how_tos/accounts_and_addresses/list_addresses.rs b/sdk/examples/how_tos/accounts_and_addresses/list_addresses.rs deleted file mode 100644 index ebcc539c71..0000000000 --- a/sdk/examples/how_tos/accounts_and_addresses/list_addresses.rs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2023 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -//! In this example we will list all addresses of an account. -//! -//! Rename `.env.example` to `.env` first, then run the command: -//! ```sh -//! cargo run --release --all-features --example list_addresses -//! ``` - -use iota_sdk::{wallet::Result, Wallet}; - -#[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(); - - let wallet = Wallet::builder() - .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) - .finish() - .await?; - let account = wallet.get_account("Alice").await?; - - for address in account.addresses().await { - println!("{}", address.address()); - } - - Ok(()) -} diff --git a/sdk/examples/how_tos/accounts_and_addresses/list_outputs.rs b/sdk/examples/how_tos/accounts_and_addresses/list_outputs.rs index 3593cbd5ee..b125b8a051 100644 --- a/sdk/examples/how_tos/accounts_and_addresses/list_outputs.rs +++ b/sdk/examples/how_tos/accounts_and_addresses/list_outputs.rs @@ -1,7 +1,7 @@ // Copyright 2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -//! In this example we will list all outputs of an account. +//! In this example we will list all outputs of a wallet. //! //! Rename `.env.example` to `.env` first, then run the command: //! ```sh @@ -19,20 +19,19 @@ async fn main() -> Result<()> { .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) .finish() .await?; - let account = wallet.get_account("Alice").await?; - // Sync account - account.sync(None).await?; + // Sync wallet + wallet.sync(None).await?; // Print output ids println!("Output ids:"); - for output in account.outputs(None).await? { + for output in wallet.outputs(None).await? { println!("{}", output.output_id); } // Print unspent output ids println!("Unspent output ids:"); - for output in account.unspent_outputs(None).await? { + for output in wallet.unspent_outputs(None).await? { println!("{}", output.output_id); } diff --git a/sdk/examples/how_tos/accounts_and_addresses/list_transactions.rs b/sdk/examples/how_tos/accounts_and_addresses/list_transactions.rs index 59b46f5044..6d07e6cb7c 100644 --- a/sdk/examples/how_tos/accounts_and_addresses/list_transactions.rs +++ b/sdk/examples/how_tos/accounts_and_addresses/list_transactions.rs @@ -1,7 +1,7 @@ // Copyright 2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -//! In this example we will list all transaction of an account. +//! In this example we will list all transaction of a wallet. //! //! Rename `.env.example` to `.env` first, then run the command: //! ```sh @@ -9,7 +9,7 @@ //! ``` use iota_sdk::{ - wallet::{account::SyncOptions, Result}, + wallet::{Result, SyncOptions}, Wallet, }; @@ -22,10 +22,9 @@ async fn main() -> Result<()> { .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) .finish() .await?; - let account = wallet.get_account("Alice").await?; - // Sync account - account + // Sync wallet + wallet .sync(Some(SyncOptions { sync_incoming_transactions: true, ..Default::default() @@ -34,13 +33,13 @@ async fn main() -> Result<()> { // Print transaction ids println!("Sent transactions:"); - for transaction in account.transactions().await { + for transaction in wallet.transactions().await { println!("{}", transaction.transaction_id); } // Print received transaction ids println!("Received transactions:"); - for transaction in account.incoming_transactions().await { + for transaction in wallet.incoming_transactions().await { println!("{}", transaction.transaction_id); } diff --git a/sdk/examples/how_tos/advanced_transactions/advanced_transaction.rs b/sdk/examples/how_tos/advanced_transactions/advanced_transaction.rs index e79eb8a300..17aa0c7792 100644 --- a/sdk/examples/how_tos/advanced_transactions/advanced_transaction.rs +++ b/sdk/examples/how_tos/advanced_transactions/advanced_transaction.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 //! In this example we will send a transaction. +//! //! Rename `.env.example` to `.env` first. //! //! `cargo run --release --all-features --example advanced_transaction` @@ -24,12 +25,13 @@ async fn main() -> Result<()> { // This example uses secrets in environment variables for simplicity which should not be done in production. dotenvy::dotenv().ok(); - // Create the wallet - let wallet = Wallet::builder().finish().await?; + // Get the wallet we generated with `create_wallet`. + let wallet = Wallet::builder() + .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) + .finish() + .await?; - // Get the account we generated with `create_account` - let account = wallet.get_account("Alice").await?; - // May want to ensure the account is synced before sending a transaction. + // May want to ensure the wallet is synced before sending a transaction. let balance = wallet.sync(None).await?; if balance.base_coin().available() >= 1_000_000 { @@ -46,13 +48,13 @@ async fn main() -> Result<()> { "rms1qpszqzadsym6wpppd6z037dvlejmjuke7s24hm95s9fg9vpua7vluaw60xu", )?)) .add_unlock_condition(TimelockUnlockCondition::new(slot_index)?) - .finish_output(account.client().get_token_supply().await?)?; + .finish_output(wallet.client().get_token_supply().await?)?; - let transaction = account.send_outputs(vec![basic_output], None).await?; + let transaction = wallet.send_outputs(vec![basic_output], None).await?; println!("Transaction sent: {}", transaction.transaction_id); // Wait for transaction to get included - let block_id = account + let block_id = wallet .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; println!( diff --git a/sdk/examples/how_tos/advanced_transactions/claim_transaction.rs b/sdk/examples/how_tos/advanced_transactions/claim_transaction.rs index 723d12d877..68319df0a9 100644 --- a/sdk/examples/how_tos/advanced_transactions/claim_transaction.rs +++ b/sdk/examples/how_tos/advanced_transactions/claim_transaction.rs @@ -7,7 +7,7 @@ //! `cargo run --release --all-features --example claim_transaction` use iota_sdk::{ - wallet::{account::OutputsToClaim, Result}, + wallet::{OutputsToClaim, Result}, Wallet, }; @@ -16,31 +16,31 @@ async fn main() -> Result<()> { // This example uses secrets in environment variables for simplicity which should not be done in production. dotenvy::dotenv().ok(); - // Create the wallet - let wallet = Wallet::builder().finish().await?; - - // Get the account we generated with `create_account` - let account = wallet.get_account("Alice").await?; + // Get the wallet we generated with `create_wallet`. + let wallet = Wallet::builder() + .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) + .finish() + .await?; - // May want to ensure the account is synced before sending a transaction. - account.sync(None).await?; + // May want to ensure the wallet is synced before sending a transaction. + wallet.sync(None).await?; // Set the stronghold password wallet .set_stronghold_password(std::env::var("STRONGHOLD_PASSWORD").unwrap()) .await?; - let output_ids = account.claimable_outputs(OutputsToClaim::All).await?; + let output_ids = wallet.claimable_outputs(OutputsToClaim::All).await?; println!("Available outputs to claim:"); for output_id in &output_ids { println!("{}", output_id); } - let transaction = account.claim_outputs(output_ids).await?; + let transaction = wallet.claim_outputs(output_ids).await?; println!("Transaction sent: {}", transaction.transaction_id); // Wait for transaction to get included - let block_id = account + let block_id = wallet .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; println!( diff --git a/sdk/examples/how_tos/advanced_transactions/send_micro_transaction.rs b/sdk/examples/how_tos/advanced_transactions/send_micro_transaction.rs index 5203d187dc..ff511714df 100644 --- a/sdk/examples/how_tos/advanced_transactions/send_micro_transaction.rs +++ b/sdk/examples/how_tos/advanced_transactions/send_micro_transaction.rs @@ -4,7 +4,7 @@ //! In this example we will send an amount below the minimum storage deposit. //! //! Make sure that `STRONGHOLD_SNAPSHOT_PATH` and `WALLET_DB_PATH` already exist by -//! running the `./how_tos/accounts_and_addresses/create_account.rs` example! +//! running the `./how_tos/accounts_and_addresses/create_wallet.rs` example! //! //! Rename `.env.example` to `.env` first, then run the command: //! ```sh @@ -12,7 +12,7 @@ //! ``` use iota_sdk::{ - wallet::{account::TransactionOptions, Result}, + wallet::{Result, TransactionOptions}, Wallet, }; @@ -26,14 +26,14 @@ async fn main() -> Result<()> { // This example uses secrets in environment variables for simplicity which should not be done in production. dotenvy::dotenv().ok(); + // Get the wallet we generated with `create_wallet`. let wallet = Wallet::builder() .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) .finish() .await?; - let account = wallet.get_account("Alice").await?; - // May want to ensure the account is synced before sending a transaction. - account.sync(None).await?; + // May want to ensure the wallet is synced before sending a transaction. + wallet.sync(None).await?; // Set the stronghold password wallet @@ -43,7 +43,7 @@ async fn main() -> Result<()> { println!("Sending '{}' coin(s) to '{}'...", SEND_MICRO_AMOUNT, RECV_ADDRESS); // Send a micro transaction - let transaction = account + let transaction = wallet .send( SEND_MICRO_AMOUNT, RECV_ADDRESS, @@ -56,7 +56,7 @@ async fn main() -> Result<()> { println!("Transaction sent: {}", transaction.transaction_id); // Wait for transaction to get included - let block_id = account + let block_id = wallet .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; diff --git a/sdk/examples/how_tos/native_tokens/burn.rs b/sdk/examples/how_tos/native_tokens/burn.rs index 887c47a8ce..15d0985236 100644 --- a/sdk/examples/how_tos/native_tokens/burn.rs +++ b/sdk/examples/how_tos/native_tokens/burn.rs @@ -6,9 +6,9 @@ //! output that minted it. //! //! Make sure that `STRONGHOLD_SNAPSHOT_PATH` and `WALLET_DB_PATH` already exist by -//! running the `./how_tos/accounts_and_addresses/create_account.rs` example! +//! running the `./how_tos/accounts_and_addresses/create_wallet.rs` example! //! -//! You may provide a TOKEN_ID that is available in the account. You can check this by running the +//! You may provide a TOKEN_ID that is available in the wallet. You can check this by running the //! `get_balance` example. You can create a new native token by running the `create_native_token` example. //! //! Rename `.env.example` to `.env` first, then run the command: @@ -22,7 +22,7 @@ use iota_sdk::{ Wallet, U256, }; -// The minimum available native token amount to search for in the account +// The minimum available native token amount to search for in the wallet const MIN_AVAILABLE_AMOUNT: u64 = 11; // The amount of the native token to burn const BURN_AMOUNT: u64 = 1; @@ -36,11 +36,9 @@ async fn main() -> Result<()> { .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) .finish() .await?; - let alias = "Alice"; - let account = wallet.get_account(alias.to_string()).await?; - // May want to ensure the account is synced before sending a transaction. - let balance = account.sync(None).await?; + // May want to ensure the wallet is synced before sending a transaction. + let balance = wallet.sync(None).await?; // Take the given token id, or use a default. let token_id = std::env::args() @@ -59,10 +57,10 @@ async fn main() -> Result<()> { .await?; // Burn a native token - let transaction = account.burn(NativeToken::new(token_id, BURN_AMOUNT)?, None).await?; + let transaction = wallet.burn(NativeToken::new(token_id, BURN_AMOUNT)?, None).await?; println!("Transaction sent: {}", transaction.transaction_id); - let block_id = account + let block_id = wallet .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; println!( @@ -71,7 +69,7 @@ async fn main() -> Result<()> { block_id ); - let balance = account.sync(None).await?; + let balance = wallet.sync(None).await?; print!("Balance after burning: "); if let Some(native_token_balance) = balance @@ -85,7 +83,7 @@ async fn main() -> Result<()> { } } else { println!( - "Native token '{token_id}' doesn't exist or there's not at least '{MIN_AVAILABLE_AMOUNT}' tokens of it in account '{alias}'" + "Native token '{token_id}' doesn't exist or there's not at least '{MIN_AVAILABLE_AMOUNT}' tokens of it in the wallet" ); } diff --git a/sdk/examples/how_tos/native_tokens/create.rs b/sdk/examples/how_tos/native_tokens/create.rs index 0a3b8a88f3..c47b7f8275 100644 --- a/sdk/examples/how_tos/native_tokens/create.rs +++ b/sdk/examples/how_tos/native_tokens/create.rs @@ -4,7 +4,7 @@ //! In this example we will create a native token. //! //! Make sure that `example.stronghold` and `example.walletdb` already exist by -//! running the `create_account` example! +//! running the `create_wallet` example! //! //! Rename `.env.example` to `.env` first, then run the command: //! ```sh @@ -31,23 +31,23 @@ async fn main() -> Result<()> { .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) .finish() .await?; - let account = wallet.get_account("Alice").await?; - let balance = account.sync(None).await?; + + let balance = wallet.sync(None).await?; // Set the stronghold password wallet .set_stronghold_password(std::env::var("STRONGHOLD_PASSWORD").unwrap()) .await?; - // We can first check if we already have an account output in our account, because an account can have many foundry + // We can first check if we already have an account output in our wallet, because an account can have many foundry // outputs and therefore we can reuse an existing one if balance.accounts().is_empty() { // If we don't have an account, we need to create one - let transaction = account.create_account_output(None, None).await?; + let transaction = wallet.create_account_output(None, None).await?; println!("Transaction sent: {}", transaction.transaction_id); // Wait for transaction to get included - let block_id = account + let block_id = wallet .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; println!( @@ -56,8 +56,8 @@ async fn main() -> Result<()> { block_id ); - account.sync(None).await?; - println!("Account synced"); + wallet.sync(None).await?; + println!("Wallet synced"); } let metadata = @@ -72,11 +72,11 @@ async fn main() -> Result<()> { foundry_metadata: Some(metadata.to_bytes()), }; - let transaction = account.create_native_token(params, None).await?; + let transaction = wallet.create_native_token(params, None).await?; println!("Transaction sent: {}", transaction.transaction.transaction_id); // Wait for transaction to get included - let block_id = account + let block_id = wallet .reissue_transaction_until_included(&transaction.transaction.transaction_id, None, None) .await?; println!( @@ -86,9 +86,9 @@ async fn main() -> Result<()> { ); println!("Created token: {}", transaction.token_id); - // Ensure the account is synced after creating the native token. - account.sync(None).await?; - println!("Account synced"); + // Ensure the wallet is synced after creating the native token. + wallet.sync(None).await?; + println!("Wallet synced"); Ok(()) } diff --git a/sdk/examples/how_tos/native_tokens/destroy_foundry.rs b/sdk/examples/how_tos/native_tokens/destroy_foundry.rs index 04ff86e03b..a324f366db 100644 --- a/sdk/examples/how_tos/native_tokens/destroy_foundry.rs +++ b/sdk/examples/how_tos/native_tokens/destroy_foundry.rs @@ -1,11 +1,11 @@ // Copyright 2022 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -//! In this example we will try to destroy the first foundry there is in the account. This is only possible if its +//! In this example we will try to destroy the first foundry there is in the wallet. This is only possible if its //! circulating supply is 0 and no native tokens were burned. //! //! Make sure that `STRONGHOLD_SNAPSHOT_PATH` and `WALLET_DB_PATH` already exist by -//! running the `./how_tos/accounts_and_addresses/create_account.rs` example! +//! running the `./how_tos/accounts_and_addresses/create_wallet.rs` example! //! //! Rename `.env.example` to `.env` first, then run the command: //! ```sh @@ -23,16 +23,14 @@ async fn main() -> Result<()> { .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) .finish() .await?; - let alias = "Alice"; - let account = wallet.get_account(alias).await?; - // May want to ensure the account is synced before sending a transaction. - let balance = account.sync(None).await?; + // May want to ensure the wallet is synced before sending a transaction. + let balance = wallet.sync(None).await?; let foundry_count = balance.foundries().len(); println!("Foundries before destroying: {foundry_count}"); - // We try to destroy the first foundry in the account + // We try to destroy the first foundry in the wallet if let Some(foundry_id) = balance.foundries().first() { let token_id = TokenId::from(*foundry_id); @@ -47,7 +45,7 @@ async fn main() -> Result<()> { .iter() .find(|native_token| *native_token.token_id() == token_id); if let Some(native_token) = native_tokens { - let output = account.get_foundry_output(token_id).await?; + let output = wallet.get_foundry_output(token_id).await?; // Check if all tokens are melted. if native_token.available() != output.as_foundry().token_scheme().as_simple().circulating_supply() { // We are not able to melt all tokens, because we don't own them or they are not unlocked. @@ -57,12 +55,12 @@ async fn main() -> Result<()> { println!("Melting remaining tokens.."); // Melt all tokens so we can destroy the foundry. - let transaction = account + let transaction = wallet .melt_native_token(token_id, native_token.available(), None) .await?; println!("Transaction sent: {}", transaction.transaction_id); - let block_id = account + let block_id = wallet .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; println!( @@ -72,15 +70,15 @@ async fn main() -> Result<()> { ); // Sync to make the foundry output available again, because it was used in the melting transaction. - account.sync(None).await?; + wallet.sync(None).await?; } println!("Destroying foundry.."); - let transaction = account.burn(*foundry_id, None).await?; + let transaction = wallet.burn(*foundry_id, None).await?; println!("Transaction sent: {}", transaction.transaction_id); - let block_id = account + let block_id = wallet .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; println!( @@ -90,12 +88,12 @@ async fn main() -> Result<()> { ); // Resync to update the foundries list. - let balance = account.sync(None).await?; + let balance = wallet.sync(None).await?; let foundry_count = balance.foundries().len(); println!("Foundries after destroying: {foundry_count}"); } else { - println!("No Foundry available in account '{alias}'"); + println!("No Foundry available in the wallet"); } Ok(()) diff --git a/sdk/examples/how_tos/native_tokens/melt.rs b/sdk/examples/how_tos/native_tokens/melt.rs index 8abab66817..90bdcc34d0 100644 --- a/sdk/examples/how_tos/native_tokens/melt.rs +++ b/sdk/examples/how_tos/native_tokens/melt.rs @@ -4,9 +4,9 @@ //! In this example we will melt an existing native token with its foundry. //! //! Make sure that `STRONGHOLD_SNAPSHOT_PATH` and `WALLET_DB_PATH` already exist by -//! running the `./how_tos/accounts_and_addresses/create_account.rs` example! +//! running the `./how_tos/accounts_and_addresses/create_wallet.rs` example! //! -//! You may provide a TOKEN_ID that is available in the account. The foundry +//! You may provide a TOKEN_ID that is available in the wallet. The foundry //! output which minted it needs to be available as well. You can check this by //! running the `get_balance` example. You can create a new native token by running //! the `create_native_token` example. @@ -30,10 +30,9 @@ async fn main() -> Result<()> { .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) .finish() .await?; - let account = wallet.get_account("Alice").await?; - // May want to ensure the account is synced before sending a transaction. - let balance = account.sync(None).await?; + // May want to ensure the wallet is synced before sending a transaction. + let balance = wallet.sync(None).await?; // Find first foundry and corresponding token id let token_id = std::env::args() @@ -49,7 +48,7 @@ async fn main() -> Result<()> { let available_balance = native_token_balance.available(); println!("Balance before melting: {available_balance}"); } else { - println!("Couldn't find native token '{token_id}' in the account"); + println!("Couldn't find native token '{token_id}' in the wallet"); return Ok(()); } @@ -60,10 +59,10 @@ async fn main() -> Result<()> { // Melt some of the circulating supply - let transaction = account.melt_native_token(token_id, MELT_AMOUNT, None).await?; + let transaction = wallet.melt_native_token(token_id, MELT_AMOUNT, None).await?; println!("Transaction sent: {}", transaction.transaction_id); - let block_id = account + let block_id = wallet .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; @@ -73,7 +72,7 @@ async fn main() -> Result<()> { block_id ); - let balance = account.sync(None).await?; + let balance = wallet.sync(None).await?; let available_balance = balance .native_tokens() .iter() diff --git a/sdk/examples/how_tos/native_tokens/mint.rs b/sdk/examples/how_tos/native_tokens/mint.rs index 4aba7d2ea4..46a6ddfce9 100644 --- a/sdk/examples/how_tos/native_tokens/mint.rs +++ b/sdk/examples/how_tos/native_tokens/mint.rs @@ -4,9 +4,9 @@ //! In this example we will mint an existing native token with its foundry. //! //! Make sure that `STRONGHOLD_SNAPSHOT_PATH` and `WALLET_DB_PATH` already exist by -//! running the `./how_tos/accounts_and_addresses/create_account.rs` example! +//! running the `./how_tos/accounts_and_addresses/create_wallet.rs` example! //! -//! You may provide a TOKEN_ID that is available in the account. The foundry +//! You may provide a TOKEN_ID that is available in the wallet. The foundry //! output which minted it needs to be available as well. You can check this by //! running the `get_balance` example. You can create a new native token by running //! the `create_native_token` example. @@ -30,10 +30,9 @@ async fn main() -> Result<()> { .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) .finish() .await?; - let account = wallet.get_account("Alice").await?; - // May want to ensure the account is synced before sending a transaction. - let balance = account.sync(None).await?; + // May want to ensure the wallet is synced before sending a transaction. + let balance = wallet.sync(None).await?; // Find first foundry and corresponding token id let token_id = std::env::args() @@ -49,7 +48,7 @@ async fn main() -> Result<()> { let available_balance = native_token_balance.available(); println!("Balance before minting: {available_balance}"); } else { - println!("Couldn't find native token '{token_id}' in the account"); + println!("Couldn't find native token '{token_id}' in the wallet"); return Ok(()); } @@ -59,10 +58,10 @@ async fn main() -> Result<()> { .await?; // Mint some more native tokens - let transaction = account.mint_native_token(token_id, MINT_AMOUNT, None).await?; + let transaction = wallet.mint_native_token(token_id, MINT_AMOUNT, None).await?; println!("Transaction sent: {}", transaction.transaction_id); - let block_id = account + let block_id = wallet .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; @@ -72,7 +71,7 @@ async fn main() -> Result<()> { block_id ); - let balance = account.sync(None).await?; + let balance = wallet.sync(None).await?; let available_balance = balance .native_tokens() .iter() diff --git a/sdk/examples/how_tos/native_tokens/send.rs b/sdk/examples/how_tos/native_tokens/send.rs index c513374920..d611442b05 100644 --- a/sdk/examples/how_tos/native_tokens/send.rs +++ b/sdk/examples/how_tos/native_tokens/send.rs @@ -4,7 +4,7 @@ //! In this example we will send native tokens. //! //! Make sure that `STRONGHOLD_SNAPSHOT_PATH` and `WALLET_DB_PATH` already exist by -//! running the `./how_tos/accounts_and_addresses/create_account.rs` example! +//! running the `./how_tos/accounts_and_addresses/create_wallet.rs` example! //! //! Rename `.env.example` to `.env` first, then run the command: //! ```sh @@ -32,10 +32,9 @@ async fn main() -> Result<()> { .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) .finish() .await?; - let account = wallet.get_account("Alice").await?; - // May want to ensure the account is synced before sending a transaction. - let balance = account.sync(None).await?; + // May want to ensure the wallet is synced before sending a transaction. + let balance = wallet.sync(None).await?; // Get a token with sufficient balance if let Some(token_id) = balance @@ -64,11 +63,11 @@ async fn main() -> Result<()> { [(*token_id, U256::from(SEND_NATIVE_TOKEN_AMOUNT))], )?]; - let transaction = account.send_native_tokens(outputs, None).await?; + let transaction = wallet.send_native_tokens(outputs, None).await?; println!("Transaction sent: {}", transaction.transaction_id); // Wait for transaction to get included - let block_id = account + let block_id = wallet .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; println!( @@ -77,7 +76,7 @@ async fn main() -> Result<()> { block_id ); - let balance = account.sync(None).await?; + let balance = wallet.sync(None).await?; let available_balance = balance .native_tokens() diff --git a/sdk/examples/how_tos/nft_collection/00_mint_issuer_nft.rs b/sdk/examples/how_tos/nft_collection/00_mint_issuer_nft.rs index cb934124ea..d49d6d177a 100644 --- a/sdk/examples/how_tos/nft_collection/00_mint_issuer_nft.rs +++ b/sdk/examples/how_tos/nft_collection/00_mint_issuer_nft.rs @@ -4,9 +4,9 @@ //! In this example we will mint the issuer NFT for the NFT collection. //! //! Make sure that `STRONGHOLD_SNAPSHOT_PATH` and `WALLET_DB_PATH` already exist by -//! running the `./how_tos/accounts_and_addresses/create_account.rs` example! +//! running the `./how_tos/accounts_and_addresses/create_wallet.rs` example! //! -//! Make sure that Account Alice already has some funds by running the +//! Make sure that the wallet already has some funds by running the //! `./how_tos/simple_transaction/request_funds.rs` example! //! //! Rename `.env.example` to `.env` first, then run the command: @@ -19,7 +19,7 @@ use iota_sdk::{ output::{NftId, Output, OutputId}, payload::signed_transaction::TransactionId, }, - wallet::{Account, MintNftParams, Result}, + wallet::{MintNftParams, Result}, Wallet, }; @@ -32,10 +32,9 @@ async fn main() -> Result<()> { .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) .finish() .await?; - let account = wallet.get_account("Alice").await?; - account.sync(None).await?; - println!("Account synced!"); + wallet.sync(None).await?; + println!("Wallet synced!"); // Set the stronghold password wallet @@ -46,9 +45,9 @@ async fn main() -> Result<()> { println!("Sending NFT minting transaction..."); let nft_mint_params = [MintNftParams::new() .with_immutable_metadata(b"This NFT will be the issuer from the awesome NFT collection".to_vec())]; - let transaction = dbg!(account.mint_nfts(nft_mint_params, None).await)?; + let transaction = dbg!(wallet.mint_nfts(nft_mint_params, None).await)?; - wait_for_inclusion(&transaction.transaction_id, &account).await?; + wait_for_inclusion(&transaction.transaction_id, &wallet).await?; for (output_index, output) in transaction.payload.transaction().outputs().iter().enumerate() { if let Output::Nft(nft_output) = output { @@ -64,14 +63,14 @@ async fn main() -> Result<()> { Ok(()) } -async fn wait_for_inclusion(transaction_id: &TransactionId, account: &Account) -> Result<()> { +async fn wait_for_inclusion(transaction_id: &TransactionId, wallet: &Wallet) -> Result<()> { println!( "Transaction sent: {}/transaction/{}", std::env::var("EXPLORER_URL").unwrap(), transaction_id ); // Wait for transaction to get included - let block_id = account + let block_id = wallet .reissue_transaction_until_included(transaction_id, None, None) .await?; println!( diff --git a/sdk/examples/how_tos/nft_collection/01_mint_collection_nft.rs b/sdk/examples/how_tos/nft_collection/01_mint_collection_nft.rs index 09a4b0386e..b71ff05350 100644 --- a/sdk/examples/how_tos/nft_collection/01_mint_collection_nft.rs +++ b/sdk/examples/how_tos/nft_collection/01_mint_collection_nft.rs @@ -4,7 +4,7 @@ //! In this example we will mint some collection NFTs with issuer feature. //! //! Make sure that `STRONGHOLD_SNAPSHOT_PATH` and `WALLET_DB_PATH` already exist by -//! running the `./how_tos/accounts_and_addresses/create_account.rs` example. +//! running the `./how_tos/accounts_and_addresses/create_wallet.rs` example. //! //! You have to provide the ISSUER_NFT_ID that was created by first running the //! `mint_issuer_nft` example! @@ -20,7 +20,7 @@ use iota_sdk::{ output::{feature::Irc27Metadata, NftId}, payload::signed_transaction::TransactionId, }, - wallet::{Account, MintNftParams, Result}, + wallet::{MintNftParams, Result}, Wallet, }; @@ -43,17 +43,16 @@ async fn main() -> Result<()> { .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) .finish() .await?; - let account = wallet.get_account("Alice").await?; - account.sync(None).await?; - println!("Account synced!"); + wallet.sync(None).await?; + println!("Wallet synced!"); // Set the stronghold password wallet .set_stronghold_password(std::env::var("STRONGHOLD_PASSWORD").unwrap()) .await?; - let bech32_hrp = account.client().get_bech32_hrp().await?; + let bech32_hrp = wallet.client().get_bech32_hrp().await?; let issuer = Bech32Address::new(bech32_hrp, NftAddress::new(issuer_nft_id)); // Create the metadata with another index for each @@ -73,11 +72,11 @@ async fn main() -> Result<()> { index * NUM_NFTS_MINTED_PER_TRANSACTION + nft_mint_params.len(), NFT_COLLECTION_SIZE ); - let transaction = account.mint_nfts(nft_mint_params.to_vec(), None).await?; - wait_for_inclusion(&transaction.transaction_id, &account).await?; + let transaction = wallet.mint_nfts(nft_mint_params.to_vec(), None).await?; + wait_for_inclusion(&transaction.transaction_id, &wallet).await?; // Sync so the new outputs are available again for new transactions - account.sync(None).await?; + wallet.sync(None).await?; } // After the NFTs are minted, the issuer nft can be sent to the so called "null address" @@ -105,14 +104,14 @@ fn get_immutable_metadata(index: usize) -> Irc27Metadata { .with_collection_name("Shimmer OG") } -async fn wait_for_inclusion(transaction_id: &TransactionId, account: &Account) -> Result<()> { +async fn wait_for_inclusion(transaction_id: &TransactionId, wallet: &Wallet) -> Result<()> { println!( "Transaction sent: {}/transaction/{}", std::env::var("EXPLORER_URL").unwrap(), transaction_id ); // Wait for transaction to get included - let block_id = account + let block_id = wallet .reissue_transaction_until_included(transaction_id, None, None) .await?; println!( diff --git a/sdk/examples/how_tos/nfts/burn_nft.rs b/sdk/examples/how_tos/nfts/burn_nft.rs index d6f55adce4..84a4ab2df5 100644 --- a/sdk/examples/how_tos/nfts/burn_nft.rs +++ b/sdk/examples/how_tos/nfts/burn_nft.rs @@ -4,7 +4,7 @@ //! In this example we will burn an existing nft output. //! //! Make sure that `STRONGHOLD_SNAPSHOT_PATH` and `WALLET_DB_PATH` already exist by -//! running the `./how_tos/accounts_and_addresses/create_account.rs` example! +//! running the `./how_tos/accounts_and_addresses/create_wallet.rs` example! //! //! Rename `.env.example` to `.env` first, then run the command: //! ```sh @@ -23,11 +23,9 @@ async fn main() -> Result<()> { .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) .finish() .await?; - let alias = "Alice"; - let account = wallet.get_account(alias).await?; - // May want to ensure the account is synced before sending a transaction. - let balance = account.sync(None).await?; + // May want to ensure the wallet is synced before sending a transaction. + let balance = wallet.sync(None).await?; // Get the first nft if let Some(nft_id) = balance.nfts().first() { @@ -39,10 +37,10 @@ async fn main() -> Result<()> { .set_stronghold_password(std::env::var("STRONGHOLD_PASSWORD").unwrap()) .await?; - let transaction = account.burn(*nft_id, None).await?; + let transaction = wallet.burn(*nft_id, None).await?; println!("Transaction sent: {}", transaction.transaction_id); - let block_id = account + let block_id = wallet .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; @@ -54,11 +52,11 @@ async fn main() -> Result<()> { println!("Burned NFT '{}'", nft_id); - let balance = account.sync(None).await?; + let balance = wallet.sync(None).await?; let nfts_after = balance.nfts(); println!("Balance after burning:\n{nfts_after:#?}",); } else { - println!("No NFT available in account '{alias}'"); + println!("No NFT available in the wallet"); } Ok(()) diff --git a/sdk/examples/how_tos/nfts/mint_nft.rs b/sdk/examples/how_tos/nfts/mint_nft.rs index 91b7c9cbaf..be2a795532 100644 --- a/sdk/examples/how_tos/nfts/mint_nft.rs +++ b/sdk/examples/how_tos/nfts/mint_nft.rs @@ -4,7 +4,7 @@ //! In this example we will mint some NFTs. //! //! Make sure that `STRONGHOLD_SNAPSHOT_PATH` and `WALLET_DB_PATH` already exist by -//! running the `./how_tos/accounts_and_addresses/create_account.rs` example! +//! running the `./how_tos/accounts_and_addresses/create_wallet.rs` example! //! //! Rename `.env.example` to `.env` first, then run the command: //! ```sh @@ -35,20 +35,17 @@ async fn main() -> Result<()> { // This example uses secrets in environment variables for simplicity which should not be done in production. dotenvy::dotenv().ok(); - // Create the wallet + // Get the wallet we generated with `create_wallet`. let wallet = Wallet::builder() .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) .finish() .await?; - // Get the account we generated with `create_account` - let account = wallet.get_account("Alice").await?; + // Ensure the wallet is synced after minting. + wallet.sync(None).await?; - // Ensure the account is synced after minting. - account.sync(None).await?; - - // We send from the first address in the account. - let sender_address = account.first_address_bech32().await; + // We send from the wallet address. + let sender_address = wallet.address().await; // Set the stronghold password wallet @@ -72,11 +69,11 @@ async fn main() -> Result<()> { .try_with_issuer(sender_address.clone())? .with_immutable_metadata(metadata.to_bytes())]; - let transaction = account.mint_nfts(nft_params, None).await?; + let transaction = wallet.mint_nfts(nft_params, None).await?; println!("Transaction sent: {}", transaction.transaction_id); // Wait for transaction to get included - let block_id = account + let block_id = wallet .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; println!( @@ -87,7 +84,7 @@ async fn main() -> Result<()> { println!("Minted NFT 1"); // Build an NFT manually by using the `NftOutputBuilder` - let token_supply = account.client().get_token_supply().await?; + let token_supply = wallet.client().get_token_supply().await?; let outputs = [ // address of the owner of the NFT NftOutputBuilder::new_with_amount(NFT2_AMOUNT, NftId::null()) @@ -97,11 +94,11 @@ async fn main() -> Result<()> { .finish_output(token_supply)?, ]; - let transaction = account.send_outputs(outputs, None).await?; + let transaction = wallet.send_outputs(outputs, None).await?; println!("Transaction sent: {}", transaction.transaction_id); // Wait for transaction to get included - let block_id = account + let block_id = wallet .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; println!( @@ -111,8 +108,8 @@ async fn main() -> Result<()> { ); println!("Minted NFT 2"); - // Ensure the account is synced after minting. - account.sync(None).await?; + // Ensure the wallet is synced after minting. + wallet.sync(None).await?; Ok(()) } diff --git a/sdk/examples/how_tos/nfts/send_nft.rs b/sdk/examples/how_tos/nfts/send_nft.rs index 070c24731a..4c24e104ff 100644 --- a/sdk/examples/how_tos/nfts/send_nft.rs +++ b/sdk/examples/how_tos/nfts/send_nft.rs @@ -4,7 +4,7 @@ //! In this example we will send an NFT. //! //! Make sure that `STRONGHOLD_SNAPSHOT_PATH` and `WALLET_DB_PATH` already exist by -//! running the `./how_tos/accounts_and_addresses/create_account.rs` example! +//! running the `./how_tos/accounts_and_addresses/create_wallet.rs` example! //! //! Rename `.env.example` to `.env` first, then run the command: //! ```sh @@ -29,10 +29,9 @@ async fn main() -> Result<()> { .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) .finish() .await?; - let account = wallet.get_account("Alice").await?; - // May want to ensure the account is synced before sending a transaction. - let balance = account.sync(None).await?; + // May want to ensure the wallet is synced before sending a transaction. + let balance = wallet.sync(None).await?; // Get the first nft if let Some(nft_id) = balance.nfts().first() { @@ -45,11 +44,11 @@ async fn main() -> Result<()> { println!("Sending NFT '{}' to '{}'...", nft_id, RECV_ADDRESS); - let transaction = account.send_nft(outputs, None).await?; + let transaction = wallet.send_nft(outputs, None).await?; println!("Transaction sent: {}", transaction.transaction_id); // Wait for transaction to get included - let block_id = account + let block_id = wallet .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; diff --git a/sdk/examples/how_tos/simple_transaction/request_funds.rs b/sdk/examples/how_tos/simple_transaction/request_funds.rs index 6203f8829b..2b19ae1d8f 100644 --- a/sdk/examples/how_tos/simple_transaction/request_funds.rs +++ b/sdk/examples/how_tos/simple_transaction/request_funds.rs @@ -1,10 +1,10 @@ // Copyright 2021 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -//! In this example we request funds from the faucet to the first address in the account. +//! In this example we request funds from the faucet to the first address in the wallet. //! //! Make sure that `STRONGHOLD_SNAPSHOT_PATH` and `WALLET_DB_PATH` already exist by -//! running the `./how_tos/accounts_and_addresses/create_account.rs` example! +//! running the `./how_tos/accounts_and_addresses/create_wallet.rs` example! //! //! Rename `.env.example` to `.env` first, then run the command: //! ```sh @@ -22,19 +22,17 @@ async fn main() -> Result<()> { .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) .finish() .await?; - let account = wallet.get_account("Alice").await?; - let balance = account.sync(None).await?; - println!("Account synced"); + let balance = wallet.sync(None).await?; + println!("Wallet synced"); - let addresses = account.addresses().await; + let wallet_address = wallet.address().await; let funds_before = balance.base_coin().available(); println!("Current available funds: {funds_before}"); println!("Requesting funds from faucet..."); - let faucet_response = - request_funds_from_faucet(&std::env::var("FAUCET_URL").unwrap(), addresses[0].address()).await?; + let faucet_response = request_funds_from_faucet(&std::env::var("FAUCET_URL").unwrap(), &wallet_address).await?; println!("Response from faucet: {}", faucet_response.trim_end()); @@ -46,7 +44,7 @@ async fn main() -> Result<()> { println!("Timeout: waiting for funds took too long"); return Ok(()); }; - let balance = account.sync(None).await?; + let balance = wallet.sync(None).await?; let funds_after = balance.base_coin().available(); if funds_after > funds_before { break funds_after; diff --git a/sdk/examples/how_tos/simple_transaction/simple_transaction.rs b/sdk/examples/how_tos/simple_transaction/simple_transaction.rs index f1024a5bd6..16915f60b0 100644 --- a/sdk/examples/how_tos/simple_transaction/simple_transaction.rs +++ b/sdk/examples/how_tos/simple_transaction/simple_transaction.rs @@ -4,7 +4,7 @@ //! In this example we will issue a simple base coin transaction. //! //! Make sure that `STRONGHOLD_SNAPSHOT_PATH` and `WALLET_DB_PATH` already exist by -//! running the `./how_tos/accounts_and_addresses/create_account.rs` example! +//! running the `./how_tos/accounts_and_addresses/create_wallet.rs` example! //! //! Rename `.env.example` to `.env` first, then run the command: //! ```sh @@ -27,9 +27,8 @@ async fn main() -> Result<()> { .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) .finish() .await?; - let account = wallet.get_account("Alice").await?; - // May want to ensure the account is synced before sending a transaction. + // May want to ensure the wallet is synced before sending a transaction. let _ = wallet.sync(None).await?; // Set the stronghold password @@ -38,10 +37,10 @@ async fn main() -> Result<()> { .await?; println!("Trying to send '{}' coins to '{}'...", SEND_AMOUNT, RECV_ADDRESS); - let transaction = account.send(SEND_AMOUNT, RECV_ADDRESS, None).await?; + let transaction = wallet.send(SEND_AMOUNT, RECV_ADDRESS, None).await?; // Wait for transaction to get included - let block_id = account + let block_id = wallet .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; diff --git a/sdk/examples/wallet/17_check_unlock_conditions.rs b/sdk/examples/wallet/17_check_unlock_conditions.rs index 4c26147502..6a40bd3cfc 100644 --- a/sdk/examples/wallet/17_check_unlock_conditions.rs +++ b/sdk/examples/wallet/17_check_unlock_conditions.rs @@ -1,10 +1,10 @@ // Copyright 2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -//! In this example we check if an output has only an address unlock condition and that the address is from the account. +//! In this example we check if an output has only an address unlock condition and that the address is from the wallet. //! //! Make sure that `STRONGHOLD_SNAPSHOT_PATH` and `WALLET_DB_PATH` already exist by -//! running the `./how_tos/accounts_and_addresses/create_account.rs` example! +//! running the `./how_tos/accounts_and_addresses/create_wallet.rs` example! //! //! ```sh //! cargo run --release --all-features --example check_unlock_conditions @@ -31,30 +31,22 @@ async fn main() -> Result<()> { .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) .finish() .await?; - let account = wallet.get_account("Alice").await?; - let account_addresses = account - .addresses() - .await - .into_iter() - .map(|a| a.into_bech32()) - .collect::>(); + let wallet_address = wallet.address().await; - println!("ADDRESSES:\n{:#?}", account_addresses); + println!("Wallet address:\n{:#?}", wallet_address); let output = BasicOutputBuilder::new_with_amount(AMOUNT) - .add_unlock_condition(AddressUnlockCondition::new(account_addresses[0].as_ref().clone())) - .finish_output(account.client().get_token_supply().await?)?; + .add_unlock_condition(AddressUnlockCondition::new(wallet_address.clone())) + .finish_output(wallet.client().get_token_supply().await?)?; let controlled_by_account = if let [UnlockCondition::Address(address_unlock_condition)] = output .unlock_conditions() .expect("output needs to have unlock conditions") .as_ref() { - // Check that address in the unlock condition belongs to the account - account_addresses - .iter() - .any(|address| address.as_ref() == address_unlock_condition.address()) + // Check that the address in the unlock condition belongs to the wallet + wallet_address.inner() == address_unlock_condition.address() } else { false }; diff --git a/sdk/examples/wallet/accounts.rs b/sdk/examples/wallet/accounts.rs deleted file mode 100644 index 28230f697b..0000000000 --- a/sdk/examples/wallet/accounts.rs +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright 2021 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -//! In this example we will print the details of two accounts in the wallet. If an account doesn't exist yet it will be -//! created. For the second account it will generate as many addresses as defined in the constant. -//! -//! 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 accounts -//! ``` - -use iota_sdk::{ - client::{ - constants::SHIMMER_COIN_TYPE, - secret::{mnemonic::MnemonicSecretManager, SecretManager}, - utils::request_funds_from_faucet, - }, - wallet::{ClientOptions, Result, Wallet}, -}; - -// The number of addresses to generate -const NUM_ADDRESSES_TO_GENERATE: u32 = 5; - -#[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(); - - let client_options = ClientOptions::new().with_node(&std::env::var("NODE_URL").unwrap())?; - - let secret_manager = MnemonicSecretManager::try_from_mnemonic(std::env::var("MNEMONIC").unwrap())?; - - let wallet = Wallet::builder() - .with_secret_manager(SecretManager::Mnemonic(secret_manager)) - .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) - .with_client_options(client_options) - .with_coin_type(SHIMMER_COIN_TYPE) - .finish() - .await?; - - // Get or create first account - let _ = wallet.get_or_create_account("Alice").await?; - - // Get or create second account - let alias2 = "Bob"; - let account2 = wallet.get_or_create_account(alias2).await?; - - let accounts = wallet.get_accounts().await?; - println!("WALLET ACCOUNTS:"); - for account in accounts { - let account = account.details().await; - println!("- {}", account.alias()); - } - - println!("Generating {NUM_ADDRESSES_TO_GENERATE} addresses for account '{alias2}'..."); - let addresses = account2 - .generate_ed25519_addresses(NUM_ADDRESSES_TO_GENERATE, None) - .await?; - - let balance = account2.sync(None).await?; - let funds_before = balance.base_coin().available(); - println!("Current available funds: {funds_before}"); - - println!("Requesting funds from faucet..."); - let faucet_response = - request_funds_from_faucet(&std::env::var("FAUCET_URL").unwrap(), addresses[0].address()).await?; - println!("Response from faucet: {}", faucet_response.trim_end()); - - println!("Waiting for funds (timeout=60s)..."); - // Check for changes to the balance - let start = std::time::Instant::now(); - let balance = loop { - if start.elapsed().as_secs() > 60 { - println!("Timeout: waiting for funds took too long"); - return Ok(()); - }; - let now = tokio::time::Instant::now(); - let balance = account2.sync(None).await?; - if balance.base_coin().available() > funds_before { - println!("Account synced in: {:.2?}", now.elapsed()); - break balance; - } else { - tokio::time::sleep(instant::Duration::from_secs(2)).await; - } - }; - - println!("New available funds: {}", balance.base_coin().available()); - - let addresses = account2.addresses().await; - println!("Number of addresses in {alias2}'s account: {}", addresses.len()); - println!("{alias2}'s base coin balance:\n{:#?}", balance.base_coin()); - - Ok(()) -} diff --git a/sdk/examples/wallet/background_syncing.rs b/sdk/examples/wallet/background_syncing.rs index 0f8217feb8..9cb953ebca 100644 --- a/sdk/examples/wallet/background_syncing.rs +++ b/sdk/examples/wallet/background_syncing.rs @@ -1,7 +1,7 @@ // Copyright 2021 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -//! In this example we will sync an account in the background. +//! In this example we will sync a wallet in the background. //! //! Rename `.env.example` to `.env` first, then run the command: //! ```sh @@ -14,6 +14,7 @@ use iota_sdk::{ request_funds_from_faucet, secret::{mnemonic::MnemonicSecretManager, SecretManager}, }, + crypto::keys::bip44::Bip44, wallet::{ClientOptions, Result, Wallet}, }; @@ -29,16 +30,14 @@ async fn main() -> Result<()> { .with_secret_manager(SecretManager::Mnemonic(secret_manager)) .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) .with_client_options(client_options) - .with_coin_type(SHIMMER_COIN_TYPE) + .with_bip_path(Bip44::new(SHIMMER_COIN_TYPE)) .finish() .await?; - // Get or create new account - let account = wallet.get_or_create_account("Alice").await?; - let addresses = account.addresses().await; + let wallet_address = wallet.address().await; // Manually sync to ensure we have the correct funds to start with - let balance = account.sync(None).await?; + let balance = wallet.sync(None).await?; let funds_before = balance.base_coin().available(); println!("Current available funds: {funds_before}"); @@ -46,8 +45,7 @@ async fn main() -> Result<()> { println!("Started background syncing"); println!("Requesting funds from faucet..."); - let faucet_response = - request_funds_from_faucet(&std::env::var("FAUCET_URL").unwrap(), addresses[0].address()).await?; + let faucet_response = request_funds_from_faucet(&std::env::var("FAUCET_URL").unwrap(), &wallet_address).await?; println!("Response from faucet: {}", faucet_response.trim_end()); println!("Waiting for funds (timeout=60s)..."); @@ -59,7 +57,7 @@ async fn main() -> Result<()> { return Ok(()); }; // We just query the balance and don't manually sync - let balance = account.balance().await?; + let balance = wallet.balance().await?; let funds_after = balance.base_coin().available(); if funds_after > funds_before { break funds_after; diff --git a/sdk/examples/wallet/events.rs b/sdk/examples/wallet/events.rs index c883e0548a..9357ae6892 100644 --- a/sdk/examples/wallet/events.rs +++ b/sdk/examples/wallet/events.rs @@ -14,6 +14,7 @@ use iota_sdk::{ constants::SHIMMER_COIN_TYPE, secret::{mnemonic::MnemonicSecretManager, SecretManager}, }, + crypto::keys::bip44::Bip44, types::block::{ address::Address, output::{unlock_condition::AddressUnlockCondition, BasicOutputBuilder}, @@ -39,31 +40,28 @@ async fn main() -> Result<()> { .with_secret_manager(SecretManager::Mnemonic(secret_manager)) .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) .with_client_options(client_options) - .with_coin_type(SHIMMER_COIN_TYPE) + .with_bip_path(Bip44::new(SHIMMER_COIN_TYPE)) .finish() .await?; wallet .listen([], move |event| { - println!("RECEIVED AN EVENT:\n{:?}", event.event); + println!("RECEIVED AN EVENT:\n{:?}", event); }) .await; - // Get or create an account - let account = wallet.get_or_create_account("Alice").await?; - - let balance = account.sync(None).await?; + let balance = wallet.sync(None).await?; println!("Balance BEFORE:\n{:#?}", balance.base_coin()); // send transaction let outputs = [BasicOutputBuilder::new_with_amount(SEND_AMOUNT) .add_unlock_condition(AddressUnlockCondition::new(Address::try_from_bech32(RECV_ADDRESS)?)) - .finish_output(account.client().get_token_supply().await?)?]; + .finish_output(wallet.client().get_token_supply().await?)?]; - let transaction = account.send_outputs(outputs, None).await?; + let transaction = wallet.send_outputs(outputs, None).await?; println!("Transaction sent: {}", transaction.transaction_id); - let block_id = account + let block_id = wallet .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; @@ -73,7 +71,7 @@ async fn main() -> Result<()> { block_id ); - let balance = account.sync(None).await?; + let balance = wallet.sync(None).await?; println!("Balance AFTER:\n{:#?}", balance.base_coin()); Ok(()) diff --git a/sdk/examples/wallet/getting_started.rs b/sdk/examples/wallet/getting_started.rs index 1ec1488ebf..35b1431d23 100644 --- a/sdk/examples/wallet/getting_started.rs +++ b/sdk/examples/wallet/getting_started.rs @@ -1,8 +1,7 @@ // Copyright 2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -//! 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. +//! In this example we will first create a new wallet and a mnemonic, and then, print the wallet's address. //! //! Rename `.env.example` to `.env` first, then run the command: //! ```sh @@ -14,6 +13,7 @@ use iota_sdk::{ constants::SHIMMER_COIN_TYPE, secret::{stronghold::StrongholdSecretManager, SecretManager}, }, + crypto::keys::bip44::Bip44, wallet::{ClientOptions, Result, Wallet}, }; @@ -23,7 +23,7 @@ async fn main() -> Result<()> { // 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. + .build("vault.stronghold")?; // The path to store the wallet snapshot. let client_options = ClientOptions::new().with_node("https://api.testnet.shimmer.network")?; @@ -31,8 +31,9 @@ async fn main() -> Result<()> { let wallet = Wallet::builder() .with_secret_manager(SecretManager::Stronghold(secret_manager)) .with_client_options(client_options) - .with_coin_type(SHIMMER_COIN_TYPE) .with_storage_path("getting-started-db") + .with_bip_path(Bip44::new(SHIMMER_COIN_TYPE)) + .with_alias("Alice".to_string()) .finish() .await?; @@ -42,15 +43,8 @@ async fn main() -> Result<()> { println!("Mnemonic: {}", mnemonic.as_ref()); 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?; - - let first_address = &account.addresses().await[0]; - println!("{}", first_address.address()); + let wallet_address = wallet.address().await; + println!("{}", wallet_address); Ok(()) } diff --git a/sdk/examples/wallet/ledger_nano.rs b/sdk/examples/wallet/ledger_nano.rs index 75a65a0ae8..e428af78cb 100644 --- a/sdk/examples/wallet/ledger_nano.rs +++ b/sdk/examples/wallet/ledger_nano.rs @@ -19,13 +19,10 @@ use iota_sdk::{ constants::SHIMMER_COIN_TYPE, secret::{ledger_nano::LedgerSecretManager, SecretManager}, }, + crypto::keys::bip44::Bip44, wallet::{ClientOptions, Result, Wallet}, }; -// The account alias used in this example -const ACCOUNT_ALIAS: &str = "ledger"; -// The number of addresses to generate -const NUM_ADDRESSES_TO_GENERATE: u32 = 1; // The address to send coins to const RECV_ADDRESS: &str = "rms1qpszqzadsym6wpppd6z037dvlejmjuke7s24hm95s9fg9vpua7vluaw60xu"; // The amount of base coins we'll send @@ -42,35 +39,30 @@ async fn main() -> Result<()> { .with_secret_manager(SecretManager::LedgerNano(secret_manager)) .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) .with_client_options(client_options) - .with_coin_type(SHIMMER_COIN_TYPE) + .with_bip_path(Bip44::new(SHIMMER_COIN_TYPE)) .finish() .await?; println!("{:?}", wallet.get_ledger_nano_status().await?); - // Get or create a new account - let account = wallet.get_or_create_account(ACCOUNT_ALIAS).await?; - - println!("Generating {NUM_ADDRESSES_TO_GENERATE} addresses..."); + println!("Generating address..."); let now = tokio::time::Instant::now(); - let addresses = account - .generate_ed25519_addresses(NUM_ADDRESSES_TO_GENERATE, None) - .await?; + let address = wallet.generate_ed25519_address(0, 0, None).await?; println!("took: {:.2?}", now.elapsed()); - println!("ADDRESSES:\n{addresses:#?}"); + println!("ADDRESS:\n{address:#?}"); let now = tokio::time::Instant::now(); - let balance = account.sync(None).await?; - println!("Account synced in: {:.2?}", now.elapsed()); + let balance = wallet.sync(None).await?; + println!("Wallet synced in: {:.2?}", now.elapsed()); println!("Balance BEFORE:\n{:?}", balance.base_coin()); println!("Sending the coin-transfer transaction..."); - let transaction = account.send(SEND_AMOUNT, RECV_ADDRESS, None).await?; + let transaction = wallet.send(SEND_AMOUNT, RECV_ADDRESS, None).await?; println!("Transaction sent: {}", transaction.transaction_id); - let block_id = account + let block_id = wallet .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; println!( @@ -80,8 +72,8 @@ async fn main() -> Result<()> { ); let now = tokio::time::Instant::now(); - let balance = account.sync(None).await?; - println!("Account synced in: {:.2?}", now.elapsed()); + let balance = wallet.sync(None).await?; + println!("Wallet synced in: {:.2?}", now.elapsed()); println!("Balance AFTER:\n{:?}", balance.base_coin()); diff --git a/sdk/examples/wallet/logger.rs b/sdk/examples/wallet/logger.rs index 4cb69e54ca..43033a3bef 100644 --- a/sdk/examples/wallet/logger.rs +++ b/sdk/examples/wallet/logger.rs @@ -13,12 +13,10 @@ use iota_sdk::{ constants::SHIMMER_COIN_TYPE, secret::{mnemonic::MnemonicSecretManager, SecretManager}, }, + crypto::keys::bip44::Bip44, wallet::{ClientOptions, Result, Wallet}, }; -// The number of addresses to generate -const NUM_ADDRESSES_TO_GENERATE: u32 = 5; - #[tokio::main] async fn main() -> Result<()> { // This example uses secrets in environment variables for simplicity which should not be done in production. @@ -41,20 +39,15 @@ async fn main() -> Result<()> { .with_secret_manager(SecretManager::Mnemonic(secret_manager)) .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) .with_client_options(client_options) - .with_coin_type(SHIMMER_COIN_TYPE) + .with_bip_path(Bip44::new(SHIMMER_COIN_TYPE)) .finish() .await?; - // Get or create a new account - let account = wallet.get_or_create_account("Alice").await?; - - println!("Generating {NUM_ADDRESSES_TO_GENERATE} addresses..."); - let _ = account - .generate_ed25519_addresses(NUM_ADDRESSES_TO_GENERATE, None) - .await?; + println!("Generating address..."); + let _ = wallet.generate_ed25519_address(0, 0, None).await?; - println!("Syncing account"); - account.sync(None).await?; + println!("Syncing wallet"); + wallet.sync(None).await?; println!("Example finished successfully"); Ok(()) diff --git a/sdk/examples/wallet/offline_signing/0_generate_addresses.rs b/sdk/examples/wallet/offline_signing/0_generate_address.rs similarity index 69% rename from sdk/examples/wallet/offline_signing/0_generate_addresses.rs rename to sdk/examples/wallet/offline_signing/0_generate_address.rs index 30357e2819..bc5f4a8330 100644 --- a/sdk/examples/wallet/offline_signing/0_generate_addresses.rs +++ b/sdk/examples/wallet/offline_signing/0_generate_address.rs @@ -5,7 +5,7 @@ //! //! Rename `.env.example` to `.env` first, then run the command: //! ```sh -//! cargo run --release --all-features --example 0_generate_addresses +//! cargo run --release --all-features --example 0_generate_address //! ``` use iota_sdk::{ @@ -13,13 +13,13 @@ use iota_sdk::{ constants::{SHIMMER_BECH32_HRP, SHIMMER_COIN_TYPE}, secret::{stronghold::StrongholdSecretManager, SecretManager}, }, - crypto::keys::bip39::Mnemonic, - wallet::{Account, ClientOptions, Result, Wallet}, + crypto::keys::{bip39::Mnemonic, bip44::Bip44}, + wallet::{ClientOptions, Result, Wallet}, }; const OFFLINE_WALLET_DB_PATH: &str = "./examples/wallet/offline_signing/example-offline-walletdb"; const STRONGHOLD_SNAPSHOT_PATH: &str = "./examples/wallet/offline_signing/example.stronghold"; -const ADDRESSES_FILE_PATH: &str = "./examples/wallet/offline_signing/example.addresses.json"; +const ADDRESS_FILE_PATH: &str = "./examples/wallet/offline_signing/example.address.json"; #[tokio::main] async fn main() -> Result<()> { @@ -43,30 +43,22 @@ async fn main() -> Result<()> { .with_secret_manager(SecretManager::Stronghold(secret_manager)) .with_storage_path(OFFLINE_WALLET_DB_PATH) .with_client_options(offline_client) - .with_coin_type(SHIMMER_COIN_TYPE) + .with_bip_path(Bip44::new(SHIMMER_COIN_TYPE)) .finish() .await?; - // Create a new account - let account = wallet - .create_account() - .with_alias("Alice") - .with_bech32_hrp(SHIMMER_BECH32_HRP) - .finish() - .await?; - - println!("Generated a new account '{}'", account.alias().await); + println!("Generated a new wallet"); - write_addresses_to_file(&account).await + write_wallet_address_to_file(&wallet).await } -async fn write_addresses_to_file(account: &Account) -> Result<()> { +async fn write_wallet_address_to_file(wallet: &Wallet) -> Result<()> { use tokio::io::AsyncWriteExt; - let addresses = account.addresses().await; - let json = serde_json::to_string_pretty(&addresses)?; - let mut file = tokio::io::BufWriter::new(tokio::fs::File::create(ADDRESSES_FILE_PATH).await?); - println!("example.addresses.json:\n{json}"); + let wallet_address = wallet.address().await; + let json = serde_json::to_string_pretty(&wallet_address)?; + let mut file = tokio::io::BufWriter::new(tokio::fs::File::create(ADDRESS_FILE_PATH).await?); + println!("example.address.json:\n{json}"); file.write_all(json.as_bytes()).await?; file.flush().await?; Ok(()) diff --git a/sdk/examples/wallet/offline_signing/1_prepare_transaction.rs b/sdk/examples/wallet/offline_signing/1_prepare_transaction.rs index ea9c3a0564..d9eee607cc 100644 --- a/sdk/examples/wallet/offline_signing/1_prepare_transaction.rs +++ b/sdk/examples/wallet/offline_signing/1_prepare_transaction.rs @@ -14,11 +14,12 @@ use iota_sdk::{ constants::SHIMMER_COIN_TYPE, secret::SecretManager, }, - wallet::{account::types::Bip44Address, ClientOptions, Result, SendParams, Wallet}, + crypto::keys::bip44::Bip44, + wallet::{types::Bip44Address, ClientOptions, Result, SendParams, Wallet}, }; const ONLINE_WALLET_DB_PATH: &str = "./examples/wallet/offline_signing/example-online-walletdb"; -const ADDRESSES_FILE_PATH: &str = "./examples/wallet/offline_signing/example.addresses.json"; +const ADDRESS_FILE_PATH: &str = "./examples/wallet/offline_signing/example.address.json"; const PREPARED_TRANSACTION_FILE_PATH: &str = "./examples/wallet/offline_signing/example.prepared_transaction.json"; // Address to which we want to send the amount. const RECV_ADDRESS: &str = "rms1qpszqzadsym6wpppd6z037dvlejmjuke7s24hm95s9fg9vpua7vluaw60xu"; @@ -33,7 +34,7 @@ async fn main() -> Result<()> { let params = [SendParams::new(SEND_AMOUNT, RECV_ADDRESS)?]; // Recovers addresses from example `0_address_generation`. - let addresses = read_addresses_from_file().await?; + let address = read_address_from_file().await?.into_bech32(); let client_options = ClientOptions::new().with_node(&std::env::var("NODE_URL").unwrap())?; @@ -42,22 +43,15 @@ async fn main() -> Result<()> { .with_secret_manager(SecretManager::Placeholder) .with_storage_path(ONLINE_WALLET_DB_PATH) .with_client_options(client_options.clone()) - .with_coin_type(SHIMMER_COIN_TYPE) + .with_bip_path(Bip44::new(SHIMMER_COIN_TYPE)) + .with_address(address) .finish() .await?; - // Create a new account - let account = wallet - .create_account() - .with_alias("Alice") - .with_addresses(addresses) - .finish() - .await?; - - // Sync the account to get the outputs for the addresses - account.sync(None).await?; + // Sync the wallet to get the outputs for the addresses + wallet.sync(None).await?; - let prepared_transaction = account.prepare_send(params.clone(), None).await?; + let prepared_transaction = wallet.prepare_send(params.clone(), None).await?; println!("Prepared transaction sending {params:?}"); @@ -66,10 +60,10 @@ async fn main() -> Result<()> { Ok(()) } -async fn read_addresses_from_file() -> Result> { +async fn read_address_from_file() -> Result { use tokio::io::AsyncReadExt; - let mut file = tokio::io::BufReader::new(tokio::fs::File::open(ADDRESSES_FILE_PATH).await?); + let mut file = tokio::io::BufReader::new(tokio::fs::File::open(ADDRESS_FILE_PATH).await?); let mut json = String::new(); file.read_to_string(&mut json).await?; diff --git a/sdk/examples/wallet/offline_signing/3_send_transaction.rs b/sdk/examples/wallet/offline_signing/3_send_transaction.rs index eeace0f994..8d1c91f13e 100644 --- a/sdk/examples/wallet/offline_signing/3_send_transaction.rs +++ b/sdk/examples/wallet/offline_signing/3_send_transaction.rs @@ -15,7 +15,7 @@ use iota_sdk::{ Client, }, types::{block::payload::signed_transaction::TransactionId, TryFromDto}, - wallet::{Account, Result}, + wallet::Result, Wallet, }; @@ -34,16 +34,13 @@ async fn main() -> Result<()> { .finish() .await?; - // Create a new account - let account = wallet.get_account("Alice").await?; - - let signed_transaction_data = read_signed_transaction_from_file(account.client()).await?; + let signed_transaction_data = read_signed_transaction_from_file(wallet.client()).await?; // Sends offline signed transaction online. - let transaction = account + let transaction = wallet .submit_and_store_transaction(signed_transaction_data, None) .await?; - wait_for_inclusion(&transaction.transaction_id, &account).await?; + wait_for_inclusion(&transaction.transaction_id, &wallet).await?; Ok(()) } @@ -63,14 +60,14 @@ async fn read_signed_transaction_from_file(client: &Client) -> Result Result<()> { +async fn wait_for_inclusion(transaction_id: &TransactionId, wallet: &Wallet) -> Result<()> { println!( "Transaction sent: {}/transaction/{}", std::env::var("EXPLORER_URL").unwrap(), transaction_id ); // Wait for transaction to get included - let block_id = account + let block_id = wallet .reissue_transaction_until_included(transaction_id, None, None) .await?; println!( diff --git a/sdk/examples/wallet/participation.rs b/sdk/examples/wallet/participation.rs index bfa7129fe3..d55f4059ea 100644 --- a/sdk/examples/wallet/participation.rs +++ b/sdk/examples/wallet/participation.rs @@ -8,7 +8,7 @@ //! * if a voting occurred, stops the voting and destroys the voting output //! //! Make sure that `STRONGHOLD_SNAPSHOT_PATH` and `WALLET_DB_PATH` already exist by -//! running the `./how_tos/accounts_and_addresses/create_account.rs` example and there are funds on the first address +//! running the `./how_tos/accounts_and_addresses/create_wallet.rs` example and there are funds on the first address //! by running the `get_funds` example! //! //! Rename `.env.example` to `.env` first, then run the command: @@ -18,7 +18,7 @@ use iota_sdk::{ client::node_manager::node::Node, - wallet::{account::types::participation::ParticipationEventRegistrationOptions, Result}, + wallet::{types::participation::ParticipationEventRegistrationOptions, Result}, Wallet, }; use url::Url; @@ -46,7 +46,6 @@ async fn main() -> Result<()> { .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) .finish() .await?; - let account = wallet.get_account("Alice").await?; // Provide the stronghold password wallet @@ -59,7 +58,7 @@ async fn main() -> Result<()> { auth: None, disabled: false, }; - let _ = account + let _ = wallet .register_participation_events(&ParticipationEventRegistrationOptions { node, // We ignore this particular event @@ -72,7 +71,7 @@ async fn main() -> Result<()> { .await?; println!("Registered events:"); - let registered_participation_events = account.get_participation_events().await?; + let registered_participation_events = wallet.get_participation_events().await?; for (i, (id, event)) in registered_participation_events.iter().enumerate() { println!("EVENT #{i}"); println!( @@ -86,11 +85,11 @@ async fn main() -> Result<()> { } println!("Checking for participation event '{PARTICIPATION_EVENT_ID_1}'"); - if let Ok(Some(event)) = account.get_participation_event(event_id).await { + if let Ok(Some(event)) = wallet.get_participation_event(event_id).await { println!("{event:#?}"); println!("Getting event status for '{PARTICIPATION_EVENT_ID_1}'"); - let event_status = account.get_participation_event_status(&event_id).await?; + let event_status = wallet.get_participation_event_status(&event_id).await?; println!("{event_status:#?}"); } else { println!("Event not found"); @@ -100,12 +99,12 @@ async fn main() -> Result<()> { // deregister an event //////////////////////////////////////////////// if !DEREGISTERED_PARTICIPATION_EVENT.is_empty() { - account + wallet .deregister_participation_event(&DEREGISTERED_PARTICIPATION_EVENT.parse()?) .await?; println!("Registered events (updated):"); - let registered_participation_events = account.get_participation_events().await?; + let registered_participation_events = wallet.get_participation_events().await?; for (i, (id, event)) in registered_participation_events.iter().enumerate() { println!("EVENT #{i}"); println!( @@ -119,8 +118,8 @@ async fn main() -> Result<()> { } } - let balance = account.sync(None).await?; - println!("Account synced"); + let balance = wallet.sync(None).await?; + println!("Wallet synced"); //////////////////////////////////////////////// // create voting output or increase voting power @@ -129,11 +128,11 @@ async fn main() -> Result<()> { println!("Current voting power: {}", balance.base_coin().voting_power()); println!("Sending transaction to increase voting power..."); - let transaction = account.increase_voting_power(INCREASE_VOTING_POWER_AMOUNT).await?; + let transaction = wallet.increase_voting_power(INCREASE_VOTING_POWER_AMOUNT).await?; println!("Transaction sent: {}", transaction.transaction_id); println!("Waiting for `increase voting power` transaction to be included..."); - let block_id = account + let block_id = wallet .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; println!( @@ -142,11 +141,11 @@ async fn main() -> Result<()> { block_id ); - let balance = account.sync(None).await?; - println!("Account synced"); + let balance = wallet.sync(None).await?; + println!("Wallet synced"); println!("New voting power: {}", balance.base_coin().voting_power()); - let voting_output = account.get_voting_output().await?.unwrap(); + let voting_output = wallet.get_voting_output().await?.unwrap(); println!("Voting output:\n{:#?}", voting_output.output); //////////////////////////////////////////////// @@ -154,11 +153,11 @@ async fn main() -> Result<()> { //////////////////////////////////////////////// println!("Sending transaction to decrease voting power..."); - let transaction = account.decrease_voting_power(DECREASE_VOTING_POWER_AMOUNT).await?; + let transaction = wallet.decrease_voting_power(DECREASE_VOTING_POWER_AMOUNT).await?; println!("Transaction sent: {}", transaction.transaction_id); println!("Waiting for `decrease voting power` transaction to be included..."); - let block_id = account + let block_id = wallet .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; println!( @@ -167,8 +166,8 @@ async fn main() -> Result<()> { block_id ); - let balance = account.sync(None).await?; - println!("Account synced"); + let balance = wallet.sync(None).await?; + println!("Wallet synced"); println!("New voting power: {}", balance.base_coin().voting_power()); //////////////////////////////////////////////// @@ -176,14 +175,14 @@ async fn main() -> Result<()> { //////////////////////////////////////////////// println!("Sending transaction to vote..."); - let transaction = account.vote(Some(event_id), Some(vec![0])).await?; + let transaction = wallet.vote(Some(event_id), Some(vec![0])).await?; // NOTE!!! // from here on out, the example will only proceed if you've set up your own participation event and // changed the constants above with a valid (i.e. ongoing) event id for println!("Transaction sent: {}", transaction.transaction_id); println!("Waiting for `vote` transaction to be included..."); - let block_id = account + let block_id = wallet .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; println!( @@ -192,25 +191,25 @@ async fn main() -> Result<()> { block_id ); - account.sync(None).await?; - println!("Account synced"); + wallet.sync(None).await?; + println!("Wallet synced"); //////////////////////////////////////////////// // get voting overview //////////////////////////////////////////////// - let overview = account.get_participation_overview(None).await?; + let overview = wallet.get_participation_overview(None).await?; println!("Particpation overview:\n{overview:?}"); //////////////////////////////////////////////// // stop vote //////////////////////////////////////////////// - let transaction = account.stop_participating(event_id).await?; + let transaction = wallet.stop_participating(event_id).await?; println!("Transaction sent: {}", transaction.transaction_id); println!("Waiting for `stop participating` transaction to be included..."); - let block_id = account + let block_id = wallet .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; println!( @@ -219,22 +218,22 @@ async fn main() -> Result<()> { block_id ); - account.sync(None).await?; - println!("Account synced"); + wallet.sync(None).await?; + println!("Wallet synced"); //////////////////////////////////////////////// // destroy voting output //////////////////////////////////////////////// - let voting_output = account.get_voting_output().await?.unwrap(); + let voting_output = wallet.get_voting_output().await?.unwrap(); println!("Voting output: {:?}", voting_output.output); // Decrease full amount, there should be no voting output afterwards - let transaction = account.decrease_voting_power(voting_output.output.amount()).await?; + let transaction = wallet.decrease_voting_power(voting_output.output.amount()).await?; println!("Transaction sent: {}", transaction.transaction_id); println!("Waiting for `decrease voting power` transaction to be included..."); - let block_id = account + let block_id = wallet .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; println!( @@ -243,10 +242,10 @@ async fn main() -> Result<()> { block_id ); - account.sync(None).await?; - println!("Account synced"); + wallet.sync(None).await?; + println!("Wallet synced"); - assert!(account.get_voting_output().await.is_err()); + assert!(wallet.get_voting_output().await.is_err()); Ok(()) } diff --git a/sdk/examples/wallet/recover_accounts.rs b/sdk/examples/wallet/recover_accounts.rs deleted file mode 100644 index 61a05dfb13..0000000000 --- a/sdk/examples/wallet/recover_accounts.rs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2021 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -//! In this example we will recover a wallet from a given mnemonic. -//! -//! Make sure there's no folder yet at `WALLET_DB_PATH`. -//! -//! Rename `.env.example` to `.env` first, then run the command: -//! ```sh -//! cargo run --release --all-features --example recover_accounts -//! ``` - -use iota_sdk::{ - client::{ - constants::SHIMMER_COIN_TYPE, - secret::{mnemonic::MnemonicSecretManager, SecretManager}, - }, - wallet::{ClientOptions, Result, Wallet}, -}; - -#[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(); - - let client_options = ClientOptions::new().with_node(&std::env::var("NODE_URL").unwrap())?; - - let secret_manager = MnemonicSecretManager::try_from_mnemonic(std::env::var("MNEMONIC").unwrap())?; - - let wallet = Wallet::builder() - .with_secret_manager(SecretManager::Mnemonic(secret_manager)) - .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) - .with_client_options(client_options) - .with_coin_type(SHIMMER_COIN_TYPE) - .finish() - .await?; - - let accounts = wallet.recover_accounts(0, 2, 2, None).await?; - - println!("Recovered {} accounts", accounts.len()); - for account in accounts.iter() { - println!("ACCOUNT #{}:", account.details().await.index()); - let now = tokio::time::Instant::now(); - let balance = account.sync(None).await?; - println!("Account synced in: {:.2?}", now.elapsed()); - println!("Balance: {balance:#?}"); - } - - Ok(()) -} diff --git a/sdk/examples/wallet/spammer.rs b/sdk/examples/wallet/spammer.rs index 52e33d43cb..d16c621fea 100644 --- a/sdk/examples/wallet/spammer.rs +++ b/sdk/examples/wallet/spammer.rs @@ -12,14 +12,17 @@ use iota_sdk::{ client::{ constants::SHIMMER_COIN_TYPE, request_funds_from_faucet, - secret::{mnemonic::MnemonicSecretManager, SecretManager}, + secret::{mnemonic::MnemonicSecretManager, SecretManage, SecretManager}, }, - types::block::{address::Bech32Address, output::BasicOutput, payload::signed_transaction::TransactionId}, - wallet::{account::FilterOptions, Account, ClientOptions, Result, SendParams, Wallet}, + crypto::keys::bip44::Bip44, + types::block::{ + address::{Address, Bech32Address, Hrp}, + output::BasicOutput, + payload::signed_transaction::TransactionId, + }, + wallet::{ClientOptions, FilterOptions, Result, SendParams, Wallet}, }; -// The account alias used in this example. -const ACCOUNT_ALIAS: &str = "spammer"; // The number of spamming rounds. const NUM_ROUNDS: usize = 1000; // The amount to send in each transaction @@ -39,23 +42,34 @@ async fn main() -> Result<()> { // Restore wallet from a mnemonic phrase. let client_options = ClientOptions::new().with_node(&std::env::var("NODE_URL").unwrap())?; let secret_manager = MnemonicSecretManager::try_from_mnemonic(std::env::var("MNEMONIC").unwrap())?; + + let bip_path = Bip44::new(SHIMMER_COIN_TYPE); + let address = Bech32Address::new( + Hrp::from_str_unchecked("smr"), + Address::from( + secret_manager + .generate_ed25519_addresses(bip_path.coin_type, bip_path.account, 0..1, None) + .await?[0], + ), + ); + let wallet = Wallet::builder() .with_secret_manager(SecretManager::Mnemonic(secret_manager)) .with_client_options(client_options) - .with_coin_type(SHIMMER_COIN_TYPE) + .with_bip_path(bip_path) + .with_address(address) .finish() .await?; - let account = wallet.get_or_create_account(ACCOUNT_ALIAS).await?; - let recv_address = account.first_address_bech32().await; + let recv_address = wallet.address().await; println!("Recv address: {}", recv_address); // Ensure there are enough available funds for spamming. - ensure_enough_funds(&account, &recv_address).await?; + ensure_enough_funds(&wallet, &recv_address).await?; // We make sure that for all threads there are always inputs available to // fund the transaction, otherwise we create enough unspent outputs. - let num_unspent_basic_outputs_with_send_amount = account + let num_unspent_basic_outputs_with_send_amount = wallet .unspent_outputs(FilterOptions { output_types: Some(vec![BasicOutput::KIND]), ..Default::default() @@ -70,12 +84,12 @@ async fn main() -> Result<()> { if num_unspent_basic_outputs_with_send_amount < 127 { println!("Creating unspent outputs..."); - let transaction = account + let transaction = wallet .send_with_params(vec![SendParams::new(SEND_AMOUNT, recv_address.clone())?; 127], None) .await?; - wait_for_inclusion(&transaction.transaction_id, &account).await?; + wait_for_inclusion(&transaction.transaction_id, &wallet).await?; - account.sync(None).await?; + wallet.sync(None).await?; } println!("Spamming transactions..."); @@ -86,14 +100,14 @@ async fn main() -> Result<()> { let mut tasks = tokio::task::JoinSet::>::new(); for n in 0..num_simultaneous_txs { - let account_clone = account.clone(); let recv_address = recv_address.clone(); + let wallet = wallet.clone(); tasks.spawn(async move { println!("Thread {n}: sending {SEND_AMOUNT} coins to own address"); let thread_timer = tokio::time::Instant::now(); - let transaction = account_clone + let transaction = wallet .send(SEND_AMOUNT, recv_address, None) .await .map_err(|err| (n, err))?; @@ -123,14 +137,14 @@ async fn main() -> Result<()> { if error_state.is_err() { // Sync when getting an error, because that's probably when no outputs are available anymore - let mut balance = account.sync(None).await?; - println!("Account synced"); + let mut balance = wallet.sync(None).await?; + println!("Wallet synced"); while balance.base_coin().available() == 0 { println!("No funds available"); tokio::time::sleep(std::time::Duration::from_secs(2)).await; - balance = account.sync(None).await?; - println!("Account synced"); + balance = wallet.sync(None).await?; + println!("Wallet synced"); } } @@ -142,8 +156,8 @@ async fn main() -> Result<()> { Ok(()) } -async fn ensure_enough_funds(account: &Account, bech32_address: &Bech32Address) -> Result<()> { - let balance = account.sync(None).await?; +async fn ensure_enough_funds(wallet: &Wallet, bech32_address: &Bech32Address) -> Result<()> { + let balance = wallet.sync(None).await?; let available_funds = balance.base_coin().available(); println!("Available funds: {available_funds}"); let min_required_funds = (1.1f64 * (127u64 * SEND_AMOUNT) as f64) as u64; @@ -163,7 +177,7 @@ async fn ensure_enough_funds(account: &Account, bech32_address: &Bech32Address) if start.elapsed().as_secs() > 60 { panic!("Requesting funds failed (timeout)"); }; - let balance = account.sync(None).await?; + let balance = wallet.sync(None).await?; let available_funds_after = balance.base_coin().available(); if available_funds_after > available_funds { break available_funds_after; @@ -183,14 +197,14 @@ async fn ensure_enough_funds(account: &Account, bech32_address: &Bech32Address) } } -async fn wait_for_inclusion(transaction_id: &TransactionId, account: &Account) -> Result<()> { +async fn wait_for_inclusion(transaction_id: &TransactionId, wallet: &Wallet) -> Result<()> { println!( "Transaction sent: {}/transaction/{}", std::env::var("EXPLORER_URL").unwrap(), transaction_id ); // Wait for transaction to get included - let block_id = account + let block_id = wallet .reissue_transaction_until_included(transaction_id, None, None) .await?; println!( diff --git a/sdk/examples/wallet/split_funds.rs b/sdk/examples/wallet/split_funds.rs deleted file mode 100644 index 49f3be624d..0000000000 --- a/sdk/examples/wallet/split_funds.rs +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright 2021 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -//! In this example we will split funds among a pre-defined number of addresses. -//! -//! Make sure there's no folder yet at `WALLET_DB_PATH`. -//! For this example it's best to use a fresh mnemonic and start with a balance on the first address only. -//! -//! Rename `.env.example` to `.env` first, then run the command: -//! ```sh -//! cargo run --release --all-features --example split_funds -//! ``` - -use iota_sdk::{ - client::{ - constants::SHIMMER_COIN_TYPE, - secret::{mnemonic::MnemonicSecretManager, SecretManager}, - }, - types::block::output::{unlock_condition::AddressUnlockCondition, BasicOutputBuilder}, - wallet::{account::types::Bip44Address, Account, ClientOptions, Result, Wallet}, -}; - -// The base coin amount to send -const SEND_AMOUNT: u64 = 1_000_000; -// The number of addresses funds are distributed to -const ADDRESSES_TO_SPLIT_FUNDS: usize = 15; - -#[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(); - - let client_options = ClientOptions::new().with_node(&std::env::var("NODE_URL").unwrap())?; - - let secret_manager = MnemonicSecretManager::try_from_mnemonic(std::env::var("MNEMONIC").unwrap())?; - - let wallet = Wallet::builder() - .with_secret_manager(SecretManager::Mnemonic(secret_manager)) - .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) - .with_client_options(client_options) - .with_coin_type(SHIMMER_COIN_TYPE) - .finish() - .await?; - - // Get account or create a new one - let account = wallet.get_or_create_account("Alice").await?; - - let _ = ensure_enough_addresses(&account, ADDRESSES_TO_SPLIT_FUNDS).await?; - - let addresses = account.addresses().await; - println!("Total address count: {}", addresses.len()); - - sync_print_balance(&account).await?; - - let addresses_with_unspent_outputs = account.addresses_with_unspent_outputs().await?; - println!( - "Addresses with balance count (before): {}", - addresses_with_unspent_outputs.len() - ); - - let token_supply = account.client().get_token_supply().await?; - - // Send split transactions - for addresses_chunk in addresses.chunks(2).map(|chunk| chunk.to_vec()) { - let outputs_per_transaction = addresses_chunk - .into_iter() - .map(|a| { - BasicOutputBuilder::new_with_amount(SEND_AMOUNT) - .add_unlock_condition(AddressUnlockCondition::new(a.address())) - .finish_output(token_supply) - .unwrap() - }) - .collect::>(); - - println!( - "Sending '{}' coins in {} outputs...", - SEND_AMOUNT, - outputs_per_transaction.len() - ); - let transaction = account.send_outputs(outputs_per_transaction, None).await?; - println!( - "Transaction sent: {}/transaction/{}", - std::env::var("EXPLORER_URL").unwrap(), - transaction.transaction_id - ); - - // Wait for transaction to get included - let block_id = account - .reissue_transaction_until_included(&transaction.transaction_id, None, None) - .await?; - - println!( - "Block included: {}/block/{}", - std::env::var("EXPLORER_URL").unwrap(), - block_id - ); - } - - sync_print_balance(&account).await?; - - let addresses_with_unspent_outputs = account.addresses_with_unspent_outputs().await?; - println!( - "Addresses with balance count (after): {}", - addresses_with_unspent_outputs.len() - ); - - println!("Example finished successfully"); - - Ok(()) -} - -async fn sync_print_balance(account: &Account) -> Result<()> { - let alias = account.alias().await; - let now = tokio::time::Instant::now(); - let balance = account.sync(None).await?; - println!("{alias}'s account synced in: {:.2?}", now.elapsed()); - println!("{alias}'s balance:\n{:#?}", balance.base_coin()); - Ok(()) -} - -async fn ensure_enough_addresses(account: &Account, limit: usize) -> Result> { - let alias = account.alias().await; - if account.addresses().await.len() < limit { - let num_addresses_to_generate = limit - account.addresses().await.len(); - println!("Generating {num_addresses_to_generate} addresses for account '{alias}'..."); - account - .generate_ed25519_addresses(num_addresses_to_generate as u32, None) - .await?; - } - Ok(account.addresses().await) -} diff --git a/sdk/examples/wallet/storage.rs b/sdk/examples/wallet/storage.rs index 98c3bcdf66..fa1b3b571d 100644 --- a/sdk/examples/wallet/storage.rs +++ b/sdk/examples/wallet/storage.rs @@ -13,12 +13,10 @@ use iota_sdk::{ constants::SHIMMER_COIN_TYPE, secret::{mnemonic::MnemonicSecretManager, SecretManager}, }, - wallet::{account::types::Bip44Address, Account, ClientOptions, Result, Wallet}, + crypto::keys::bip44::Bip44, + wallet::{types::Bip44Address, ClientOptions, Result, Wallet}, }; -// The maximum number of addresses to generate -const MAX_ADDRESSES_TO_GENERATE: usize = 3; - #[tokio::main] async fn main() -> Result<()> { // This example uses secrets in environment variables for simplicity which should not be done in production. @@ -32,48 +30,25 @@ async fn main() -> Result<()> { .with_secret_manager(SecretManager::Mnemonic(secret_manager)) .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) .with_client_options(client_options) - .with_coin_type(SHIMMER_COIN_TYPE) + .with_bip_path(Bip44::new(SHIMMER_COIN_TYPE)) .finish() .await?; - // Get account or create a new one - let account = wallet.get_or_create_account("Alice").await?; - - let addresses = generate_max_addresses(&account, MAX_ADDRESSES_TO_GENERATE).await?; - let bech32_addresses = addresses - .into_iter() - .map(|address| address.into_bech32()) - .collect::>(); - - println!("Total address count:\n{:?}", account.addresses().await.len()); - println!("ADDRESSES:\n{bech32_addresses:#?}"); + let bech32_address = wallet.address().await; - sync_print_balance(&account).await?; + println!("ADDRESS:\n{bech32_address}"); - #[cfg(debug_assertions)] - wallet.verify_integrity().await?; + sync_print_balance(&wallet).await?; println!("Example finished successfully"); Ok(()) } -async fn generate_max_addresses(account: &Account, max: usize) -> Result> { - let alias = account.alias().await; - if account.addresses().await.len() < max { - let num_addresses_to_generate = max - account.addresses().await.len(); - println!("Generating {num_addresses_to_generate} addresses for account '{alias}'..."); - account - .generate_ed25519_addresses(num_addresses_to_generate as u32, None) - .await?; - } - Ok(account.addresses().await) -} - -async fn sync_print_balance(account: &Account) -> Result<()> { - let alias = account.alias().await; +async fn sync_print_balance(wallet: &Wallet) -> Result<()> { + let alias = wallet.alias().await; let now = tokio::time::Instant::now(); - let balance = account.sync(None).await?; - println!("{alias}'s account synced in: {:.2?}", now.elapsed()); - println!("{alias}'s balance:\n{:#?}", balance.base_coin()); + let balance = wallet.sync(None).await?; + println!("Wallet synced in: {:.2?}", now.elapsed()); + println!("Balance:\n{:#?}", balance.base_coin()); Ok(()) } diff --git a/sdk/examples/wallet/wallet.rs b/sdk/examples/wallet/wallet.rs index ab0e45222c..12f8b2ec33 100644 --- a/sdk/examples/wallet/wallet.rs +++ b/sdk/examples/wallet/wallet.rs @@ -3,11 +3,9 @@ //! In this example we will: //! * create a wallet from a mnemonic phrase -//! * create an account if it does not exist yet -//! * generate some addresses for that account - if necessary -//! * print all addresses in the account -//! * print all addresses with funds in the account -//! * make a coin transaction +//! * print the wallet address (as Bech32) +//! * print funds on the wallet address +//! * issue a coin transaction //! //! Make sure there's no `STRONGHOLD_SNAPSHOT_PATH` file and no `WALLET_DB_PATH` folder yet! //! @@ -16,17 +14,16 @@ //! cargo run --release --all-features --example wallet //! ``` +use crypto::keys::bip44::Bip44; use iota_sdk::{ client::{ constants::SHIMMER_COIN_TYPE, secret::{mnemonic::MnemonicSecretManager, SecretManager}, }, types::block::payload::signed_transaction::TransactionId, - wallet::{Account, ClientOptions, Result, Wallet}, + wallet::{ClientOptions, Result, Wallet}, }; -// The number of addresses to generate in this account -const MAX_ADDRESSES_TO_GENERATE: usize = 10; // The amount of coins to send const SEND_AMOUNT: u64 = 1_000_000; // The address to send the coins to @@ -39,22 +36,14 @@ async fn main() -> Result<()> { let wallet = create_wallet().await?; - let account = wallet.get_or_create_account("Alice").await?; - print_accounts(&wallet).await?; - - generate_addresses(&account, MAX_ADDRESSES_TO_GENERATE).await?; - print_addresses(&account).await?; - // Change to `true` to print the full balance report - sync_print_balance(&account, false).await?; - - print_addresses_with_funds(&account).await?; + sync_print_balance(&wallet, false).await?; println!("Sending '{}' coins to '{}'...", SEND_AMOUNT, RECV_ADDRESS); - let transaction = account.send(SEND_AMOUNT, RECV_ADDRESS, None).await?; - wait_for_inclusion(&transaction.transaction_id, &account).await?; + let transaction = wallet.send(SEND_AMOUNT, RECV_ADDRESS, None).await?; + wait_for_inclusion(&transaction.transaction_id, &wallet).await?; - sync_print_balance(&account, false).await?; + sync_print_balance(&wallet, false).await?; println!("Example finished successfully"); Ok(()) @@ -67,81 +56,36 @@ async fn create_wallet() -> Result { .with_secret_manager(SecretManager::Mnemonic(secret_manager)) .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) .with_client_options(client_options) - .with_coin_type(SHIMMER_COIN_TYPE) + .with_bip_path(Bip44::new(SHIMMER_COIN_TYPE)) .finish() .await } -async fn print_accounts(wallet: &Wallet) -> Result<()> { - let accounts = wallet.get_accounts().await?; - println!("Accounts:"); - for account in accounts { - let details = account.details().await; - println!("- {}", details.alias()); - } - Ok(()) -} - -async fn generate_addresses(account: &Account, max: usize) -> Result<()> { - if account.addresses().await.len() < max { - let num_addresses_to_generate = max - account.addresses().await.len(); - println!("Generating {num_addresses_to_generate} addresses ..."); - let now = tokio::time::Instant::now(); - account - .generate_ed25519_addresses(num_addresses_to_generate as u32, None) - .await?; - println!("Finished in: {:.2?}", now.elapsed()); - } - Ok(()) -} - -async fn print_addresses(account: &Account) -> Result<()> { - let addresses = account.addresses().await; - println!("{}'s addresses:", account.alias().await); - for address in addresses { - println!("- {}", address.address()); - } +async fn print_address(wallet: &Wallet) -> Result<()> { + println!("Wallet address: {}", wallet.address().await); Ok(()) } -async fn sync_print_balance(account: &Account, full_report: bool) -> Result<()> { - let alias = account.alias().await; +async fn sync_print_balance(wallet: &Wallet, full_report: bool) -> Result<()> { let now = tokio::time::Instant::now(); - let balance = account.sync(None).await?; - println!("{alias}'s account synced in: {:.2?}", now.elapsed()); + let balance = wallet.sync(None).await?; + println!("Wallet synced in: {:.2?}", now.elapsed()); if full_report { - println!("{alias}'s balance:\n{balance:#?}"); + println!("Balance:\n{balance:#?}"); } else { - println!("{alias}'s coin balance:\n{:#?}", balance.base_coin()); - } - Ok(()) -} - -async fn print_addresses_with_funds(account: &Account) -> Result<()> { - let addresses_with_unspent_outputs = account.addresses_with_unspent_outputs().await?; - println!( - "{}'s addresses with funds/assets: {}", - account.alias().await, - addresses_with_unspent_outputs.len() - ); - for address_with_unspent_outputs in addresses_with_unspent_outputs { - println!("- {}", address_with_unspent_outputs.address()); - println!(" Output Ids:"); - for output_id in address_with_unspent_outputs.output_ids() { - println!(" {}", output_id); - } + println!("Coin balance:\n{:#?}", balance.base_coin()); } Ok(()) } -async fn wait_for_inclusion(transaction_id: &TransactionId, account: &Account) -> Result<()> { +async fn wait_for_inclusion(transaction_id: &TransactionId, wallet: &Wallet) -> Result<()> { println!( "Transaction sent: {}/transaction/{}", std::env::var("EXPLORER_URL").unwrap(), transaction_id ); // Wait for transaction to get included - let block_id = account + let block_id = wallet .reissue_transaction_until_included(transaction_id, None, None) .await?; println!( diff --git a/sdk/src/client/error.rs b/sdk/src/client/error.rs index 65ec83edb5..6d99b5ded1 100644 --- a/sdk/src/client/error.rs +++ b/sdk/src/client/error.rs @@ -36,8 +36,8 @@ pub enum Error { /// Block types error #[error("{0}")] Block(#[from] crate::types::block::Error), - /// The wallet account has enough funds, but split on too many outputs - #[error("the wallet account has enough funds, but split on too many outputs: {0}, max. is 128, consolidate them")] + /// The wallet has enough funds, but split on too many outputs + #[error("the wallet has enough funds, but split on too many outputs: {0}, max. is 128, consolidate them")] ConsolidationRequired(usize), /// Crypto.rs error #[error("{0}")] diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index 9c05c833f1..ce004f33d2 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -4,7 +4,7 @@ #![cfg_attr(not(feature = "std"), no_std)] #![cfg_attr(docsrs, feature(doc_cfg))] // TODO missing_docs -#![deny(clippy::nursery, rust_2018_idioms, /* warnings, */ unreachable_pub)] +#![deny(clippy::nursery, rust_2018_idioms, /* warnings, unreachable_pub */)] #![allow( clippy::redundant_pub_crate, clippy::missing_const_for_fn, diff --git a/sdk/src/wallet/account/builder.rs b/sdk/src/wallet/account/builder.rs deleted file mode 100644 index 98a50fae3d..0000000000 --- a/sdk/src/wallet/account/builder.rs +++ /dev/null @@ -1,178 +0,0 @@ -// Copyright 2021 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use std::collections::{HashMap, HashSet}; - -use tokio::sync::RwLock; - -use crate::{ - client::secret::{SecretManage, SecretManager}, - types::block::address::{Address, Bech32Address, Ed25519Address, Hrp}, - wallet::{ - account::{types::Bip44Address, Account, AccountDetails}, - Error, Wallet, - }, -}; - -/// The AccountBuilder -pub struct AccountBuilder { - addresses: Option>, - alias: Option, - bech32_hrp: Option, - wallet: Wallet, -} - -impl AccountBuilder -where - crate::wallet::Error: From, -{ - /// Create an IOTA client builder - pub fn new(wallet: Wallet) -> Self { - Self { - addresses: None, - alias: None, - bech32_hrp: None, - wallet, - } - } - - /// Set the addresses, should only be used for accounts with an offline counterpart account from which the addresses - /// were exported - pub fn with_addresses(mut self, addresses: impl Into>>) -> Self { - self.addresses = addresses.into(); - self - } - - /// Set the alias - pub fn with_alias(mut self, alias: impl Into) -> Self { - self.alias = Some(alias.into()); - self - } - - /// Set the bech32 HRP - pub fn with_bech32_hrp(mut self, bech32_hrp: impl Into>) -> Self { - self.bech32_hrp = bech32_hrp.into(); - self - } - - /// Build the Account and add it to the accounts from Wallet - /// Also generates the first address of the account and if it's not the first account, the address for the first - /// account will also be generated and compared, so no accounts get generated with different seeds - pub async fn finish(&mut self) -> crate::wallet::Result> { - let mut accounts = self.wallet.accounts.write().await; - let account_index = accounts.len() as u32; - // If no alias is provided, the account index will be set as alias - let account_alias = self.alias.clone().unwrap_or_else(|| account_index.to_string()); - log::debug!( - "[ACCOUNT BUILDER] creating new account {} with index {}", - account_alias, - account_index - ); - - // Check that the alias isn't already used for another account - for account in accounts.iter() { - let account = account.details().await; - if account.alias().to_lowercase() == account_alias.to_lowercase() { - return Err(Error::AccountAliasAlreadyExists(account_alias)); - } - } - - let coin_type = self.wallet.coin_type.load(core::sync::atomic::Ordering::Relaxed); - - // If addresses are provided we will use them directly without the additional checks, because then we assume - // that it's for offline signing and the secretManager can't be used - let addresses = match &self.addresses { - Some(addresses) => addresses.clone(), - None => { - let mut bech32_hrp = self.bech32_hrp; - if let Some(first_account) = accounts.first() { - let first_account_coin_type = *first_account.details().await.coin_type(); - // Generate the first address of the first account and compare it to the stored address from the - // first account to prevent having multiple accounts created with different - // seeds - let first_account_public_address = - get_first_public_address(&self.wallet.secret_manager, first_account_coin_type, 0).await?; - let first_account_addresses = first_account.public_addresses().await; - - if Address::Ed25519(first_account_public_address) - != first_account_addresses - .first() - .ok_or(Error::FailedToGetRemainder)? - .address - .inner - { - return Err(Error::InvalidMnemonic( - "first account address used another seed".to_string(), - )); - } - - // Get bech32_hrp from address - if let Some(address) = first_account_addresses.first() { - if bech32_hrp.is_none() { - bech32_hrp = Some(address.address.hrp); - } - } - } - - // get bech32_hrp - let bech32_hrp = { - match bech32_hrp { - Some(bech32_hrp) => bech32_hrp, - None => self.wallet.client().get_bech32_hrp().await?, - } - }; - - let first_public_address = - get_first_public_address(&self.wallet.secret_manager, coin_type, account_index).await?; - - let first_public_account_address = Bip44Address { - address: Bech32Address::new(bech32_hrp, first_public_address), - key_index: 0, - internal: false, - }; - - vec![first_public_account_address] - } - }; - - let account = AccountDetails { - index: account_index, - coin_type, - alias: account_alias, - public_addresses: addresses, - internal_addresses: Vec::new(), - addresses_with_unspent_outputs: Vec::new(), - outputs: HashMap::new(), - locked_outputs: HashSet::new(), - unspent_outputs: HashMap::new(), - transactions: HashMap::new(), - pending_transactions: HashSet::new(), - incoming_transactions: HashMap::new(), - inaccessible_incoming_transactions: HashSet::new(), - native_token_foundries: HashMap::new(), - }; - - let account = Account::new(account, self.wallet.inner.clone()).await?; - #[cfg(feature = "storage")] - account.save(None).await?; - accounts.push(account.clone()); - - Ok(account) - } -} - -/// Generate the first public address of an account -pub(crate) async fn get_first_public_address( - secret_manager: &RwLock, - coin_type: u32, - account_index: u32, -) -> crate::wallet::Result -where - crate::wallet::Error: From, -{ - Ok(secret_manager - .read() - .await - .generate_ed25519_addresses(coin_type, account_index, 0..1, None) - .await?[0]) -} diff --git a/sdk/src/wallet/account/mod.rs b/sdk/src/wallet/account/mod.rs deleted file mode 100644 index 8e52ef7f0f..0000000000 --- a/sdk/src/wallet/account/mod.rs +++ /dev/null @@ -1,751 +0,0 @@ -// Copyright 2021 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -/// The module with the AccountBuilder. -pub(crate) mod builder; -/// Constants used for the account and account operations. -pub(crate) mod constants; -/// The account operations like address generation, syncing and creating transactions. -pub(crate) mod operations; -/// Types used in an account and returned from methods. -pub mod types; -/// Methods to update the account state. -pub(crate) mod update; - -use std::{ - collections::{HashMap, HashSet}, - ops::Deref, - sync::Arc, -}; - -use getset::{Getters, Setters}; -use serde::{Deserialize, Serialize}; -use tokio::sync::{Mutex, RwLock}; - -#[cfg(feature = "participation")] -pub use self::operations::participation::{AccountParticipationOverview, ParticipationEventWithNodes}; -use self::types::{ - address::{AddressWithUnspentOutputs, Bip44Address}, - Balance, OutputData, TransactionWithMetadata, TransactionWithMetadataDto, -}; -pub use self::{ - operations::{ - output_claiming::OutputsToClaim, - output_consolidation::ConsolidationParams, - syncing::{ - options::{AccountSyncOptions, AliasSyncOptions, NftSyncOptions}, - SyncOptions, - }, - transaction::{ - high_level::{ - create_account::CreateAccountParams, - minting::{ - create_native_token::{ - CreateNativeTokenParams, CreateNativeTokenTransactionDto, - PreparedCreateNativeTokenTransactionDto, - }, - mint_nfts::MintNftParams, - }, - }, - prepare_output::{Assets, Features, OutputParams, ReturnStrategy, StorageDeposit, Unlocks}, - RemainderValueStrategy, TransactionOptions, - }, - }, - types::OutputDataDto, -}; -use super::core::WalletInner; -use crate::{ - client::{ - secret::{SecretManage, SecretManager}, - Client, - }, - types::{ - api::core::OutputWithMetadataResponse, - block::{ - address::Bech32Address, - output::{dto::FoundryOutputDto, AccountId, FoundryId, FoundryOutput, NftId, Output, OutputId, TokenId}, - payload::{signed_transaction::TransactionId, SignedTransactionPayload}, - }, - TryFromDto, - }, - wallet::{account::types::InclusionState, Result}, -}; - -/// Options to filter outputs -#[derive(Debug, Default, Clone, Serialize, Deserialize, Eq, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct FilterOptions { - /// Filter all outputs where the booked milestone index is below the specified timestamp - pub lower_bound_booked_timestamp: Option, - /// Filter all outputs where the booked milestone index is above the specified timestamp - pub upper_bound_booked_timestamp: Option, - /// Filter all outputs for the provided types (Basic = 3, Account = 4, Foundry = 5, NFT = 6). - pub output_types: Option>, - /// Return all account outputs matching these IDs. - pub account_ids: Option>, - /// Return all foundry outputs matching these IDs. - pub foundry_ids: Option>, - /// Return all nft outputs matching these IDs. - pub nft_ids: Option>, -} - -/// Details of an account. -#[derive(Clone, Debug, Eq, PartialEq, Getters, Setters)] -#[getset(get = "pub")] -pub struct AccountDetails { - /// The account index - index: u32, - /// The coin type - coin_type: u32, - /// The account alias. - alias: String, - /// Public addresses - pub(crate) public_addresses: Vec, - /// Internal addresses - pub(crate) internal_addresses: Vec, - /// Addresses with unspent outputs - // used to improve performance for syncing and get balance because it's in most cases only a subset of all - // addresses - addresses_with_unspent_outputs: Vec, - /// Outputs - // stored separated from the account for performance? - outputs: HashMap, - /// Unspent outputs that are currently used as input for transactions - // outputs used in transactions should be locked here so they don't get used again, which would result in a - // conflicting transaction - pub(crate) locked_outputs: HashSet, - /// Unspent outputs - // have unspent outputs in a separated hashmap so we don't need to iterate over all outputs we have - unspent_outputs: HashMap, - /// Sent transactions - // stored separated from the account for performance and only the transaction id here? where to add the network id? - // transactions: HashSet, - transactions: HashMap, - /// Pending transactions - // Maybe pending transactions even additionally separated? - pending_transactions: HashSet, - /// Transaction payloads for received outputs with inputs when not pruned before syncing, can be used to determine - /// the sender address(es) - incoming_transactions: HashMap, - /// Some incoming transactions can be pruned by the node before we requested them, then this node can never return - /// it. To avoid useless requests, these transaction ids are stored here and cleared when new client options are - /// set, because another node might still have them. - inaccessible_incoming_transactions: HashSet, - /// Foundries for native tokens in outputs - native_token_foundries: HashMap, -} - -/// A thread guard over an account, so we can lock the account during operations. -#[derive(Debug)] -pub struct Account { - inner: Arc, - pub(crate) wallet: Arc>, -} - -impl Clone for Account { - fn clone(&self) -> Self { - Self { - inner: self.inner.clone(), - wallet: self.wallet.clone(), - } - } -} - -impl Account { - pub fn get_secret_manager(&self) -> &Arc> { - self.wallet.get_secret_manager() - } -} - -#[derive(Debug)] -pub struct AccountInner { - details: RwLock, - // mutex to prevent multiple sync calls at the same or almost the same time, the u128 is a timestamp - // if the last synced time was < `MIN_SYNC_INTERVAL` second ago, we don't sync, but only calculate the balance - // again, because sending transactions can change that - pub(crate) last_synced: Mutex, - pub(crate) default_sync_options: Mutex, -} - -// impl Deref so we can use `account.details()` instead of `account.details.read()` -impl Deref for Account { - type Target = AccountInner; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl Account -where - crate::wallet::Error: From, -{ - /// Create a new Account with an AccountDetails - pub(crate) async fn new(details: AccountDetails, wallet: Arc>) -> Result { - #[cfg(feature = "storage")] - let default_sync_options = wallet - .storage_manager - .read() - .await - .get_default_sync_options(*details.index()) - .await? - .unwrap_or_default(); - #[cfg(not(feature = "storage"))] - let default_sync_options = Default::default(); - - Ok(Self { - wallet, - inner: Arc::new(AccountInner { - details: RwLock::new(details), - last_synced: Default::default(), - default_sync_options: Mutex::new(default_sync_options), - }), - }) - } - - // Get the Client - pub fn client(&self) -> &Client { - &self.wallet.client - } - - /// Get the [`Output`] that minted a native token by the token ID. First try to get it - /// from the account, if it isn't in the account try to get it from the node - pub async fn get_foundry_output(&self, native_token_id: TokenId) -> Result { - let foundry_id = FoundryId::from(native_token_id); - - for output_data in self.details().await.outputs().values() { - if let Output::Foundry(foundry_output) = &output_data.output { - if foundry_output.id() == foundry_id { - return Ok(output_data.output.clone()); - } - } - } - - // Foundry was not found in the account, try to get it from the node - let foundry_output_id = self.client().foundry_output_id(foundry_id).await?; - let output = self.client().get_output(&foundry_output_id).await?; - - Ok(output) - } - - /// Save the account to the database, accepts the updated_account as option so we don't need to drop it before - /// saving - #[cfg(feature = "storage")] - pub(crate) async fn save(&self, updated_account: Option<&AccountDetails>) -> Result<()> { - log::debug!("[save] saving account to database"); - match updated_account { - Some(account) => { - let mut storage_manager = self.wallet.storage_manager.write().await; - storage_manager.save_account(account).await?; - drop(storage_manager); - } - None => { - let account_details = self.details().await; - let mut storage_manager = self.wallet.storage_manager.write().await; - storage_manager.save_account(&account_details).await?; - drop(storage_manager); - drop(account_details); - } - } - Ok(()) - } - - #[cfg(feature = "events")] - pub(crate) async fn emit(&self, account_index: u32, wallet_event: super::events::types::WalletEvent) { - self.wallet.emit(account_index, wallet_event).await - } -} - -impl AccountInner { - pub async fn details(&self) -> tokio::sync::RwLockReadGuard<'_, AccountDetails> { - self.details.read().await - } - - pub async fn details_mut(&self) -> tokio::sync::RwLockWriteGuard<'_, AccountDetails> { - self.details.write().await - } - - pub async fn alias(&self) -> String { - self.details().await.alias.clone() - } - - /// Get the [`OutputData`] of an output stored in the account - pub async fn get_output(&self, output_id: &OutputId) -> Option { - self.details().await.outputs().get(output_id).cloned() - } - - /// Get the [`Transaction`] of a transaction stored in the account - pub async fn get_transaction(&self, transaction_id: &TransactionId) -> Option { - self.details().await.transactions().get(transaction_id).cloned() - } - - /// Get the transaction with inputs of an incoming transaction stored in the account - /// List might not be complete, if the node pruned the data already - pub async fn get_incoming_transaction(&self, transaction_id: &TransactionId) -> Option { - self.details() - .await - .incoming_transactions() - .get(transaction_id) - .cloned() - } - - /// Returns all addresses of the account - pub async fn addresses(&self) -> Vec { - let account_details = self.details().await; - let mut all_addresses = account_details.public_addresses().clone(); - all_addresses.extend(account_details.internal_addresses().clone()); - - all_addresses.to_vec() - } - - /// Returns the first address of the account as bech32 - pub async fn first_address_bech32(&self) -> Bech32Address { - // PANIC: unwrap is fine as one address is always generated during account creation. - self.addresses().await.into_iter().next().unwrap().into_bech32() - } - - /// Returns all public addresses of the account - pub(crate) async fn public_addresses(&self) -> Vec { - self.details().await.public_addresses().to_vec() - } - - /// Returns only addresses of the account with balance - pub async fn addresses_with_unspent_outputs(&self) -> Result> { - Ok(self.details().await.addresses_with_unspent_outputs().to_vec()) - } - - fn filter_outputs<'a>( - &self, - outputs: impl Iterator, - filter: impl Into>, - ) -> Result> { - let filter = filter.into(); - - if let Some(filter) = filter { - let mut filtered_outputs = Vec::new(); - - for output in outputs { - match &output.output { - Output::Account(alias) => { - if let Some(account_ids) = &filter.account_ids { - let account_id = alias.account_id_non_null(&output.output_id); - if account_ids.contains(&account_id) { - filtered_outputs.push(output.clone()); - continue; - } - } - } - Output::Foundry(foundry) => { - if let Some(foundry_ids) = &filter.foundry_ids { - let foundry_id = foundry.id(); - if foundry_ids.contains(&foundry_id) { - filtered_outputs.push(output.clone()); - continue; - } - } - } - Output::Nft(nft) => { - if let Some(nft_ids) = &filter.nft_ids { - let nft_id = nft.nft_id_non_null(&output.output_id); - if nft_ids.contains(&nft_id) { - filtered_outputs.push(output.clone()); - continue; - } - } - } - _ => {} - } - - // TODO check if we can still filter since milestone_timestamp_booked is gone - // if let Some(lower_bound_booked_timestamp) = filter.lower_bound_booked_timestamp { - // if output.metadata.milestone_timestamp_booked() < lower_bound_booked_timestamp { - // continue; - // } - // } - // if let Some(upper_bound_booked_timestamp) = filter.upper_bound_booked_timestamp { - // if output.metadata.milestone_timestamp_booked() > upper_bound_booked_timestamp { - // continue; - // } - // } - - if let Some(output_types) = &filter.output_types { - if !output_types.contains(&output.output.kind()) { - continue; - } - } - - // If ids are provided, only return them and no other outputs. - if filter.account_ids.is_none() && filter.foundry_ids.is_none() && filter.nft_ids.is_none() { - filtered_outputs.push(output.clone()); - } - } - - Ok(filtered_outputs) - } else { - Ok(outputs.cloned().collect()) - } - } - - /// Returns outputs of the account - pub async fn outputs(&self, filter: impl Into> + Send) -> Result> { - self.filter_outputs(self.details().await.outputs.values(), filter) - } - - /// Returns unspent outputs of the account - pub async fn unspent_outputs(&self, filter: impl Into> + Send) -> Result> { - self.filter_outputs(self.details().await.unspent_outputs.values(), filter) - } - - /// Gets the unspent account output matching the given ID. - pub async fn unspent_account_output(&self, account_id: &AccountId) -> Result> { - self.unspent_outputs(FilterOptions { - account_ids: Some([*account_id].into()), - ..Default::default() - }) - .await - .map(|res| res.get(0).cloned()) - } - - /// Gets the unspent foundry output matching the given ID. - pub async fn unspent_foundry_output(&self, foundry_id: &FoundryId) -> Result> { - self.unspent_outputs(FilterOptions { - foundry_ids: Some([*foundry_id].into()), - ..Default::default() - }) - .await - .map(|res| res.get(0).cloned()) - } - - /// Gets the unspent nft output matching the given ID. - pub async fn unspent_nft_output(&self, nft_id: &NftId) -> Result> { - self.unspent_outputs(FilterOptions { - nft_ids: Some([*nft_id].into()), - ..Default::default() - }) - .await - .map(|res| res.get(0).cloned()) - } - - /// Returns all incoming transactions of the account - pub async fn incoming_transactions(&self) -> Vec { - self.details().await.incoming_transactions.values().cloned().collect() - } - - /// Returns all transactions of the account - pub async fn transactions(&self) -> Vec { - self.details().await.transactions.values().cloned().collect() - } - - /// Returns all pending transactions of the account - pub async fn pending_transactions(&self) -> Vec { - let mut transactions = Vec::new(); - let account_details = self.details().await; - - for transaction_id in &account_details.pending_transactions { - if let Some(transaction) = account_details.transactions.get(transaction_id) { - transactions.push(transaction.clone()); - } - } - - transactions - } -} - -pub(crate) fn build_transaction_from_payload_and_inputs( - tx_id: TransactionId, - tx_payload: SignedTransactionPayload, - inputs: Vec, -) -> crate::wallet::Result { - Ok(TransactionWithMetadata { - payload: tx_payload.clone(), - block_id: inputs.first().map(|i| *i.metadata.block_id()), - inclusion_state: InclusionState::Confirmed, - timestamp: 0, - // TODO check if we keep a timestamp in Transaction since milestone_timestamp_spent is gone - // inputs - // .first() - // .and_then(|i| i.metadata.milestone_timestamp_spent.map(|t| t as u128 * 1000)) - // .unwrap_or_else(|| crate::utils::unix_timestamp_now().as_millis()), - transaction_id: tx_id, - network_id: tx_payload.transaction().network_id(), - incoming: true, - note: None, - inputs, - }) -} - -/// Dto for an Account. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AccountDetailsDto { - /// The account index - pub index: u32, - /// The coin type - pub coin_type: u32, - /// The account alias. - pub alias: String, - /// Public addresses - pub public_addresses: Vec, - /// Internal addresses - pub internal_addresses: Vec, - /// Addresses with unspent outputs - pub addresses_with_unspent_outputs: Vec, - /// Outputs - pub outputs: HashMap, - /// Unspent outputs that are currently used as input for transactions - pub locked_outputs: HashSet, - /// Unspent outputs - pub unspent_outputs: HashMap, - /// Sent transactions - pub transactions: HashMap, - /// Pending transactions - pub pending_transactions: HashSet, - /// Incoming transactions - pub incoming_transactions: HashMap, - /// Foundries for native tokens in outputs - #[serde(default)] - pub native_token_foundries: HashMap, -} - -impl TryFromDto for AccountDetails { - type Dto = AccountDetailsDto; - type Error = crate::wallet::Error; - - fn try_from_dto_with_params_inner( - dto: Self::Dto, - params: crate::types::ValidationParams<'_>, - ) -> core::result::Result { - Ok(Self { - index: dto.index, - coin_type: dto.coin_type, - alias: dto.alias, - public_addresses: dto.public_addresses, - internal_addresses: dto.internal_addresses, - addresses_with_unspent_outputs: dto.addresses_with_unspent_outputs, - outputs: dto - .outputs - .into_iter() - .map(|(id, o)| Ok((id, OutputData::try_from_dto_with_params(o, ¶ms)?))) - .collect::>()?, - locked_outputs: dto.locked_outputs, - unspent_outputs: dto - .unspent_outputs - .into_iter() - .map(|(id, o)| Ok((id, OutputData::try_from_dto_with_params(o, ¶ms)?))) - .collect::>()?, - transactions: dto - .transactions - .into_iter() - .map(|(id, o)| Ok((id, TransactionWithMetadata::try_from_dto_with_params(o, ¶ms)?))) - .collect::>()?, - pending_transactions: dto.pending_transactions, - incoming_transactions: dto - .incoming_transactions - .into_iter() - .map(|(id, o)| Ok((id, TransactionWithMetadata::try_from_dto_with_params(o, ¶ms)?))) - .collect::>()?, - inaccessible_incoming_transactions: Default::default(), - native_token_foundries: dto - .native_token_foundries - .into_iter() - .map(|(id, o)| Ok((id, FoundryOutput::try_from_dto_with_params(o, ¶ms)?))) - .collect::>()?, - }) - } -} - -impl From<&AccountDetails> for AccountDetailsDto { - fn from(value: &AccountDetails) -> Self { - Self { - index: *value.index(), - coin_type: *value.coin_type(), - alias: value.alias().clone(), - public_addresses: value.public_addresses().clone(), - internal_addresses: value.internal_addresses().clone(), - addresses_with_unspent_outputs: value.addresses_with_unspent_outputs().clone(), - outputs: value - .outputs() - .iter() - .map(|(id, output)| (*id, OutputDataDto::from(output))) - .collect(), - locked_outputs: value.locked_outputs().clone(), - unspent_outputs: value - .unspent_outputs() - .iter() - .map(|(id, output)| (*id, OutputDataDto::from(output))) - .collect(), - transactions: value - .transactions() - .iter() - .map(|(id, transaction)| (*id, TransactionWithMetadataDto::from(transaction))) - .collect(), - pending_transactions: value.pending_transactions().clone(), - incoming_transactions: value - .incoming_transactions() - .iter() - .map(|(id, transaction)| (*id, TransactionWithMetadataDto::from(transaction))) - .collect(), - native_token_foundries: value - .native_token_foundries() - .iter() - .map(|(id, foundry)| (*id, FoundryOutputDto::from(foundry))) - .collect(), - } - } -} - -#[cfg(test)] -mod test { - use core::str::FromStr; - - use pretty_assertions::assert_eq; - - use super::*; - use crate::types::block::{ - address::{Address, Ed25519Address}, - input::{Input, UtxoInput}, - output::{AddressUnlockCondition, BasicOutput, Output}, - payload::signed_transaction::{SignedTransactionPayload, Transaction, TransactionId}, - protocol::ProtocolParameters, - rand::mana::rand_mana_allotment, - signature::{Ed25519Signature, Signature}, - unlock::{ReferenceUnlock, SignatureUnlock, Unlock, Unlocks}, - }; - - const TRANSACTION_ID: &str = "0x24a1f46bdb6b2bf38f1c59f73cdd4ae5b418804bb231d76d06fbf246498d588300000000"; - const ED25519_ADDRESS: &str = "0xe594f9a895c0e0a6760dd12cffc2c3d1e1cbf7269b328091f96ce3d0dd550b75"; - const ED25519_PUBLIC_KEY: &str = "0x1da5ddd11ba3f961acab68fafee3177d039875eaa94ac5fdbff8b53f0c50bfb9"; - const ED25519_SIGNATURE: &str = "0xc6a40edf9a089f42c18f4ebccb35fe4b578d93b879e99b87f63573324a710d3456b03fb6d1fcc027e6401cbd9581f790ee3ed7a3f68e9c225fcb9f1cd7b7110d"; - - #[test] - fn serialize() { - let protocol_parameters = ProtocolParameters::new( - 2, - "testnet", - "rms", - crate::types::block::output::RentStructure::new(500, 1, 10, 1, 1, 1), - 1_813_620_509_061_365, - 1582328545, - 10, - 20, - ) - .unwrap(); - - let transaction_id = TransactionId::new(prefix_hex::decode(TRANSACTION_ID).unwrap()); - let input1 = Input::Utxo(UtxoInput::new(transaction_id, 0).unwrap()); - let input2 = Input::Utxo(UtxoInput::new(transaction_id, 1).unwrap()); - let bytes: [u8; 32] = prefix_hex::decode(ED25519_ADDRESS).unwrap(); - let address = Address::from(Ed25519Address::new(bytes)); - let amount = 1_000_000; - let output = Output::Basic( - BasicOutput::build_with_amount(amount) - .add_unlock_condition(AddressUnlockCondition::new(address)) - .finish_with_params(protocol_parameters.clone()) - .unwrap(), - ); - let transaction = Transaction::builder(protocol_parameters.network_id()) - .with_inputs([input1, input2]) - .add_output(output) - .add_mana_allotment(rand_mana_allotment(&protocol_parameters)) - .finish_with_params(protocol_parameters) - .unwrap(); - - let pub_key_bytes = prefix_hex::decode(ED25519_PUBLIC_KEY).unwrap(); - let sig_bytes = prefix_hex::decode(ED25519_SIGNATURE).unwrap(); - let signature = Ed25519Signature::try_from_bytes(pub_key_bytes, sig_bytes).unwrap(); - let sig_unlock = Unlock::from(SignatureUnlock::from(Signature::from(signature))); - let ref_unlock = Unlock::from(ReferenceUnlock::new(0).unwrap()); - let unlocks = Unlocks::new([sig_unlock, ref_unlock]).unwrap(); - - let tx_payload = SignedTransactionPayload::new(transaction, unlocks).unwrap(); - - let incoming_transaction = TransactionWithMetadata { - transaction_id: TransactionId::from_str( - "0x131fc4cb8f315ae36ae3bf6a4e4b3486d5f17581288f1217410da3e0700d195a00000000", - ) - .unwrap(), - payload: tx_payload, - block_id: None, - network_id: 0, - timestamp: 0, - inclusion_state: InclusionState::Pending, - incoming: false, - note: None, - inputs: Vec::new(), - }; - - let mut incoming_transactions = HashMap::new(); - incoming_transactions.insert( - TransactionId::from_str("0x131fc4cb8f315ae36ae3bf6a4e4b3486d5f17581288f1217410da3e0700d195a00000000") - .unwrap(), - incoming_transaction, - ); - - let account = AccountDetails { - index: 0, - coin_type: 4218, - alias: "Alice".to_string(), - public_addresses: vec![Bip44Address { - address: crate::types::block::address::Bech32Address::from_str( - "rms1qpllaj0pyveqfkwxmnngz2c488hfdtmfrj3wfkgxtk4gtyrax0jaxzt70zy", - ) - .unwrap(), - key_index: 0, - internal: false, - }], - internal_addresses: Vec::new(), - addresses_with_unspent_outputs: Vec::new(), - outputs: HashMap::new(), - locked_outputs: HashSet::new(), - unspent_outputs: HashMap::new(), - transactions: HashMap::new(), - pending_transactions: HashSet::new(), - incoming_transactions, - inaccessible_incoming_transactions: HashSet::new(), - native_token_foundries: HashMap::new(), - }; - - let deser_account = AccountDetails::try_from_dto( - serde_json::from_str::( - &serde_json::to_string(&AccountDetailsDto::from(&account)).unwrap(), - ) - .unwrap(), - ) - .unwrap(); - - assert_eq!(account, deser_account); - } - - impl AccountDetails { - /// Returns a mock of this type with the following values: - /// index: 0, coin_type: 4218, alias: "Alice", public_addresses: contains a single public account address - /// (rms1qpllaj0pyveqfkwxmnngz2c488hfdtmfrj3wfkgxtk4gtyrax0jaxzt70zy), all other fields are set to their Rust - /// defaults. - #[cfg(feature = "storage")] - pub(crate) fn mock() -> Self { - Self { - index: 0, - coin_type: 4218, - alias: "Alice".to_string(), - public_addresses: vec![Bip44Address { - address: crate::types::block::address::Bech32Address::from_str( - "rms1qpllaj0pyveqfkwxmnngz2c488hfdtmfrj3wfkgxtk4gtyrax0jaxzt70zy", - ) - .unwrap(), - key_index: 0, - internal: false, - }], - internal_addresses: Vec::new(), - addresses_with_unspent_outputs: Vec::new(), - outputs: HashMap::new(), - locked_outputs: HashSet::new(), - unspent_outputs: HashMap::new(), - transactions: HashMap::new(), - pending_transactions: HashSet::new(), - incoming_transactions: HashMap::new(), - inaccessible_incoming_transactions: HashSet::new(), - native_token_foundries: HashMap::new(), - } - } - } -} diff --git a/sdk/src/wallet/account/operations/address_generation.rs b/sdk/src/wallet/account/operations/address_generation.rs deleted file mode 100644 index 5d7d100cfc..0000000000 --- a/sdk/src/wallet/account/operations/address_generation.rs +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright 2021 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -#[cfg(feature = "ledger_nano")] -use crate::client::secret::{ledger_nano::LedgerSecretManager, DowncastSecretManager}; -use crate::{ - client::secret::{GenerateAddressOptions, SecretManage}, - types::block::address::Bech32Address, - wallet::account::{types::address::Bip44Address, Account}, -}; -#[cfg(all(feature = "events", feature = "ledger_nano"))] -use crate::{ - types::block::address::ToBech32Ext, - wallet::events::types::{AddressData, WalletEvent}, -}; - -impl Account -where - crate::wallet::Error: From, -{ - /// Generate addresses and stores them in the account - /// ```ignore - /// let public_addresses = account.generate_ed25519_addresses(2, None).await?; - /// // internal addresses are used for remainder outputs, if the RemainderValueStrategy for transactions is set to ChangeAddress - /// let internal_addresses = account - /// .generate_ed25519_addresses( - /// 1, - /// Some(GenerateAddressOptions { - /// internal: true, - /// ..Default::default() - /// }), - /// ) - /// .await?; - /// ``` - pub async fn generate_ed25519_addresses( - &self, - amount: u32, - options: impl Into> + Send, - ) -> crate::wallet::Result> { - let options = options.into().unwrap_or_default(); - log::debug!( - "[ADDRESS GENERATION] generating {amount} addresses, internal: {}", - options.internal - ); - if amount == 0 { - return Ok(Vec::new()); - } - - let account_details = self.details().await; - - // get the highest index for the public or internal addresses - let highest_current_index_plus_one = if options.internal { - account_details.internal_addresses.len() as u32 - } else { - account_details.public_addresses.len() as u32 - }; - - // get bech32_hrp - let bech32_hrp = { - match account_details.public_addresses.first() { - Some(address) => address.address.hrp, - None => self.client().get_bech32_hrp().await?, - } - }; - - let address_range = highest_current_index_plus_one..highest_current_index_plus_one + amount; - - // If we don't sync, then we want to display the prompt on the ledger with the address. But the user - // needs to have it visible on the computer first, so we need to generate it without the - // prompt first - #[cfg(feature = "ledger_nano")] - let addresses = { - use crate::wallet::account::SecretManager; - let secret_manager = self.wallet.secret_manager.read().await; - if secret_manager - .downcast::() - .or_else(|| { - secret_manager.downcast::().and_then(|s| { - if let SecretManager::LedgerNano(n) = s { - Some(n) - } else { - None - } - }) - }) - .is_some() - { - #[cfg(feature = "events")] - let changed_options = { - // Change options so ledger will not show the prompt the first time - let mut changed_options = options; - changed_options.ledger_nano_prompt = false; - changed_options - }; - let mut addresses = Vec::new(); - - for address_index in address_range { - #[cfg(feature = "events")] - { - // Generate without prompt to be able to display it - let address = self - .wallet - .secret_manager - .read() - .await - .generate_ed25519_addresses( - account_details.coin_type, - account_details.index, - address_index..address_index + 1, - Some(changed_options), - ) - .await?; - self.emit( - account_details.index, - WalletEvent::LedgerAddressGeneration(AddressData { - address: address[0].to_bech32(bech32_hrp), - }), - ) - .await; - } - // Generate with prompt so the user can verify - let address = self - .wallet - .secret_manager - .read() - .await - .generate_ed25519_addresses( - account_details.coin_type, - account_details.index, - address_index..address_index + 1, - Some(options), - ) - .await?; - addresses.push(address[0]); - } - addresses - } else { - self.wallet - .secret_manager - .read() - .await - .generate_ed25519_addresses( - account_details.coin_type, - account_details.index, - address_range, - Some(options), - ) - .await? - } - }; - - #[cfg(not(feature = "ledger_nano"))] - let addresses = self - .wallet - .secret_manager - .read() - .await - .generate_ed25519_addresses( - account_details.coin_type, - account_details.index, - address_range, - Some(options), - ) - .await?; - - drop(account_details); - - let generate_addresses: Vec = addresses - .into_iter() - .enumerate() - .map(|(index, address)| Bip44Address { - address: Bech32Address::new(bech32_hrp, address), - key_index: highest_current_index_plus_one + index as u32, - internal: options.internal, - }) - .collect(); - - self.update_account_addresses(options.internal, generate_addresses.clone()) - .await?; - - Ok(generate_addresses) - } - - /// Generate an internal address and store in the account, internal addresses are used for remainder outputs - pub(crate) async fn generate_remainder_address(&self) -> crate::wallet::Result { - let result = self - .generate_ed25519_addresses(1, Some(GenerateAddressOptions::internal())) - .await? - .first() - .ok_or(crate::wallet::Error::FailedToGetRemainder)? - .clone(); - - Ok(result) - } -} diff --git a/sdk/src/wallet/account/operations/balance.rs b/sdk/src/wallet/account/operations/balance.rs deleted file mode 100644 index 82cd9ad673..0000000000 --- a/sdk/src/wallet/account/operations/balance.rs +++ /dev/null @@ -1,359 +0,0 @@ -// Copyright 2022 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use primitive_types::U256; - -use crate::{ - client::secret::SecretManage, - types::block::{ - address::Bech32Address, - output::{unlock_condition::UnlockCondition, FoundryId, NativeTokensBuilder, Output, Rent}, - ConvertTo, - }, - wallet::{ - account::{ - operations::helpers::time::can_output_be_unlocked_forever_from_now_on, - types::{AddressWithUnspentOutputs, Balance, NativeTokensBalance}, - Account, AccountDetails, OutputsToClaim, - }, - Error, Result, - }, -}; - -impl Account -where - Error: From, - crate::client::Error: From, -{ - /// Get the balance of the account. - pub async fn balance(&self) -> Result { - log::debug!("[BALANCE] balance"); - - let account_details = self.details().await; - - self.balance_inner(account_details.addresses_with_unspent_outputs.iter(), &account_details) - .await - } - - /// Get the balance of the given addresses. - pub async fn addresses_balance(&self, addresses: Vec>) -> Result { - log::debug!("[BALANCE] addresses_balance"); - - let account_details = self.details().await; - - let addresses_with_unspent_outputs = addresses - .into_iter() - .map(|address| { - let address = address.convert()?; - account_details - .addresses_with_unspent_outputs - .iter() - .find(|&a| a.address == address) - .ok_or(Error::AddressNotFoundInAccount(address)) - }) - .collect::>>()?; - - self.balance_inner(addresses_with_unspent_outputs.into_iter(), &account_details) - .await - } - - async fn balance_inner( - &self, - addresses_with_unspent_outputs: impl Iterator + Send, - account_details: &AccountDetails, - ) -> Result { - let network_id = self.client().get_network_id().await?; - let rent_structure = self.client().get_rent_structure().await?; - let mut balance = Balance::default(); - let mut total_rent_amount = 0; - let mut total_native_tokens = NativeTokensBuilder::default(); - - #[cfg(feature = "participation")] - let voting_output = self.get_voting_output().await?; - - for address_with_unspent_outputs in addresses_with_unspent_outputs { - #[cfg(feature = "participation")] - { - if let Some(voting_output) = &voting_output { - if voting_output.output.as_basic().address() == address_with_unspent_outputs.address.inner() { - balance.base_coin.voting_power = voting_output.output.amount(); - } - } - } - - for output_id in &address_with_unspent_outputs.output_ids { - if let Some(data) = account_details.unspent_outputs.get(output_id) { - // Check if output is from the network we're currently connected to - if data.network_id != network_id { - continue; - } - - let output = &data.output; - let rent = output.rent_cost(rent_structure); - - // Add account and foundry outputs here because they can't have a - // [`StorageDepositReturnUnlockCondition`] or time related unlock conditions - match output { - Output::Account(output) => { - // Add amount - balance.base_coin.total += output.amount(); - // Add storage deposit - balance.required_storage_deposit.account += rent; - if !account_details.locked_outputs.contains(output_id) { - total_rent_amount += rent; - } - // Add native tokens - total_native_tokens.add_native_tokens(output.native_tokens().clone())?; - - let account_id = output.account_id_non_null(output_id); - balance.accounts.push(account_id); - } - Output::Foundry(output) => { - // Add amount - balance.base_coin.total += output.amount(); - // Add storage deposit - balance.required_storage_deposit.foundry += rent; - if !account_details.locked_outputs.contains(output_id) { - total_rent_amount += rent; - } - // Add native tokens - total_native_tokens.add_native_tokens(output.native_tokens().clone())?; - - balance.foundries.push(output.id()); - } - _ => { - // If there is only an [AddressUnlockCondition], then we can spend the output at any time - // without restrictions - if let [UnlockCondition::Address(_)] = output - .unlock_conditions() - .expect("output needs to have unlock conditions") - .as_ref() - { - // add nft_id for nft outputs - if let Output::Nft(output) = &output { - let nft_id = output.nft_id_non_null(output_id); - balance.nfts.push(nft_id); - } - - // Add amount - balance.base_coin.total += output.amount(); - - // Add storage deposit - if output.is_basic() { - balance.required_storage_deposit.basic += rent; - if output - .native_tokens() - .map(|native_tokens| !native_tokens.is_empty()) - .unwrap_or(false) - && !account_details.locked_outputs.contains(output_id) - { - total_rent_amount += rent; - } - } else if output.is_nft() { - balance.required_storage_deposit.nft += rent; - if !account_details.locked_outputs.contains(output_id) { - total_rent_amount += rent; - } - } - - // Add native tokens - if let Some(native_tokens) = output.native_tokens() { - total_native_tokens.add_native_tokens(native_tokens.clone())?; - } - } else { - // if we have multiple unlock conditions for basic or nft outputs, then we might can't - // spend the balance at the moment or in the future - - let account_addresses = self.addresses().await; - let slot_index = self.client().get_slot_index().await?; - let is_claimable = - self.claimable_outputs(OutputsToClaim::All).await?.contains(output_id); - - // For outputs that are expired or have a timelock unlock condition, but no expiration - // unlock condition and we then can unlock them, then - // they can never be not available for us anymore - // and should be added to the balance - if is_claimable { - // check if output can be unlocked always from now on, in that case it should be - // added to the total amount - let output_can_be_unlocked_now_and_in_future = - can_output_be_unlocked_forever_from_now_on( - // We use the addresses with unspent outputs, because other addresses of - // the account without unspent - // outputs can't be related to this output - &account_details.addresses_with_unspent_outputs, - output, - slot_index, - ); - - if output_can_be_unlocked_now_and_in_future { - // If output has a StorageDepositReturnUnlockCondition, the amount of it should - // be subtracted, because this part - // needs to be sent back - let amount = output - .unlock_conditions() - .and_then(|u| u.storage_deposit_return()) - .map_or_else( - || output.amount(), - |sdr| { - if account_addresses - .iter() - .any(|a| a.address.inner == *sdr.return_address()) - { - // sending to ourself, we get the full amount - output.amount() - } else { - // Sending to someone else - output.amount() - sdr.amount() - } - }, - ); - - // add nft_id for nft outputs - if let Output::Nft(output) = &output { - let nft_id = output.nft_id_non_null(output_id); - balance.nfts.push(nft_id); - } - - // Add amount - balance.base_coin.total += amount; - - // Add storage deposit - if output.is_basic() { - balance.required_storage_deposit.basic += rent; - // Amount for basic outputs isn't added to total_rent_amount if there aren't - // native tokens, since we can - // spend it without burning. - if output - .native_tokens() - .map(|native_tokens| !native_tokens.is_empty()) - .unwrap_or(false) - && !account_details.locked_outputs.contains(output_id) - { - total_rent_amount += rent; - } - } else if output.is_nft() { - balance.required_storage_deposit.nft += rent; - if !account_details.locked_outputs.contains(output_id) { - total_rent_amount += rent; - } - } - - // Add native tokens - if let Some(native_tokens) = output.native_tokens() { - total_native_tokens.add_native_tokens(native_tokens.clone())?; - } - } else { - // only add outputs that can't be locked now and at any point in the future - balance.potentially_locked_outputs.insert(*output_id, true); - } - } else { - // Don't add expired outputs that can't ever be unlocked by us - if let Some(expiration) = output - .unlock_conditions() - .expect("output needs to have unlock conditions") - .expiration() - { - // Not expired, could get unlockable when it's expired, so we insert it - if slot_index < expiration.slot_index() { - balance.potentially_locked_outputs.insert(*output_id, false); - } - } else { - balance.potentially_locked_outputs.insert(*output_id, false); - } - } - } - } - } - } - } - } - - self.finish( - balance, - account_details, - network_id, - total_rent_amount, - total_native_tokens, - ) - } - - fn finish( - &self, - mut balance: Balance, - account_details: &AccountDetails, - network_id: u64, - total_rent_amount: u64, - total_native_tokens: NativeTokensBuilder, - ) -> Result { - // for `available` get locked_outputs, sum outputs amount and subtract from total_amount - log::debug!("[BALANCE] locked outputs: {:#?}", account_details.locked_outputs); - - let mut locked_amount = 0; - let mut locked_native_tokens = NativeTokensBuilder::default(); - - for locked_output in &account_details.locked_outputs { - // Skip potentially_locked_outputs, as their amounts aren't added to the balance - if balance.potentially_locked_outputs.contains_key(locked_output) { - continue; - } - if let Some(output_data) = account_details.unspent_outputs.get(locked_output) { - // Only check outputs that are in this network - if output_data.network_id == network_id { - locked_amount += output_data.output.amount(); - if let Some(native_tokens) = output_data.output.native_tokens() { - locked_native_tokens.add_native_tokens(native_tokens.clone())?; - } - } - } - } - - log::debug!( - "[BALANCE] total_amount: {}, locked_amount: {}, total_rent_amount: {}", - balance.base_coin.total, - locked_amount, - total_rent_amount, - ); - - locked_amount += total_rent_amount; - - for native_token in total_native_tokens.finish_set()? { - // Check if some amount is currently locked - let locked_native_token_amount = locked_native_tokens.iter().find_map(|(id, amount)| { - if id == native_token.token_id() { - Some(amount) - } else { - None - } - }); - - let metadata = account_details - .native_token_foundries - .get(&FoundryId::from(*native_token.token_id())) - .and_then(|foundry| foundry.immutable_features().metadata()) - .cloned(); - - balance.native_tokens.push(NativeTokensBalance { - token_id: *native_token.token_id(), - total: native_token.amount(), - available: native_token.amount() - *locked_native_token_amount.unwrap_or(&U256::from(0u8)), - metadata, - }) - } - - #[cfg(not(feature = "participation"))] - { - balance.base_coin.available = balance.base_coin.total.saturating_sub(locked_amount); - } - #[cfg(feature = "participation")] - { - balance.base_coin.available = balance - .base_coin - .total - .saturating_sub(locked_amount) - .saturating_sub(balance.base_coin.voting_power); - } - - Ok(balance) - } -} diff --git a/sdk/src/wallet/account/operations/output_finder.rs b/sdk/src/wallet/account/operations/output_finder.rs deleted file mode 100644 index e15c6cf683..0000000000 --- a/sdk/src/wallet/account/operations/output_finder.rs +++ /dev/null @@ -1,285 +0,0 @@ -// Copyright 2021 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use std::cmp; - -use crate::{ - client::secret::{GenerateAddressOptions, SecretManage}, - wallet::account::{operations::syncing::SyncOptions, types::AddressWithUnspentOutputs, Account}, -}; - -impl Account -where - crate::wallet::Error: From, - crate::client::Error: From, -{ - /// Search addresses with unspent outputs - /// `address_gap_limit`: The number of addresses to search for, after the last address with unspent outputs - /// Addresses that got crated during this operation and have a higher key_index than the latest one with outputs, - /// will be removed again, to keep the account size smaller - pub(crate) async fn search_addresses_with_outputs( - &self, - mut address_gap_limit: u32, - sync_options: Option, - ) -> crate::wallet::Result { - log::debug!("[search_addresses_with_outputs]"); - let mut sync_options = match sync_options { - Some(opt) => opt, - None => self.default_sync_options().await.clone(), - }; - - // store the current index, so we can remove new addresses with higher indexes later again, if they don't have - // outputs - let (highest_public_address_index, highest_internal_address_index) = { - let account_details = self.details().await; - ( - account_details - .public_addresses - .last() - .map(|a| a.key_index) - .expect("account needs to have a public address"), - account_details.internal_addresses.last().map(|a| a.key_index), - ) - }; - - // public addresses - if sync_options.address_start_index != 0 { - let mut address_amount_to_generate = - sync_options.address_start_index.abs_diff(highest_public_address_index); - // -1 if it's larger than 0, to get the correct amount, because the address with the actual start index - // gets generated later - address_amount_to_generate = address_amount_to_generate.saturating_sub(1); - log::debug!( - "[search_addresses_with_outputs] generate {address_amount_to_generate} public addresses below the start index" - ); - self.generate_ed25519_addresses(address_amount_to_generate, None) - .await?; - } - // internal addresses - if sync_options.address_start_index_internal != 0 { - let mut address_amount_to_generate = sync_options - .address_start_index_internal - .abs_diff(highest_internal_address_index.unwrap_or(0)); - // -1 if it's larger than 0, to get the correct amount, because the address with the actual start index - // gets generated later - if address_amount_to_generate > 0 && highest_internal_address_index.is_some() { - address_amount_to_generate -= 1; - } - log::debug!( - "[search_addresses_with_outputs] generate {address_amount_to_generate} internal addresses below the start index" - ); - self.generate_ed25519_addresses(address_amount_to_generate, Some(GenerateAddressOptions::internal())) - .await?; - } - - let mut address_gap_limit_internal = address_gap_limit; - - let mut latest_outputs_count = 0; - loop { - // Also needs to be in the loop so it gets updated every round for internal use without modifying the values - // outside - let (highest_public_address_index, highest_internal_address_index) = { - let account_details = self.details().await; - ( - account_details - .public_addresses - .last() - .map(|a| a.key_index) - .expect("account needs to have a public address"), - account_details.internal_addresses.last().map(|a| a.key_index), - ) - }; - log::debug!( - "[search_addresses_with_outputs] address_gap_limit: {address_gap_limit}, address_gap_limit_internal: {address_gap_limit_internal}" - ); - // generate public and internal addresses - let addresses = self.generate_ed25519_addresses(address_gap_limit, None).await?; - let internal_addresses = self - .generate_ed25519_addresses(address_gap_limit_internal, Some(GenerateAddressOptions::internal())) - .await?; - - let address_start_index = addresses - .first() - .map(|a| { - // If the index is 1, then we only have the single address before we got during account creation - // To also sync that, we set the index to 0 - if a.key_index == 1 { 0 } else { a.key_index } - }) - // +1, because we don't want to sync the latest address again - .unwrap_or(highest_public_address_index + 1); - - let address_start_index_internal = internal_addresses - .first() - .map(|a| a.key_index) - // +1, because we don't want to sync the latest address again - .unwrap_or_else(|| highest_internal_address_index.unwrap_or(0) + 1); - - sync_options.force_syncing = true; - sync_options.address_start_index = address_start_index; - sync_options.address_start_index_internal = address_start_index_internal; - self.sync(Some(sync_options.clone())).await?; - - let output_count = self.details().await.unspent_outputs.len(); - - // break if we didn't find more outputs with the new addresses - if output_count <= latest_outputs_count { - break; - } - - latest_outputs_count = output_count; - - // Update address_gap_limit to only generate the amount of addresses we need to have `address_gap_limit` - // amount of empty addresses after the latest one with outputs - - let account_details = self.details().await; - - let highest_address_index = account_details - .public_addresses - .iter() - .max_by_key(|a| *a.key_index()) - .map(|a| *a.key_index()) - .expect("account needs to have at least one public address"); - - let highest_address_index_internal = account_details - .internal_addresses - .iter() - .max_by_key(|a| *a.key_index()) - .map(|a| *a.key_index()) - .unwrap_or(0); - - drop(account_details); - - let addresses_with_unspent_outputs = self.addresses_with_unspent_outputs().await?; - - let (addresses_with_outputs_internal, address_with_outputs): ( - Vec<&AddressWithUnspentOutputs>, - Vec<&AddressWithUnspentOutputs>, - ) = addresses_with_unspent_outputs.iter().partition(|a| a.internal); - - let latest_address_index_with_outputs = address_with_outputs - .iter() - .max_by_key(|a| *a.key_index()) - .map(|a| *a.key_index() as i64) - // -1 as default, because we will subtract this value and want to have the amount of empty addresses in - // a row and not the address index - .unwrap_or(-1); - - let latest_address_index_with_outputs_internal = addresses_with_outputs_internal - .iter() - .max_by_key(|a| *a.key_index()) - .map(|a| *a.key_index() as i64) - // -1 as default, because we will subtract this value and want to have the amount of empty addresses in - // a row and not the address index - .unwrap_or(-1); - - log::debug!( - "new highest_address_index: {highest_address_index}, internal: {highest_address_index_internal}" - ); - log::debug!( - "new latest_address_index_with_outputs: {latest_address_index_with_outputs:?}, internal: {latest_address_index_with_outputs_internal:?}" - ); - - let empty_addresses_in_row = (highest_address_index as i64 - latest_address_index_with_outputs) as u32; - - let empty_addresses_in_row_internal = - (highest_address_index_internal as i64 - latest_address_index_with_outputs_internal) as u32; - - log::debug!( - "new empty_addresses_in_row: {empty_addresses_in_row}, internal: {empty_addresses_in_row_internal}" - ); - - if empty_addresses_in_row > address_gap_limit { - log::debug!("empty_addresses_in_row: {empty_addresses_in_row}, setting address_gap_limit to 0"); - address_gap_limit = 0; - } else { - address_gap_limit -= empty_addresses_in_row; - } - if empty_addresses_in_row_internal > address_gap_limit_internal { - log::debug!( - "empty_addresses_in_row_internal: {empty_addresses_in_row_internal}, setting address_gap_limit_internal to 0" - ); - address_gap_limit_internal = 0; - } else { - address_gap_limit_internal -= empty_addresses_in_row_internal; - } - - log::debug!("new address_gap_limit: {address_gap_limit}, internal: {address_gap_limit_internal}"); - - if address_gap_limit == 0 && address_gap_limit_internal == 0 { - break; - } - } - - self.clean_account_after_recovery(highest_public_address_index, highest_internal_address_index) - .await; - - #[cfg(feature = "storage")] - { - log::debug!( - "[search_addresses_with_outputs] storing account {} with new synced data", - self.alias().await - ); - self.save(None).await?; - } - - Ok(latest_outputs_count) - } - - /// During search_addresses_with_outputs we created new addresses that don't have funds, so we remove them again. - // `old_highest_public_address_index` is not optional, because we need to have at least one public address in the - // account - async fn clean_account_after_recovery( - &self, - old_highest_public_address_index: u32, - old_highest_internal_address_index: Option, - ) { - let mut account_details = self.details_mut().await; - - let (internal_addresses_with_unspent_outputs, public_addresses_with_spent_outputs): ( - Vec<&AddressWithUnspentOutputs>, - Vec<&AddressWithUnspentOutputs>, - ) = account_details - .addresses_with_unspent_outputs() - .iter() - .partition(|address| address.internal); - - let highest_public_index_with_outputs = public_addresses_with_spent_outputs - .iter() - .map(|a| a.key_index) - .max() - // We want to have at least one public address - .unwrap_or(0); - - let highest_internal_index_with_outputs = internal_addresses_with_unspent_outputs - .iter() - .map(|a| a.key_index) - .max(); - - // The new highest index should be either the old one before we searched for funds or if we found addresses with - // funds the highest index from an address with outputs - let new_latest_public_index = cmp::max(highest_public_index_with_outputs, old_highest_public_address_index); - account_details.public_addresses = account_details - .public_addresses - .clone() - .into_iter() - .filter(|a| a.key_index <= new_latest_public_index) - .collect(); - - account_details.internal_addresses = - if old_highest_internal_address_index.is_none() && highest_internal_index_with_outputs.is_none() { - // For internal addresses we don't leave an empty address, that's only required for the public address - Vec::new() - } else { - let new_latest_internal_index = cmp::max( - highest_internal_index_with_outputs.unwrap_or(0), - old_highest_internal_address_index.unwrap_or(0), - ); - account_details - .internal_addresses - .clone() - .into_iter() - .filter(|a| a.key_index <= new_latest_internal_index) - .collect() - }; - } -} diff --git a/sdk/src/wallet/account/operations/syncing/addresses/mod.rs b/sdk/src/wallet/account/operations/syncing/addresses/mod.rs deleted file mode 100644 index a4e75d13c5..0000000000 --- a/sdk/src/wallet/account/operations/syncing/addresses/mod.rs +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2021 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -mod output_ids; -mod outputs; - -use std::collections::HashSet; - -use crate::{ - client::secret::SecretManage, - wallet::account::{operations::syncing::SyncOptions, types::address::AddressWithUnspentOutputs, Account}, -}; - -impl Account -where - crate::wallet::Error: From, -{ - /// Get the addresses that should be synced with the current known unspent output ids - /// Also adds account and nft addresses from unspent account or nft outputs that have no Timelock, Expiration or - /// StorageDepositReturn [`UnlockCondition`] - pub(crate) async fn get_addresses_to_sync( - &self, - options: &SyncOptions, - ) -> crate::wallet::Result> { - log::debug!("[SYNC] get_addresses_to_sync"); - - let mut addresses_before_syncing = self.addresses().await; - - // If custom addresses are provided check if they are in the account and only use them - if !options.addresses.is_empty() { - let mut specific_addresses_to_sync = HashSet::new(); - for bech32_address in &options.addresses { - match addresses_before_syncing.iter().find(|a| &a.address == bech32_address) { - Some(address) => { - specific_addresses_to_sync.insert(address.clone()); - } - None => { - return Err(crate::wallet::Error::AddressNotFoundInAccount(bech32_address.clone())); - } - } - } - addresses_before_syncing = specific_addresses_to_sync.into_iter().collect(); - } else if options.address_start_index != 0 || options.address_start_index_internal != 0 { - // Filter addresses when address_start_index(_internal) is not 0, so we skip these addresses - addresses_before_syncing.retain(|a| { - if a.internal { - a.key_index >= options.address_start_index_internal - } else { - a.key_index >= options.address_start_index - } - }); - } - - // Check if selected addresses contains addresses with balance so we can correctly update them - let addresses_with_unspent_outputs = self.addresses_with_unspent_outputs().await?; - let mut addresses_with_old_output_ids = Vec::new(); - for address in addresses_before_syncing { - let mut output_ids = Vec::new(); - // Add currently known unspent output ids, so we can later compare them with the new output ids and see if - // one got spent (is missing in the new returned output ids) - if let Some(address_with_unspent_outputs) = addresses_with_unspent_outputs - .iter() - .find(|a| a.address == address.address) - { - output_ids = address_with_unspent_outputs.output_ids.to_vec(); - } - addresses_with_old_output_ids.push(AddressWithUnspentOutputs { - address: address.address, - key_index: address.key_index, - internal: address.internal, - output_ids, - }) - } - - Ok(addresses_with_old_output_ids) - } -} diff --git a/sdk/src/wallet/account/update.rs b/sdk/src/wallet/account/update.rs deleted file mode 100644 index 930efd41c0..0000000000 --- a/sdk/src/wallet/account/update.rs +++ /dev/null @@ -1,281 +0,0 @@ -// Copyright 2022 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use std::collections::HashMap; - -use crate::{ - client::secret::SecretManage, - types::block::output::{OutputId, OutputMetadata}, - wallet::account::{ - operations::syncing::options::SyncOptions, - types::{address::AddressWithUnspentOutputs, InclusionState, OutputData, TransactionWithMetadata}, - Account, Bip44Address, - }, -}; -#[cfg(feature = "events")] -use crate::{ - types::{ - api::core::OutputWithMetadataResponse, block::payload::signed_transaction::dto::SignedTransactionPayloadDto, - }, - wallet::{ - account::types::OutputDataDto, - events::types::{NewOutputEvent, SpentOutputEvent, TransactionInclusionEvent, WalletEvent}, - }, -}; - -impl Account -where - crate::wallet::Error: From, -{ - /// Set the alias for the account - pub async fn set_alias(&self, alias: &str) -> crate::wallet::Result<()> { - let mut account_details = self.details_mut().await; - account_details.alias = alias.to_string(); - #[cfg(feature = "storage")] - self.save(Some(&account_details)).await?; - Ok(()) - } - - /// Update account with newly synced data and emit events for outputs - pub(crate) async fn update_account( - &self, - addresses_with_unspent_outputs: Vec, - unspent_outputs: Vec, - spent_or_unsynced_output_metadata_map: HashMap>, - options: &SyncOptions, - ) -> crate::wallet::Result<()> { - log::debug!("[SYNC] Update account with new synced transactions"); - - let network_id = self.client().get_network_id().await?; - let mut account_details = self.details_mut().await; - #[cfg(feature = "events")] - let account_index = account_details.index; - - // Update addresses_with_unspent_outputs - // only keep addresses below the address start index, because we synced the addresses above and will update them - account_details.addresses_with_unspent_outputs.retain(|a| { - if a.internal { - a.key_index < options.address_start_index_internal - } else { - a.key_index < options.address_start_index - } - }); - - // then add all synced addresses with balance, all other addresses that had balance before will then be removed - // from this list - account_details - .addresses_with_unspent_outputs - .extend(addresses_with_unspent_outputs); - - // Update spent outputs - for (output_id, output_metadata_response_opt) in spent_or_unsynced_output_metadata_map { - // If we got the output response and it's still unspent, skip it - if let Some(output_metadata_response) = output_metadata_response_opt { - if output_metadata_response.is_spent() { - account_details.unspent_outputs.remove(&output_id); - if let Some(output_data) = account_details.outputs.get_mut(&output_id) { - output_data.metadata = output_metadata_response; - } - } else { - // not spent, just not synced, skip - continue; - } - } - - if let Some(output) = account_details.outputs.get(&output_id) { - // Could also be outputs from other networks after we switched the node, so we check that first - if output.network_id == network_id { - log::debug!("[SYNC] Spent output {}", output_id); - account_details.locked_outputs.remove(&output_id); - account_details.unspent_outputs.remove(&output_id); - // Update spent data fields - if let Some(output_data) = account_details.outputs.get_mut(&output_id) { - output_data.metadata.set_spent(true); - output_data.is_spent = true; - #[cfg(feature = "events")] - { - self.emit( - account_index, - WalletEvent::SpentOutput(Box::new(SpentOutputEvent { - output: OutputDataDto::from(&*output_data), - })), - ) - .await; - } - } - } - } - } - - // Add new synced outputs - for output_data in unspent_outputs { - // Insert output, if it's unknown emit the NewOutputEvent - if account_details - .outputs - .insert(output_data.output_id, output_data.clone()) - .is_none() - { - #[cfg(feature = "events")] - { - let transaction = account_details - .incoming_transactions - .get(output_data.output_id.transaction_id()); - self.emit( - account_index, - WalletEvent::NewOutput(Box::new(NewOutputEvent { - output: OutputDataDto::from(&output_data), - transaction: transaction - .as_ref() - .map(|tx| SignedTransactionPayloadDto::from(&tx.payload)), - transaction_inputs: transaction.as_ref().map(|tx| { - tx.inputs - .clone() - .into_iter() - .map(OutputWithMetadataResponse::from) - .collect() - }), - })), - ) - .await; - } - }; - if !output_data.is_spent { - account_details - .unspent_outputs - .insert(output_data.output_id, output_data); - } - } - - #[cfg(feature = "storage")] - { - log::debug!( - "[SYNC] storing account {} with new synced data", - account_details.alias() - ); - self.save(Some(&account_details)).await?; - } - Ok(()) - } - - /// Update account with newly synced transactions - pub(crate) async fn update_account_with_transactions( - &self, - updated_transactions: Vec, - spent_output_ids: Vec, - output_ids_to_unlock: Vec, - ) -> crate::wallet::Result<()> { - log::debug!("[SYNC] Update account with new synced transactions"); - - let mut account_details = self.details_mut().await; - - for transaction in updated_transactions { - match transaction.inclusion_state { - InclusionState::Confirmed | InclusionState::Conflicting | InclusionState::UnknownPruned => { - let transaction_id = transaction.payload.transaction().id(); - account_details.pending_transactions.remove(&transaction_id); - log::debug!( - "[SYNC] inclusion_state of {transaction_id} changed to {:?}", - transaction.inclusion_state - ); - #[cfg(feature = "events")] - { - self.emit( - account_details.index, - WalletEvent::TransactionInclusion(TransactionInclusionEvent { - transaction_id, - inclusion_state: transaction.inclusion_state, - }), - ) - .await; - } - } - _ => {} - } - account_details - .transactions - .insert(transaction.payload.transaction().id(), transaction.clone()); - } - - for output_to_unlock in &spent_output_ids { - if let Some(output) = account_details.outputs.get_mut(output_to_unlock) { - output.is_spent = true; - } - account_details.locked_outputs.remove(output_to_unlock); - account_details.unspent_outputs.remove(output_to_unlock); - log::debug!("[SYNC] Unlocked spent output {}", output_to_unlock); - } - - for output_to_unlock in &output_ids_to_unlock { - account_details.locked_outputs.remove(output_to_unlock); - log::debug!( - "[SYNC] Unlocked unspent output {} because of a conflicting transaction", - output_to_unlock - ); - } - - #[cfg(feature = "storage")] - { - log::debug!( - "[SYNC] storing account {} with new synced transactions", - account_details.alias() - ); - self.save(Some(&account_details)).await?; - } - Ok(()) - } - - /// Update account with newly generated addresses - pub(crate) async fn update_account_addresses( - &self, - internal: bool, - new_addresses: Vec, - ) -> crate::wallet::Result<()> { - log::debug!("[update_account_addresses]"); - - let mut account_details = self.details_mut().await; - - // add addresses to the account - if internal { - account_details.internal_addresses.extend(new_addresses); - } else { - account_details.public_addresses.extend(new_addresses); - }; - - #[cfg(feature = "storage")] - { - log::debug!("[update_account_addresses] storing account {}", account_details.index()); - self.save(Some(&account_details)).await?; - } - Ok(()) - } - - // Should only be called from the Wallet so all accounts are on the same state - // Will update the addresses with a possible new Bech32 HRP and clear the inaccessible_incoming_transactions. - pub(crate) async fn update_account_bech32_hrp(&mut self) -> crate::wallet::Result<()> { - let bech32_hrp = self.client().get_bech32_hrp().await?; - log::debug!("[UPDATE ACCOUNT WITH BECH32 HRP] new bech32_hrp: {}", bech32_hrp); - let mut account_details = self.details_mut().await; - for address in &mut account_details.addresses_with_unspent_outputs { - address.address.hrp = bech32_hrp; - } - for address in &mut account_details.public_addresses { - address.address.hrp = bech32_hrp; - } - for address in &mut account_details.internal_addresses { - address.address.hrp = bech32_hrp; - } - - account_details.inaccessible_incoming_transactions.clear(); - - #[cfg(feature = "storage")] - { - log::debug!( - "[SYNC] storing account {} after updating it with new bech32 hrp", - account_details.alias() - ); - self.save(Some(&account_details)).await?; - } - - Ok(()) - } -} diff --git a/sdk/src/wallet/account/constants.rs b/sdk/src/wallet/constants.rs similarity index 94% rename from sdk/src/wallet/account/constants.rs rename to sdk/src/wallet/constants.rs index bdd3b81fa1..59cf49b8f2 100644 --- a/sdk/src/wallet/account/constants.rs +++ b/sdk/src/wallet/constants.rs @@ -11,7 +11,7 @@ pub(crate) const DEFAULT_LEDGER_OUTPUT_CONSOLIDATION_THRESHOLD: usize = 15; /// Amount of API request that can be sent in parallel during syncing pub(crate) const PARALLEL_REQUESTS_AMOUNT: usize = 500; -/// ms before an account actually syncs with the network, before it just returns the previous syncing result +/// ms before the wallet actually syncs with the network, before it just returns the previous syncing result /// this is done to prevent unnecessary simultaneous synchronizations pub(crate) const MIN_SYNC_INTERVAL: u128 = 5; diff --git a/sdk/src/wallet/core/builder.rs b/sdk/src/wallet/core/builder.rs index 31e2bba876..06a24ebb16 100644 --- a/sdk/src/wallet/core/builder.rs +++ b/sdk/src/wallet/core/builder.rs @@ -1,16 +1,12 @@ // Copyright 2021 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use std::sync::{ - atomic::{AtomicU32, AtomicUsize}, - Arc, -}; #[cfg(feature = "storage")] -use std::{collections::HashSet, sync::atomic::Ordering}; +use std::collections::HashSet; +use std::sync::{atomic::AtomicUsize, Arc}; -use futures::{future::try_join_all, FutureExt}; use serde::Serialize; -use tokio::sync::RwLock; +use tokio::sync::{Mutex, RwLock}; use super::operations::storage::SaveLoadWallet; #[cfg(feature = "events")] @@ -18,21 +14,25 @@ use crate::wallet::events::EventEmitter; #[cfg(all(feature = "storage", not(feature = "rocksdb")))] use crate::wallet::storage::adapter::memory::Memory; #[cfg(feature = "storage")] -use crate::wallet::{ - account::AccountDetails, - storage::{StorageManager, StorageOptions}, -}; +use crate::wallet::storage::{StorageManager, StorageOptions}; use crate::{ - client::secret::{SecretManage, SecretManager}, - wallet::{core::WalletInner, Account, ClientOptions, Wallet}, + client::secret::{GenerateAddressOptions, SecretManage, SecretManager}, + types::block::address::{Address, Bech32Address}, + wallet::{ + core::{Bip44, WalletData, WalletInner}, + operations::syncing::SyncOptions, + ClientOptions, Wallet, + }, }; /// Builder for the wallet. #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct WalletBuilder { + pub(crate) bip_path: Option, + pub(crate) address: Option, + pub(crate) alias: Option, pub(crate) client_options: Option, - pub(crate) coin_type: Option, #[cfg(feature = "storage")] pub(crate) storage_options: Option, #[serde(skip)] @@ -42,8 +42,10 @@ pub struct WalletBuilder { impl Default for WalletBuilder { fn default() -> Self { Self { + bip_path: Default::default(), + address: Default::default(), + alias: Default::default(), client_options: Default::default(), - coin_type: Default::default(), #[cfg(feature = "storage")] storage_options: Default::default(), secret_manager: Default::default(), @@ -63,15 +65,27 @@ where } } - /// Set the client options for the core nodes. - pub fn with_client_options(mut self, client_options: impl Into>) -> Self { - self.client_options = client_options.into(); + /// Set the BIP44 path of the wallet. + pub fn with_bip_path(mut self, bip_path: impl Into>) -> Self { + self.bip_path = bip_path.into(); self } - /// Set the coin type for the wallet. Registered coin types can be found at . - pub fn with_coin_type(mut self, coin_type: impl Into>) -> Self { - self.coin_type = coin_type.into(); + /// Set the wallet address. + pub fn with_address(mut self, address: impl Into>) -> Self { + self.address = address.into(); + self + } + + /// Set the alias of the wallet. + pub fn with_alias(mut self, alias: impl Into>) -> Self { + self.alias = alias.into(); + self + } + + /// Set the client options for the core nodes. + pub fn with_client_options(mut self, client_options: impl Into>) -> Self { + self.client_options = client_options.into(); self } @@ -111,9 +125,10 @@ where impl WalletBuilder where crate::wallet::Error: From, + crate::client::Error: From, Self: SaveLoadWallet, { - /// Builds the wallet + /// Builds the wallet. pub async fn finish(mut self) -> crate::wallet::Result> { log::debug!("[WalletBuilder]"); @@ -126,12 +141,6 @@ where if self.client_options.is_none() { return Err(crate::wallet::Error::MissingParameter("client_options")); } - if self.coin_type.is_none() { - return Err(crate::wallet::Error::MissingParameter("coin_type")); - } - if self.secret_manager.is_none() { - return Err(crate::wallet::Error::MissingParameter("secret_manager")); - } } #[cfg(all(feature = "rocksdb", feature = "storage"))] @@ -144,56 +153,79 @@ where let mut storage_manager = StorageManager::new(storage, storage_options.encryption_key.clone()).await?; #[cfg(feature = "storage")] - let read_manager_builder = Self::load(&storage_manager).await?; + let loaded_wallet_builder = Self::load(&storage_manager).await?; #[cfg(not(feature = "storage"))] - let read_manager_builder: Option = None; + let loaded_wallet_builder: Option = None; - // Prioritize provided client_options and secret_manager over stored ones - let new_provided_client_options = if self.client_options.is_none() { - let loaded_client_options = read_manager_builder + // May use a previously stored client options if those weren't provided + let provided_client_options = if self.client_options.is_none() { + let loaded_client_options = loaded_wallet_builder .as_ref() .and_then(|data| data.client_options.clone()) .ok_or(crate::wallet::Error::MissingParameter("client_options"))?; // Update self so it gets used and stored again - self.client_options.replace(loaded_client_options); + self.client_options = Some(loaded_client_options); false } else { true }; + // May use a previously stored secret manager if it wasn't provided if self.secret_manager.is_none() { - let secret_manager = read_manager_builder + let secret_manager = loaded_wallet_builder .as_ref() - .and_then(|data| data.secret_manager.clone()) - .ok_or(crate::wallet::Error::MissingParameter("secret_manager"))?; + .and_then(|builder| builder.secret_manager.clone()); - // Update self so it gets used and stored again - self.secret_manager.replace(secret_manager); + self.secret_manager = secret_manager; + } + + // May use a previously stored BIP path if it wasn't provided + if self.bip_path.is_none() { + self.bip_path = loaded_wallet_builder.as_ref().and_then(|builder| builder.bip_path); } - if self.coin_type.is_none() { - self.coin_type = read_manager_builder.and_then(|builder| builder.coin_type); + // May use a previously stored wallet alias if it wasn't provided + if self.alias.is_none() { + self.alias = loaded_wallet_builder.as_ref().and_then(|builder| builder.alias.clone()); } - let coin_type = self.coin_type.ok_or(crate::wallet::Error::MissingParameter( - "coin_type (IOTA: 4218, Shimmer: 4219)", - ))?; + + // May use a previously stored wallet address if it wasn't provided + if self.address.is_none() { + self.address = loaded_wallet_builder + .as_ref() + .and_then(|builder| builder.address.clone()); + } + + // May create a default Ed25519 wallet address if there's a secret manager. + if self.address.is_none() { + if self.secret_manager.is_some() { + let address = self.create_default_wallet_address().await?; + self.address = Some(address); + } else { + return Err(crate::wallet::Error::MissingParameter("address")); + } + } + // Panic: can be safely unwrapped now + let address = self.address.as_ref().unwrap().clone(); #[cfg(feature = "storage")] - let mut accounts = storage_manager.get_accounts().await?; + let mut wallet_data = storage_manager.load_wallet_data().await?; - // Check against potential account coin type before saving the wallet data + // The bip path must not change. #[cfg(feature = "storage")] - if let Some(account) = accounts.first() { - if *account.coin_type() != coin_type { - return Err(crate::wallet::Error::InvalidCoinType { - new_coin_type: coin_type, - existing_coin_type: *account.coin_type(), + if let Some(wallet_data) = &wallet_data { + let new_bip_path = self.bip_path; + let old_bip_path = wallet_data.bip_path; + if new_bip_path != old_bip_path { + return Err(crate::wallet::Error::BipPathMismatch { + new_bip_path, + old_bip_path, }); } } - // Store wallet data in storage + // Store the wallet builder (for convenience reasons) #[cfg(feature = "storage")] self.save(&storage_manager).await?; @@ -203,56 +235,87 @@ where // It happened that inputs got locked, the transaction failed, but they weren't unlocked again, so we do this // here #[cfg(feature = "storage")] - unlock_unused_inputs(&mut accounts)?; - #[cfg(not(feature = "storage"))] - let accounts = Vec::new(); - let wallet_inner = Arc::new(WalletInner { + if let Some(wallet_data) = &mut wallet_data { + unlock_unused_inputs(wallet_data)?; + } + + // Create the node client. + let client = self + .client_options + .clone() + .ok_or(crate::wallet::Error::MissingParameter("client_options"))? + .finish() + .await?; + + // Build the wallet. + let wallet_inner = WalletInner { + default_sync_options: Mutex::new(SyncOptions::default()), + last_synced: Mutex::new(0), background_syncing_status: AtomicUsize::new(0), - client: self - .client_options - .clone() - .ok_or(crate::wallet::Error::MissingParameter("client_options"))? - .finish() - .await?, - coin_type: AtomicU32::new(coin_type), - secret_manager: self - .secret_manager - .ok_or(crate::wallet::Error::MissingParameter("secret_manager"))?, + client, + secret_manager: self.secret_manager.expect("make WalletInner::secret_manager optional?"), #[cfg(feature = "events")] event_emitter, #[cfg(feature = "storage")] storage_options, #[cfg(feature = "storage")] storage_manager: tokio::sync::RwLock::new(storage_manager), - }); - - let mut accounts: Vec> = try_join_all( - accounts - .into_iter() - .map(|a| Account::new(a, wallet_inner.clone()).boxed()), - ) - .await?; + }; + let wallet_data = WalletData::new(self.bip_path, address, self.alias.clone()); + let wallet = Wallet { + inner: Arc::new(wallet_inner), + data: Arc::new(RwLock::new(wallet_data)), + }; // If the wallet builder is not set, it means the user provided it and we need to update the addresses. // In the other case it was loaded from the database and addresses are up to date. - if new_provided_client_options { - for account in accounts.iter_mut() { - // Safe to unwrap because we create the client if accounts aren't empty - account.update_account_bech32_hrp().await?; - } + if provided_client_options { + wallet.update_bech32_hrp().await?; } - Ok(Wallet { - inner: wallet_inner, - accounts: Arc::new(RwLock::new(accounts)), - }) + Ok(wallet) + } + + /// Generate the wallet address. + pub(crate) async fn create_default_wallet_address(&self) -> crate::wallet::Result { + let bech32_hrp = self + .client_options + .as_ref() + .unwrap() + .network_info + .protocol_parameters + .bech32_hrp; + let bip_path = self.bip_path.as_ref().unwrap(); + + Ok(Bech32Address::new( + bech32_hrp, + Address::Ed25519( + self.secret_manager + .as_ref() + .unwrap() + .read() + .await + .generate_ed25519_addresses( + bip_path.coin_type, + bip_path.account, + bip_path.address_index..bip_path.address_index + 1, + GenerateAddressOptions { + internal: bip_path.change != 0, + ledger_nano_prompt: false, + }, + ) + .await?[0], + ), + )) } #[cfg(feature = "storage")] pub(crate) async fn from_wallet(wallet: &Wallet) -> Self { Self { + bip_path: wallet.bip_path().await, + address: Some(wallet.address().await), + alias: wallet.alias().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()), } @@ -262,25 +325,23 @@ where // Check if any of the locked inputs is not used in a transaction and unlock them, so they get available for new // transactions #[cfg(feature = "storage")] -fn unlock_unused_inputs(accounts: &mut [AccountDetails]) -> crate::wallet::Result<()> { +fn unlock_unused_inputs(wallet_data: &mut WalletData) -> crate::wallet::Result<()> { log::debug!("[unlock_unused_inputs]"); - for account in accounts.iter_mut() { - let mut used_inputs = HashSet::new(); - for transaction_id in account.pending_transactions() { - if let Some(tx) = account.transactions().get(transaction_id) { - for input in &tx.inputs { - used_inputs.insert(*input.metadata.output_id()); - } + let mut used_inputs = HashSet::new(); + for transaction_id in &wallet_data.pending_transactions { + if let Some(tx) = wallet_data.transactions.get(transaction_id) { + for input in &tx.inputs { + used_inputs.insert(*input.metadata.output_id()); } } - account.locked_outputs.retain(|input| { - let used = used_inputs.contains(input); - if !used { - log::debug!("unlocking unused input {input}"); - } - used - }) } + wallet_data.locked_outputs.retain(|input| { + let used = used_inputs.contains(input); + if !used { + log::debug!("unlocking unused input {input}"); + } + used + }); Ok(()) } @@ -296,9 +357,13 @@ pub(crate) mod dto { #[serde(rename_all = "camelCase")] pub struct WalletBuilderDto { #[serde(default, skip_serializing_if = "Option::is_none")] - pub(crate) client_options: Option, + pub(crate) bip_path: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) address: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub(crate) coin_type: Option, + pub(crate) alias: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) client_options: Option, #[cfg(feature = "storage")] #[serde(default, skip_serializing_if = "Option::is_none")] pub(crate) storage_options: Option, @@ -307,8 +372,10 @@ pub(crate) mod dto { impl From for WalletBuilder { fn from(value: WalletBuilderDto) -> Self { Self { + bip_path: value.bip_path, + address: value.address, + alias: value.alias, client_options: value.client_options, - coin_type: value.coin_type, #[cfg(feature = "storage")] storage_options: value.storage_options, secret_manager: None, diff --git a/sdk/src/wallet/core/mod.rs b/sdk/src/wallet/core/mod.rs index 0eb401fdf9..568e26b4fc 100644 --- a/sdk/src/wallet/core/mod.rs +++ b/sdk/src/wallet/core/mod.rs @@ -4,19 +4,23 @@ pub(crate) mod builder; pub(crate) mod operations; -use core::sync::atomic::Ordering; -use std::sync::{ - atomic::{AtomicU32, AtomicUsize}, - Arc, +use std::{ + collections::{HashMap, HashSet}, + sync::{atomic::AtomicUsize, Arc}, }; -use crypto::keys::bip39::{Mnemonic, MnemonicRef}; -use tokio::sync::RwLock; +use crypto::keys::{ + bip39::{Mnemonic, MnemonicRef}, + bip44::Bip44, +}; +use serde::{Deserialize, Serialize}; +use tokio::sync::{Mutex, RwLock}; pub use self::builder::WalletBuilder; +use super::types::{TransactionWithMetadata, TransactionWithMetadataDto}; #[cfg(feature = "events")] use crate::wallet::events::{ - types::{Event, WalletEventType}, + types::{WalletEvent, WalletEventType}, EventEmitter, }; #[cfg(feature = "storage")] @@ -26,23 +30,33 @@ use crate::{ secret::{SecretManage, SecretManager}, verify_mnemonic, Client, }, - wallet::account::{builder::AccountBuilder, operations::syncing::SyncOptions, types::Balance, Account}, + types::{ + block::{ + address::{Bech32Address, Hrp}, + output::{dto::FoundryOutputDto, AccountId, FoundryId, FoundryOutput, NftId, Output, OutputId, TokenId}, + payload::signed_transaction::{dto::TransactionDto, Transaction, TransactionId}, + }, + TryFromDto, + }, + wallet::{ + operations::syncing::SyncOptions, + types::{OutputData, OutputDataDto}, + FilterOptions, Result, + }, }; -/// The wallet, used to create and get accounts. One wallet can hold many accounts, but they should -/// all share the same secret_manager type with the same seed/mnemonic. +/// The stateful wallet used to interact with an IOTA network. #[derive(Debug)] pub struct Wallet { pub(crate) inner: Arc>, - // TODO should we use a hashmap instead of a vec? - pub(crate) accounts: Arc>>>, + pub(crate) data: Arc>, } impl Clone for Wallet { fn clone(&self) -> Self { Self { inner: self.inner.clone(), - accounts: self.accounts.clone(), + data: self.data.clone(), } } } @@ -63,20 +77,20 @@ where pub fn builder() -> WalletBuilder { WalletBuilder::::new() } - - /// Create a new account - pub fn create_account(&self) -> AccountBuilder { - log::debug!("creating account"); - AccountBuilder::::new(self.clone()) - } } +/// Wallet inner. #[derive(Debug)] pub struct WalletInner { + // mutex to prevent multiple sync calls at the same or almost the same time, the u128 is a timestamp + // if the last synced time was < `MIN_SYNC_INTERVAL` second ago, we don't sync, but only calculate the balance + // again, because sending transactions can change that + pub(crate) last_synced: Mutex, + pub(crate) default_sync_options: Mutex, // 0 = not running, 1 = running, 2 = stopping pub(crate) background_syncing_status: AtomicUsize, pub(crate) client: Client, - pub(crate) coin_type: AtomicU32, + // TODO: make this optional? pub(crate) secret_manager: Arc>, #[cfg(feature = "events")] pub(crate) event_emitter: tokio::sync::RwLock, @@ -86,93 +100,322 @@ pub struct WalletInner { pub(crate) storage_manager: tokio::sync::RwLock, } +/// Wallet data. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct WalletData { + /// The wallet BIP44 path. + pub(crate) bip_path: Option, + /// The wallet address. + pub(crate) address: Bech32Address, + /// The wallet alias. + pub(crate) alias: Option, + /// Outputs + // stored separated from the wallet for performance? + pub(crate) outputs: HashMap, + /// Unspent outputs that are currently used as input for transactions + // outputs used in transactions should be locked here so they don't get used again, which would result in a + // conflicting transaction + pub(crate) locked_outputs: HashSet, + /// Unspent outputs + // have unspent outputs in a separated hashmap so we don't need to iterate over all outputs we have + pub(crate) unspent_outputs: HashMap, + /// Sent transactions + // stored separated from the wallet for performance and only the transaction id here? where to add the network id? + // transactions: HashSet, + pub(crate) transactions: HashMap, + /// Pending transactions + // Maybe pending transactions even additionally separated? + pub(crate) pending_transactions: HashSet, + /// Transaction payloads for received outputs with inputs when not pruned before syncing, can be used to determine + /// the sender address(es) + pub(crate) incoming_transactions: HashMap, + /// Some incoming transactions can be pruned by the node before we requested them, then this node can never return + /// it. To avoid useless requests, these transaction ids are stored here and cleared when new client options are + /// set, because another node might still have them. + pub(crate) inaccessible_incoming_transactions: HashSet, + /// Foundries for native tokens in outputs + pub(crate) native_token_foundries: HashMap, +} + +impl WalletData { + pub(crate) fn new(bip_path: Option, address: Bech32Address, alias: Option) -> Self { + Self { + bip_path, + address, + alias, + outputs: HashMap::new(), + locked_outputs: HashSet::new(), + unspent_outputs: HashMap::new(), + transactions: HashMap::new(), + pending_transactions: HashSet::new(), + incoming_transactions: HashMap::new(), + inaccessible_incoming_transactions: HashSet::new(), + native_token_foundries: HashMap::new(), + } + } +} + impl Wallet where crate::wallet::Error: From, crate::client::Error: From, { - /// Get all accounts - pub async fn get_accounts(&self) -> crate::wallet::Result>> { - Ok(self.accounts.read().await.clone()) - } - - /// Get all account aliases - pub async fn get_account_aliases(&self) -> crate::wallet::Result> { - let accounts = self.accounts.read().await; - let mut account_aliases = Vec::with_capacity(accounts.len()); - for handle in accounts.iter() { - account_aliases.push(handle.details().await.alias().clone()); + /// Create a new wallet. + pub(crate) async fn new(inner: Arc>, data: WalletData) -> Result { + #[cfg(feature = "storage")] + let default_sync_options = inner + .storage_manager + .read() + .await + .get_default_sync_options() + .await? + .unwrap_or_default(); + #[cfg(not(feature = "storage"))] + let default_sync_options = Default::default(); + + // TODO: maybe move this into a `reset` fn or smth to avoid this kinda-weird block. + { + let mut last_synced = inner.last_synced.lock().await; + *last_synced = Default::default(); + let mut sync_options = inner.default_sync_options.lock().await; + *sync_options = default_sync_options; } - Ok(account_aliases) + + Ok(Self { + inner, + data: Arc::new(RwLock::new(data)), + }) } - /// Removes the latest account (account with the largest account index). - pub async fn remove_latest_account(&self) -> crate::wallet::Result<()> { - let mut largest_account_index_opt = None; - let mut accounts = self.accounts.write().await; + /// Get the [`Output`] that minted a native token by the token ID. First try to get it + /// from the wallet, if it isn't in the wallet try to get it from the node + pub async fn get_foundry_output(&self, native_token_id: TokenId) -> Result { + let foundry_id = FoundryId::from(native_token_id); - for account in accounts.iter() { - let account_index = *account.details().await.index(); - if let Some(largest_account_index) = largest_account_index_opt { - if account_index > largest_account_index { - largest_account_index_opt = Some(account_index); + for output_data in self.data.read().await.outputs.values() { + if let Output::Foundry(foundry_output) = &output_data.output { + if foundry_output.id() == foundry_id { + return Ok(output_data.output.clone()); } - } else { - largest_account_index_opt = Some(account_index) } } - if let Some(largest_account_index) = largest_account_index_opt { - for i in 0..accounts.len() { - if let Some(account) = accounts.get(i) { - if *account.details().await.index() == largest_account_index { - let _ = accounts.remove(i); + // Foundry was not found in the wallet, try to get it from the node + let foundry_output_id = self.client().foundry_output_id(foundry_id).await?; + let output = self.client().get_output(&foundry_output_id).await?; + + Ok(output) + } + + /// Save the wallet to the database, accepts the updated wallet data as option so we don't need to drop it before + /// saving + #[cfg(feature = "storage")] + pub(crate) async fn save(&self, updated_wallet: Option<&WalletData>) -> Result<()> { + log::debug!("[save] wallet data"); + match updated_wallet { + Some(wallet) => { + let mut storage_manager = self.storage_manager.write().await; + storage_manager.save_wallet_data(wallet).await?; + drop(storage_manager); + } + None => { + let wallet_data = self.data.read().await; + let mut storage_manager = self.storage_manager.write().await; + storage_manager.save_wallet_data(&wallet_data).await?; + drop(storage_manager); + drop(wallet_data); + } + } + Ok(()) + } + + #[cfg(feature = "events")] + pub(crate) async fn emit(&self, wallet_event: super::events::types::WalletEvent) { + self.inner.emit(wallet_event).await + } - #[cfg(feature = "storage")] - self.storage_manager - .write() - .await - .remove_account(largest_account_index) - .await?; + pub(crate) async fn data(&self) -> tokio::sync::RwLockReadGuard<'_, WalletData> { + self.data.read().await + } + + pub(crate) async fn data_mut(&self) -> tokio::sync::RwLockWriteGuard<'_, WalletData> { + self.data.write().await + } + + /// Get the alias of the wallet if one was set. + pub async fn alias(&self) -> Option { + self.data().await.alias.clone() + } + + /// Get the wallet address. + pub async fn address(&self) -> Bech32Address { + self.data().await.address.clone() + } + + /// Get the wallet's configured Bech32 HRP. + pub async fn bech32_hrp(&self) -> Hrp { + self.data().await.address.hrp + } + + /// Get the wallet's configured bip path. + pub async fn bip_path(&self) -> Option { + self.data().await.bip_path + } + + /// Get the [`OutputData`] of an output stored in the wallet. + pub async fn get_output(&self, output_id: &OutputId) -> Option { + self.data().await.outputs.get(output_id).cloned() + } + + /// Get the [`TransactionWithMetadata`] of a transaction stored in the wallet. + pub async fn get_transaction(&self, transaction_id: &TransactionId) -> Option { + self.data().await.transactions.get(transaction_id).cloned() + } - return Ok(()); + /// Get the transaction with inputs of an incoming transaction stored in the wallet. + /// List might not be complete, if the node pruned the data already + pub async fn get_incoming_transaction(&self, transaction_id: &TransactionId) -> Option { + self.data().await.incoming_transactions.get(transaction_id).cloned() + } + + fn filter_outputs<'a>( + &self, + outputs: impl Iterator, + filter: impl Into>, + ) -> Result> { + let filter = filter.into(); + + if let Some(filter) = filter { + let mut filtered_outputs = Vec::new(); + + for output in outputs { + match &output.output { + Output::Account(alias) => { + if let Some(account_ids) = &filter.account_ids { + let account_id = alias.account_id_non_null(&output.output_id); + if account_ids.contains(&account_id) { + filtered_outputs.push(output.clone()); + continue; + } + } + } + Output::Foundry(foundry) => { + if let Some(foundry_ids) = &filter.foundry_ids { + let foundry_id = foundry.id(); + if foundry_ids.contains(&foundry_id) { + filtered_outputs.push(output.clone()); + continue; + } + } } + Output::Nft(nft) => { + if let Some(nft_ids) = &filter.nft_ids { + let nft_id = nft.nft_id_non_null(&output.output_id); + if nft_ids.contains(&nft_id) { + filtered_outputs.push(output.clone()); + continue; + } + } + } + _ => {} + } + + // TODO filter based on slot index + // if let Some(lower_bound_booked_timestamp) = filter.lower_bound_booked_timestamp { + // if output.metadata.milestone_timestamp_booked() < lower_bound_booked_timestamp { + // continue; + // } + // } + // if let Some(upper_bound_booked_timestamp) = filter.upper_bound_booked_timestamp { + // if output.metadata.milestone_timestamp_booked() > upper_bound_booked_timestamp { + // continue; + // } + // } + + if let Some(output_types) = &filter.output_types { + if !output_types.contains(&output.output.kind()) { + continue; + } + } + + // If ids are provided, only return them and no other outputs. + if filter.account_ids.is_none() && filter.foundry_ids.is_none() && filter.nft_ids.is_none() { + filtered_outputs.push(output.clone()); } } + + Ok(filtered_outputs) + } else { + Ok(outputs.cloned().collect()) } + } - Ok(()) + /// Returns outputs of the wallet. + pub async fn outputs(&self, filter: impl Into> + Send) -> Result> { + self.filter_outputs(self.data().await.outputs.values(), filter) } - /// Get the balance of all accounts added together - pub async fn balance(&self) -> crate::wallet::Result { - let mut balance = Balance::default(); - let accounts = self.accounts.read().await; + /// Returns unspent outputs of the wallet. + pub async fn unspent_outputs(&self, filter: impl Into> + Send) -> Result> { + self.filter_outputs(self.data().await.unspent_outputs.values(), filter) + } - for account in accounts.iter() { - balance += account.balance().await?; - } + /// Gets the unspent account output matching the given ID. + pub async fn unspent_account_output(&self, account_id: &AccountId) -> Result> { + self.unspent_outputs(FilterOptions { + account_ids: Some([*account_id].into()), + ..Default::default() + }) + .await + .map(|res| res.get(0).cloned()) + } + + /// Gets the unspent foundry output matching the given ID. + pub async fn unspent_foundry_output(&self, foundry_id: &FoundryId) -> Result> { + self.unspent_outputs(FilterOptions { + foundry_ids: Some([*foundry_id].into()), + ..Default::default() + }) + .await + .map(|res| res.get(0).cloned()) + } + + /// Gets the unspent nft output matching the given ID. + pub async fn unspent_nft_output(&self, nft_id: &NftId) -> Result> { + self.unspent_outputs(FilterOptions { + nft_ids: Some([*nft_id].into()), + ..Default::default() + }) + .await + .map(|res| res.get(0).cloned()) + } + + /// Returns all incoming transactions of the wallet + pub async fn incoming_transactions(&self) -> Vec { + self.data().await.incoming_transactions.values().cloned().collect() + } - Ok(balance) + /// Returns all transactions of the wallet + pub async fn transactions(&self) -> Vec { + self.data().await.transactions.values().cloned().collect() } - /// Sync all accounts - pub async fn sync(&self, options: Option) -> crate::wallet::Result { - let mut balance = Balance::default(); + /// Returns all pending transactions of the wallet + pub async fn pending_transactions(&self) -> Vec { + let mut transactions = Vec::new(); + let wallet_data = self.data().await; - for account in self.accounts.read().await.iter() { - balance += account.sync(options.clone()).await?; + for transaction_id in &wallet_data.pending_transactions { + if let Some(transaction) = wallet_data.transactions.get(transaction_id) { + transactions.push(transaction.clone()); + } } - Ok(balance) + transactions } } impl WalletInner { - pub fn coin_type(&self) -> u32 { - self.coin_type.load(Ordering::Relaxed) - } - /// Get the [SecretManager] pub fn get_secret_manager(&self) -> &Arc> { &self.secret_manager @@ -184,7 +427,7 @@ impl WalletInner { pub async fn listen + Send>(&self, events: I, handler: F) where I::IntoIter: Send, - F: Fn(&Event) + 'static + Send + Sync, + F: Fn(&WalletEvent) + 'static + Send + Sync, { let mut emitter = self.event_emitter.write().await; emitter.on(events, handler); @@ -213,15 +456,15 @@ impl WalletInner { } #[cfg(feature = "events")] - pub(crate) async fn emit(&self, account_index: u32, event: crate::wallet::events::types::WalletEvent) { - self.event_emitter.read().await.emit(account_index, event); + pub(crate) async fn emit(&self, event: crate::wallet::events::types::WalletEvent) { + self.event_emitter.read().await.emit(event); } - /// Helper function to test events. Emits a provided event with account index 0. + /// Helper function to test events. #[cfg(feature = "events")] #[cfg_attr(docsrs, doc(cfg(feature = "events")))] pub async fn emit_test_event(&self, event: crate::wallet::events::types::WalletEvent) { - self.emit(0, event).await + self.emit(event).await } } @@ -230,3 +473,244 @@ impl Drop for Wallet { log::debug!("drop Wallet"); } } + +/// Dto for the wallet data. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WalletDataDto { + pub bip_path: Option, + pub address: Bech32Address, + pub alias: Option, + pub outputs: HashMap, + pub locked_outputs: HashSet, + pub unspent_outputs: HashMap, + pub transactions: HashMap, + pub pending_transactions: HashSet, + pub incoming_transactions: HashMap, + #[serde(default)] + pub native_token_foundries: HashMap, +} + +impl TryFromDto for WalletData { + type Dto = WalletDataDto; + type Error = crate::wallet::Error; + + fn try_from_dto_with_params_inner( + dto: Self::Dto, + params: crate::types::ValidationParams<'_>, + ) -> core::result::Result { + Ok(Self { + bip_path: dto.bip_path, + address: dto.address, + alias: dto.alias, + outputs: dto + .outputs + .into_iter() + .map(|(id, o)| Ok((id, OutputData::try_from_dto_with_params(o, ¶ms)?))) + .collect::>()?, + locked_outputs: dto.locked_outputs, + unspent_outputs: dto + .unspent_outputs + .into_iter() + .map(|(id, o)| Ok((id, OutputData::try_from_dto_with_params(o, ¶ms)?))) + .collect::>()?, + transactions: dto + .transactions + .into_iter() + .map(|(id, o)| Ok((id, TransactionWithMetadata::try_from_dto_with_params(o, ¶ms)?))) + .collect::>()?, + pending_transactions: dto.pending_transactions, + incoming_transactions: dto + .incoming_transactions + .into_iter() + .map(|(id, o)| Ok((id, TransactionWithMetadata::try_from_dto_with_params(o, ¶ms)?))) + .collect::>()?, + inaccessible_incoming_transactions: Default::default(), + native_token_foundries: dto + .native_token_foundries + .into_iter() + .map(|(id, o)| Ok((id, FoundryOutput::try_from_dto_with_params(o, ¶ms)?))) + .collect::>()?, + }) + } +} + +impl From<&WalletData> for WalletDataDto { + fn from(value: &WalletData) -> Self { + Self { + bip_path: value.bip_path, + address: value.address.clone(), + alias: value.alias.clone(), + outputs: value + .outputs + .iter() + .map(|(id, output)| (*id, OutputDataDto::from(output))) + .collect(), + locked_outputs: value.locked_outputs.clone(), + unspent_outputs: value + .unspent_outputs + .iter() + .map(|(id, output)| (*id, OutputDataDto::from(output))) + .collect(), + transactions: value + .transactions + .iter() + .map(|(id, transaction)| (*id, TransactionWithMetadataDto::from(transaction))) + .collect(), + pending_transactions: value.pending_transactions.clone(), + incoming_transactions: value + .incoming_transactions + .iter() + .map(|(id, transaction)| (*id, TransactionWithMetadataDto::from(transaction))) + .collect(), + native_token_foundries: value + .native_token_foundries + .iter() + .map(|(id, foundry)| (*id, FoundryOutputDto::from(foundry))) + .collect(), + } + } +} + +#[cfg(test)] +mod test { + use core::str::FromStr; + + use pretty_assertions::assert_eq; + + use super::*; + use crate::{ + types::block::{ + address::{Address, Ed25519Address}, + input::{Input, UtxoInput}, + output::{AddressUnlockCondition, BasicOutput, Output}, + payload::signed_transaction::{SignedTransactionPayload, Transaction, TransactionId}, + protocol::ProtocolParameters, + rand::mana::rand_mana_allotment, + signature::{Ed25519Signature, Signature}, + unlock::{ReferenceUnlock, SignatureUnlock, Unlock, Unlocks}, + }, + wallet::types::InclusionState, + }; + + const TRANSACTION_ID: &str = "0x24a1f46bdb6b2bf38f1c59f73cdd4ae5b418804bb231d76d06fbf246498d588300000000"; + const ED25519_ADDRESS: &str = "0xe594f9a895c0e0a6760dd12cffc2c3d1e1cbf7269b328091f96ce3d0dd550b75"; + const ED25519_PUBLIC_KEY: &str = "0x1da5ddd11ba3f961acab68fafee3177d039875eaa94ac5fdbff8b53f0c50bfb9"; + const ED25519_SIGNATURE: &str = "0xc6a40edf9a089f42c18f4ebccb35fe4b578d93b879e99b87f63573324a710d3456b03fb6d1fcc027e6401cbd9581f790ee3ed7a3f68e9c225fcb9f1cd7b7110d"; + + #[test] + fn serialize() { + let protocol_parameters = ProtocolParameters::new( + 2, + "testnet", + "rms", + crate::types::block::output::RentStructure::new(500, 1, 10, 1, 1, 1), + 1_813_620_509_061_365, + 1582328545, + 10, + 20, + ) + .unwrap(); + + let transaction_id = TransactionId::new(prefix_hex::decode(TRANSACTION_ID).unwrap()); + let input1 = Input::Utxo(UtxoInput::new(transaction_id, 0).unwrap()); + let input2 = Input::Utxo(UtxoInput::new(transaction_id, 1).unwrap()); + let bytes: [u8; 32] = prefix_hex::decode(ED25519_ADDRESS).unwrap(); + let address = Address::from(Ed25519Address::new(bytes)); + let amount = 1_000_000; + let output = Output::Basic( + BasicOutput::build_with_amount(amount) + .add_unlock_condition(AddressUnlockCondition::new(address)) + .finish_with_params(protocol_parameters.clone()) + .unwrap(), + ); + let transaction = Transaction::builder(protocol_parameters.network_id()) + .with_inputs([input1, input2]) + .add_output(output) + .add_mana_allotment(rand_mana_allotment(&protocol_parameters)) + .finish_with_params(protocol_parameters) + .unwrap(); + + let pub_key_bytes = prefix_hex::decode(ED25519_PUBLIC_KEY).unwrap(); + let sig_bytes = prefix_hex::decode(ED25519_SIGNATURE).unwrap(); + let signature = Ed25519Signature::try_from_bytes(pub_key_bytes, sig_bytes).unwrap(); + let sig_unlock = Unlock::from(SignatureUnlock::from(Signature::from(signature))); + let ref_unlock = Unlock::from(ReferenceUnlock::new(0).unwrap()); + let unlocks = Unlocks::new([sig_unlock, ref_unlock]).unwrap(); + + let tx_payload = SignedTransactionPayload::new(transaction, unlocks).unwrap(); + + let incoming_transaction = TransactionWithMetadata { + transaction_id: TransactionId::from_str( + "0x131fc4cb8f315ae36ae3bf6a4e4b3486d5f17581288f1217410da3e0700d195a00000000", + ) + .unwrap(), + payload: tx_payload, + block_id: None, + network_id: 0, + timestamp: 0, + inclusion_state: InclusionState::Pending, + incoming: false, + note: None, + inputs: Vec::new(), + }; + + let mut incoming_transactions = HashMap::new(); + incoming_transactions.insert( + TransactionId::from_str("0x131fc4cb8f315ae36ae3bf6a4e4b3486d5f17581288f1217410da3e0700d195a00000000") + .unwrap(), + incoming_transaction, + ); + + let wallet_data = WalletData { + bip_path: Some(Bip44::new(4218)), + address: crate::types::block::address::Bech32Address::from_str( + "rms1qpllaj0pyveqfkwxmnngz2c488hfdtmfrj3wfkgxtk4gtyrax0jaxzt70zy", + ) + .unwrap(), + alias: Some("Alice".to_string()), + outputs: HashMap::new(), + locked_outputs: HashSet::new(), + unspent_outputs: HashMap::new(), + transactions: HashMap::new(), + pending_transactions: HashSet::new(), + incoming_transactions, + inaccessible_incoming_transactions: HashSet::new(), + native_token_foundries: HashMap::new(), + }; + + let deser_wallet_data = WalletData::try_from_dto( + serde_json::from_str::(&serde_json::to_string(&WalletDataDto::from(&wallet_data)).unwrap()) + .unwrap(), + ) + .unwrap(); + + assert_eq!(wallet_data, deser_wallet_data); + } + + impl WalletData { + /// Returns a mock of this type with the following values: + /// index: 0, coin_type: 4218, alias: "Alice", address: + /// rms1qpllaj0pyveqfkwxmnngz2c488hfdtmfrj3wfkgxtk4gtyrax0jaxzt70zy, all other fields are set to their Rust + /// defaults. + #[cfg(feature = "storage")] + pub(crate) fn mock() -> Self { + Self { + bip_path: Some(Bip44::new(4218)), + address: crate::types::block::address::Bech32Address::from_str( + "rms1qpllaj0pyveqfkwxmnngz2c488hfdtmfrj3wfkgxtk4gtyrax0jaxzt70zy", + ) + .unwrap(), + alias: Some("Alice".to_string()), + outputs: HashMap::new(), + locked_outputs: HashSet::new(), + unspent_outputs: HashMap::new(), + transactions: HashMap::new(), + pending_transactions: HashSet::new(), + incoming_transactions: HashMap::new(), + inaccessible_incoming_transactions: HashSet::new(), + native_token_foundries: HashMap::new(), + } + } + } +} diff --git a/sdk/src/wallet/core/operations/account_recovery.rs b/sdk/src/wallet/core/operations/account_recovery.rs deleted file mode 100644 index 743639080d..0000000000 --- a/sdk/src/wallet/core/operations/account_recovery.rs +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright 2021 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use instant::Instant; - -use crate::{ - client::secret::SecretManage, - wallet::{account::SyncOptions, task, Account, Wallet}, -}; - -impl Wallet -where - crate::wallet::Error: From, - crate::client::Error: From, -{ - /// Find accounts with unspent outputs. - /// - /// Arguments: - /// - /// * `account_start_index`: The index of the first account to search for. - /// * `account_gap_limit`: The number of accounts to search for, after the last account with unspent outputs. - /// * `address_gap_limit`: The number of addresses to search for, after the last address with unspent outputs, in - /// each account. - /// * `sync_options`: Optional parameter to specify the sync options. The `address_start_index` and `force_syncing` - /// fields will be overwritten to skip existing addresses. - /// - /// Returns: - /// - /// A vector of Account - pub async fn recover_accounts( - &self, - account_start_index: u32, - account_gap_limit: u32, - address_gap_limit: u32, - sync_options: Option, - ) -> crate::wallet::Result>> { - log::debug!("[recover_accounts]"); - let start_time = Instant::now(); - let mut max_account_index_to_keep = None; - - // Search for addresses in current accounts - for account in self.accounts.read().await.iter() { - // If the gap limit is 0, there is no need to search for funds - if address_gap_limit > 0 { - account - .search_addresses_with_outputs(address_gap_limit, sync_options.clone()) - .await?; - } - let account_index = *account.details().await.index(); - match max_account_index_to_keep { - Some(max_account_index) => { - if account_index > max_account_index { - max_account_index_to_keep = Some(account_index); - } - } - None => max_account_index_to_keep = Some(account_index), - } - } - - // Create accounts below account_start_index, because we don't want to have gaps in the accounts, but we also - // don't want to sync them - for _ in max_account_index_to_keep.unwrap_or(0)..account_start_index { - // Don't return possible errors here, because we could then still have empty accounts - let _ = self.create_account().finish().await; - } - - // Don't return possible errors here already, because we would then still have empty accounts - let new_accounts_discovery_result = self - .search_new_accounts( - account_gap_limit, - address_gap_limit, - &mut max_account_index_to_keep, - sync_options.clone(), - ) - .await; - - // remove accounts without outputs - let mut new_accounts = Vec::new(); - let mut accounts = self.accounts.write().await; - - for account in accounts.iter() { - let account_index = *account.details().await.index(); - let mut keep_account = false; - - if let Some(max_account_index_to_keep) = max_account_index_to_keep { - if account_index <= max_account_index_to_keep { - new_accounts.push((account_index, account.clone())); - keep_account = true; - } - } - - if !keep_account { - // accounts are stored during syncing, delete the empty accounts again - #[cfg(feature = "storage")] - { - log::debug!("[recover_accounts] delete empty account {}", account_index); - self.storage_manager.write().await.remove_account(account_index).await?; - } - } - } - new_accounts.sort_by_key(|(index, _acc)| *index); - *accounts = new_accounts.into_iter().map(|(_, acc)| acc).collect(); - drop(accounts); - - // Handle result after cleaning up the empty accounts - new_accounts_discovery_result?; - - log::debug!("[recover_accounts] finished in {:?}", start_time.elapsed()); - Ok(self.accounts.read().await.clone()) - } - - /// Generate new accounts and search for unspent outputs - async fn search_new_accounts( - &self, - account_gap_limit: u32, - address_gap_limit: u32, - max_account_index_to_keep: &mut Option, - sync_options: Option, - ) -> crate::wallet::Result<()> { - let mut updated_account_gap_limit = account_gap_limit; - loop { - log::debug!("[recover_accounts] generating {updated_account_gap_limit} new accounts"); - - // Generate account with addresses and get their outputs in parallel - let results = futures::future::try_join_all((0..updated_account_gap_limit).map(|_| { - let mut new_account = self.create_account(); - let sync_options_ = sync_options.clone(); - async move { - task::spawn(async move { - let new_account = new_account.finish().await?; - let account_outputs_count = new_account - .search_addresses_with_outputs(address_gap_limit, sync_options_) - .await?; - let account_index = *new_account.details().await.index(); - crate::wallet::Result::Ok((account_index, account_outputs_count)) - }) - .await? - } - })) - .await?; - - let mut new_accounts_with_outputs = 0; - let mut highest_account_index = 0; - for (account_index, outputs_count) in results { - if outputs_count != 0 { - new_accounts_with_outputs += 1; - - match *max_account_index_to_keep { - Some(max_account_index) => { - if account_index > max_account_index { - *max_account_index_to_keep = Some(account_index); - } - } - None => *max_account_index_to_keep = Some(account_index), - } - } - - if account_index > highest_account_index { - highest_account_index = account_index; - } - } - - // Break if there is no new account with outputs - if new_accounts_with_outputs == 0 { - break; - } - - // Update account_gap_limit to only create so many new accounts, that we would check the initial provided - // account_gap_limit amount of empty accounts - if let Some(max_account_index_to_keep) = &max_account_index_to_keep { - let empty_accounts_in_row = highest_account_index - max_account_index_to_keep; - log::debug!("[recover_accounts] empty_accounts_in_row {empty_accounts_in_row}"); - updated_account_gap_limit = account_gap_limit - empty_accounts_in_row; - } - } - - Ok(()) - } -} diff --git a/sdk/src/wallet/core/operations/address_generation.rs b/sdk/src/wallet/core/operations/address_generation.rs index 6fb477fa87..fe902ee8ec 100644 --- a/sdk/src/wallet/core/operations/address_generation.rs +++ b/sdk/src/wallet/core/operations/address_generation.rs @@ -1,12 +1,10 @@ // Copyright 2022 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use std::sync::atomic::Ordering; - use crate::{ client::secret::{GenerateAddressOptions, SecretManage, SecretManager}, - types::block::address::{Ed25519Address, Hrp}, - wallet::Wallet, + types::block::address::Ed25519Address, + wallet::{Error, Wallet}, }; #[cfg(all(feature = "events", feature = "ledger_nano"))] use crate::{ @@ -18,12 +16,7 @@ impl Wallet { /// Generate an address without storing it /// ```ignore /// let public_addresses = wallet - /// .generate_ed25519_addresses( - /// 0, - /// false, - /// 0, - /// None, - /// ) + /// .generate_ed25519_address(None) /// .await?; /// ``` pub async fn generate_ed25519_address( @@ -32,6 +25,10 @@ impl Wallet { address_index: u32, options: impl Into> + Send, ) -> crate::wallet::Result { + // TODO #1279: not sure yet whether we also should allow this method to generate addresses for different bip + // paths. + let coin_type = self.bip_path().await.ok_or(Error::MissingBipPath)?.coin_type; + let address = match &*self.secret_manager.read().await { #[cfg(feature = "ledger_nano")] SecretManager::LedgerNano(ledger_nano) => { @@ -50,74 +47,46 @@ impl Wallet { // Generate without prompt to be able to display it let address = ledger_nano .generate_ed25519_addresses( - self.coin_type.load(Ordering::Relaxed), + coin_type, account_index, address_index..address_index + 1, changed_options, ) .await?; - let bech32_hrp = self.get_bech32_hrp().await?; + let bech32_hrp = self.bech32_hrp().await; - self.emit( - account_index, - WalletEvent::LedgerAddressGeneration(AddressData { - address: address[0].to_bech32(bech32_hrp), - }), - ) + self.emit(WalletEvent::LedgerAddressGeneration(AddressData { + address: address[0].to_bech32(bech32_hrp), + })) .await; } // Generate with prompt so the user can verify ledger_nano - .generate_ed25519_addresses( - self.coin_type.load(Ordering::Relaxed), - account_index, - address_index..address_index + 1, - options, - ) + .generate_ed25519_addresses(coin_type, account_index, address_index..address_index + 1, options) .await? } else { ledger_nano - .generate_ed25519_addresses( - self.coin_type.load(Ordering::Relaxed), - account_index, - address_index..address_index + 1, - options, - ) + .generate_ed25519_addresses(coin_type, account_index, address_index..address_index + 1, options) .await? } } #[cfg(feature = "stronghold")] SecretManager::Stronghold(stronghold) => { stronghold - .generate_ed25519_addresses( - self.coin_type.load(Ordering::Relaxed), - account_index, - address_index..address_index + 1, - options, - ) + .generate_ed25519_addresses(coin_type, account_index, address_index..address_index + 1, options) .await? } SecretManager::Mnemonic(mnemonic) => { mnemonic - .generate_ed25519_addresses( - self.coin_type.load(Ordering::Relaxed), - account_index, - address_index..address_index + 1, - options, - ) + .generate_ed25519_addresses(coin_type, account_index, address_index..address_index + 1, options) .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, - ) + .generate_ed25519_addresses(coin_type, account_index, address_index..address_index + 1, options) .await? } SecretManager::Placeholder => return Err(crate::client::Error::PlaceholderSecretManager.into()), @@ -128,25 +97,3 @@ impl Wallet { .ok_or(crate::wallet::Error::MissingParameter("address"))?) } } - -impl Wallet -where - crate::wallet::Error: From, - crate::client::Error: From, -{ - /// Get the bech32 hrp from the first account address or if not existent, from the client - pub async fn get_bech32_hrp(&self) -> crate::wallet::Result { - Ok(match self.get_accounts().await?.first() { - Some(account) => { - account - .public_addresses() - .await - .first() - .expect("missing first public address") - .address - .hrp - } - None => self.client().get_bech32_hrp().await?, - }) - } -} diff --git a/sdk/src/wallet/core/operations/background_syncing.rs b/sdk/src/wallet/core/operations/background_syncing.rs index 174317c9c2..9840b967b6 100644 --- a/sdk/src/wallet/core/operations/background_syncing.rs +++ b/sdk/src/wallet/core/operations/background_syncing.rs @@ -7,7 +7,7 @@ use tokio::time::sleep; use crate::{ client::secret::SecretManage, - wallet::{account::operations::syncing::SyncOptions, Wallet}, + wallet::{operations::syncing::SyncOptions, Wallet}, }; /// The default interval for background syncing @@ -18,7 +18,7 @@ where crate::wallet::Error: From, crate::client::Error: From, { - /// Start the background syncing process for all accounts, default interval is 7 seconds + /// Start the background syncing process for the wallet, default interval is 7 seconds pub async fn start_background_syncing( &self, options: Option, @@ -49,18 +49,12 @@ where .unwrap(); runtime.block_on(async { 'outer: loop { - log::debug!("[background_syncing]: syncing accounts"); - for account in wallet.accounts.read().await.iter() { - // Check if the process should stop before syncing each account so it stops faster - if wallet.background_syncing_status.load(Ordering::Relaxed) == 2 { - log::debug!("[background_syncing]: stopping"); - break 'outer; - } - match account.sync(options.clone()).await { - Ok(_) => {} - Err(err) => log::debug!("[background_syncing] error: {}", err), - }; + log::debug!("[background_syncing]: syncing wallet"); + + if let Err(err) = wallet.sync(options.clone()).await { + log::debug!("[background_syncing] error: {}", err) } + // split interval syncing to seconds so stopping the process doesn't have to wait long let seconds = interval.unwrap_or(DEFAULT_BACKGROUNDSYNCING_INTERVAL).as_secs(); for _ in 0..seconds { @@ -78,7 +72,7 @@ where Ok(()) } - /// Stop the background syncing of the accounts + /// Stop the background syncing of the wallet pub async fn stop_background_syncing(&self) -> crate::wallet::Result<()> { log::debug!("[stop_background_syncing]"); // immediately return if not running diff --git a/sdk/src/wallet/core/operations/client.rs b/sdk/src/wallet/core/operations/client.rs index 2750e4a9f6..fadc9309ed 100644 --- a/sdk/src/wallet/core/operations/client.rs +++ b/sdk/src/wallet/core/operations/client.rs @@ -30,6 +30,7 @@ impl Wallet { impl Wallet where + crate::client::Error: From, crate::wallet::Error: From, WalletBuilder: SaveLoadWallet, { @@ -67,9 +68,7 @@ where } *self.client.network_info.write().await = network_info; - for account in self.accounts.write().await.iter_mut() { - account.update_account_bech32_hrp().await?; - } + self.update_bech32_hrp().await?; } #[cfg(feature = "storage")] @@ -154,9 +153,7 @@ where .update_node_manager(node_manager_builder.build(HashMap::new())) .await?; - for account in self.accounts.write().await.iter_mut() { - account.update_account_bech32_hrp().await?; - } + self.update_bech32_hrp().await?; Ok(()) } diff --git a/sdk/src/wallet/core/operations/get_account.rs b/sdk/src/wallet/core/operations/get_account.rs deleted file mode 100644 index 463207333e..0000000000 --- a/sdk/src/wallet/core/operations/get_account.rs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2021 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use crate::{ - client::secret::SecretManage, - wallet::{ - account::{types::AccountIdentifier, Account}, - Wallet, - }, -}; - -impl Wallet { - /// Get an account with an AccountIdentifier - pub async fn get_account + Send>( - &self, - identifier: I, - ) -> crate::wallet::Result> { - let account_id = identifier.into(); - let accounts = self.accounts.read().await; - - match &account_id { - AccountIdentifier::Index(index) => { - for account in accounts.iter() { - let account_details = account.details().await; - - if account_details.index() == index { - return Ok(account.clone()); - } - } - } - AccountIdentifier::Alias(alias) => { - for account in accounts.iter() { - let account_details = account.details().await; - - if account_details.alias() == alias { - return Ok(account.clone()); - } - } - } - }; - - Err(crate::wallet::Error::AccountNotFound(serde_json::to_string( - &account_id, - )?)) - } -} - -impl Wallet -where - crate::wallet::Error: From, -{ - pub async fn get_or_create_account(&self, alias: impl Into + Send) -> crate::wallet::Result> { - let alias = alias.into(); - match self.get_account(&alias).await { - Err(crate::wallet::Error::AccountNotFound(_)) => self.create_account().with_alias(alias).finish().await, - res => res, - } - } -} diff --git a/sdk/src/wallet/core/operations/mod.rs b/sdk/src/wallet/core/operations/mod.rs index 8948801765..e01ca173a6 100644 --- a/sdk/src/wallet/core/operations/mod.rs +++ b/sdk/src/wallet/core/operations/mod.rs @@ -1,11 +1,9 @@ // Copyright 2021 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -pub(crate) mod account_recovery; pub(crate) mod address_generation; pub(crate) mod background_syncing; pub(crate) mod client; -pub(crate) mod get_account; #[cfg(feature = "ledger_nano")] pub(crate) mod ledger_nano; pub(crate) mod storage; @@ -13,5 +11,3 @@ pub(crate) mod storage; pub(crate) mod stronghold; #[cfg(feature = "stronghold")] pub(crate) mod stronghold_backup; -#[cfg(debug_assertions)] -pub(crate) mod verify_integrity; diff --git a/sdk/src/wallet/core/operations/storage.rs b/sdk/src/wallet/core/operations/storage.rs index 2109618030..7ccbc935de 100644 --- a/sdk/src/wallet/core/operations/storage.rs +++ b/sdk/src/wallet/core/operations/storage.rs @@ -12,7 +12,7 @@ mod storage_stub { }, wallet::{ core::builder::dto::WalletBuilderDto, - storage::constants::{SECRET_MANAGER_KEY, WALLET_INDEXATION_KEY}, + storage::constants::{SECRET_MANAGER_KEY, WALLET_BUILDER_KEY}, WalletBuilder, }, }; @@ -34,13 +34,13 @@ mod storage_stub { crate::wallet::Error: From, { async fn save(&self, storage: &impl StorageAdapter) -> crate::wallet::Result<()> { - log::debug!("save_wallet_data"); - storage.set(WALLET_INDEXATION_KEY, self).await?; + log::debug!("[save] wallet builder"); + storage.set(WALLET_BUILDER_KEY, self).await?; if let Some(secret_manager) = &self.secret_manager { let secret_manager = secret_manager.read().await; if let Some(config) = secret_manager.to_config() { - log::debug!("save_secret_manager: {config:?}"); + log::debug!("[save] secret manager: {config:?}"); storage.set(SECRET_MANAGER_KEY, &config).await?; } } @@ -50,14 +50,14 @@ mod storage_stub { async fn load( storage: &impl StorageAdapter, ) -> crate::wallet::Result> { - log::debug!("get_wallet_data"); - if let Some(data) = storage.get::(WALLET_INDEXATION_KEY).await? { - log::debug!("get_wallet_data {data:?}"); + log::debug!("[load] wallet builder"); + if let Some(wallet_builder_dto) = storage.get::(WALLET_BUILDER_KEY).await? { + log::debug!("[load] wallet builder dto: {wallet_builder_dto:?}"); let secret_manager_dto = storage.get(SECRET_MANAGER_KEY).await?; - log::debug!("get_secret_manager {secret_manager_dto:?}"); + log::debug!("[load] secret manager dto: {secret_manager_dto:?}"); - Ok(Some(Self::from(data).with_secret_manager( + Ok(Some(Self::from(wallet_builder_dto).with_secret_manager( secret_manager_dto.map(|dto| S::from_config(&dto)).transpose()?, ))) } else { @@ -69,17 +69,17 @@ mod storage_stub { #[async_trait] impl SaveLoadWallet for WalletBuilder { async fn save(&self, storage: &impl StorageAdapter) -> crate::wallet::Result<()> { - log::debug!("save_wallet_data"); - storage.set(WALLET_INDEXATION_KEY, self).await?; + log::debug!("[save] wallet builder"); + storage.set(WALLET_BUILDER_KEY, self).await?; Ok(()) } async fn load( storage: &impl StorageAdapter, ) -> crate::wallet::Result> { - log::debug!("get_wallet_data"); - let res = storage.get::(WALLET_INDEXATION_KEY).await?; - log::debug!("get_wallet_data {res:?}"); + log::debug!("[load] wallet builder"); + let res = storage.get::(WALLET_BUILDER_KEY).await?; + log::debug!("[load] wallet builder: {res:?}"); Ok(res.map(Into::into)) } } diff --git a/sdk/src/wallet/core/operations/stronghold_backup/mod.rs b/sdk/src/wallet/core/operations/stronghold_backup/mod.rs index 4c667bd77a..ee8bb3fca9 100644 --- a/sdk/src/wallet/core/operations/stronghold_backup/mod.rs +++ b/sdk/src/wallet/core/operations/stronghold_backup/mod.rs @@ -3,11 +3,9 @@ pub(crate) mod stronghold_snapshot; -use std::{fs, path::PathBuf, sync::atomic::Ordering}; +use std::{fs, path::PathBuf}; -use futures::{future::try_join_all, FutureExt}; - -use self::stronghold_snapshot::read_data_from_stronghold_snapshot; +use self::stronghold_snapshot::read_wallet_data_from_stronghold_snapshot; #[cfg(feature = "storage")] use crate::wallet::WalletBuilder; use crate::{ @@ -16,7 +14,7 @@ use crate::{ utils::Password, }, types::block::address::Hrp, - wallet::{Account, Wallet}, + wallet::Wallet, }; impl Wallet { @@ -58,19 +56,19 @@ impl Wallet { } /// Restore a backup from a Stronghold file - /// Replaces client_options, coin_type, secret_manager and accounts. Returns an error if accounts were already + /// Replaces client_options, bip_path, secret_manager and wallet. Returns an error if the wallet was already /// created If Stronghold is used as secret_manager, the existing Stronghold file will be overwritten. If a /// mnemonic was stored, it will be gone. - /// if ignore_if_coin_type_mismatch.is_some(), client options will not be restored - /// if ignore_if_coin_type_mismatch == Some(true), client options coin type and accounts will not be restored if the + /// if ignore_if_bip_path_mismatch.is_some(), client options will not be restored + /// if ignore_if_bip_path_mismatch == Some(true), client options coin type and wallet will not be restored if the /// coin type doesn't match - /// if ignore_if_bech32_hrp_mismatch == Some("rms"), but addresses have something different like "smr", no accounts - /// will be restored. + /// If a bech32 hrp is provided to ignore_if_bech32_hrp_mismatch, that doesn't match the one of the current address, + /// the wallet will not be restored. pub async fn restore_backup( &self, backup_path: PathBuf, stronghold_password: impl Into + Send, - ignore_if_coin_type_mismatch: Option, + ignore_if_bip_path_mismatch: Option, ignore_if_bech32_hrp_mismatch: Option, ) -> crate::wallet::Result<()> { let stronghold_password = stronghold_password.into(); @@ -81,12 +79,7 @@ impl Wallet { return Err(crate::wallet::Error::Backup("backup path doesn't exist")); } - // We don't want to overwrite possible existing accounts - if !self.accounts.read().await.is_empty() { - return Err(crate::wallet::Error::Backup( - "can't restore backup when there are already accounts", - )); - } + let mut wallet_data = self.data.write().await; let mut secret_manager = self.secret_manager.as_ref().write().await; // Get the current snapshot path if set @@ -101,25 +94,23 @@ impl Wallet { .password(stronghold_password.clone()) .build(backup_path.clone())?; - let (read_client_options, read_coin_type, read_secret_manager, read_accounts) = - read_data_from_stronghold_snapshot::(&new_stronghold).await?; + let (read_client_options, read_secret_manager, read_wallet_data) = + read_wallet_data_from_stronghold_snapshot::(&new_stronghold).await?; + + let read_bip_path = read_wallet_data.as_ref().and_then(|data| data.bip_path); - // If the coin type is not matching the current one, then the addresses in the accounts will also not be - // correct, so we will not restore them - let ignore_backup_values = ignore_if_coin_type_mismatch.map_or(false, |ignore| { + // If the bip path is not matching the current one, we may ignore the backup + let ignore_backup_values = ignore_if_bip_path_mismatch.map_or(false, |ignore| { if ignore { - read_coin_type.map_or(true, |read_coin_type| { - self.coin_type.load(Ordering::Relaxed) != read_coin_type - }) + // TODO: #1279 okay that if both are none we always load the backup values? + wallet_data.bip_path != read_bip_path } else { false } }); if !ignore_backup_values { - if let Some(read_coin_type) = read_coin_type { - self.coin_type.store(read_coin_type, Ordering::Relaxed); - } + (*wallet_data).bip_path = read_bip_path; } if let Some(mut read_secret_manager) = read_secret_manager { @@ -148,37 +139,21 @@ impl Wallet { // drop secret manager, otherwise we get a deadlock in set_client_options() (there inside of save_wallet_data()) drop(secret_manager); - if ignore_if_coin_type_mismatch.is_none() { + if ignore_if_bip_path_mismatch.is_none() { if let Some(read_client_options) = read_client_options { self.set_client_options(read_client_options).await?; } } - let mut accounts = self.accounts.write().await; - if !ignore_backup_values { - if let Some(read_accounts) = read_accounts { - let restore_accounts = ignore_if_bech32_hrp_mismatch.map_or(true, |expected_bech32_hrp| { + if let Some(read_wallet_data) = read_wallet_data { + let restore_wallet = ignore_if_bech32_hrp_mismatch.map_or(true, |expected_bech32_hrp| { // Only restore if bech32 hrps match - read_accounts.first().map_or(true, |account| { - account - .public_addresses - .first() - .expect("account needs to have a public address") - .address() - .hrp() - == &expected_bech32_hrp - }) + read_wallet_data.address.hrp() == &expected_bech32_hrp }); - if restore_accounts { - let restored_account = try_join_all( - read_accounts - .into_iter() - .map(|a| Account::new(a, self.inner.clone()).boxed()), - ) - .await?; - *accounts = restored_account; + if restore_wallet { + *wallet_data = read_wallet_data; } } } @@ -199,12 +174,12 @@ impl Wallet { .expect("can't convert os string"), ) .with_client_options(self.client_options().await) - .with_coin_type(self.coin_type.load(Ordering::Relaxed)); + .with_bip_path(self.data().await.bip_path); + wallet_builder.save(&*self.storage_manager.read().await).await?; - // also save account to db - for account in accounts.iter() { - account.save(None).await?; - } + + // also save wallet data to db + self.save(Some(&wallet_data)).await?; } Ok(()) @@ -233,20 +208,20 @@ impl Wallet { } /// Restore a backup from a Stronghold file - /// Replaces client_options, coin_type, secret_manager and accounts. Returns an error if accounts were already + /// Replaces client_options, bip path, secret_manager and wallet. Returns an error if the wallet was already /// created If Stronghold is used as secret_manager, the existing Stronghold file will be overwritten. If a /// mnemonic was stored, it will be gone. - /// if ignore_if_coin_type_mismatch.is_some(), client options will not be restored - /// if ignore_if_coin_type_mismatch == Some(true), client options coin type and accounts will not be restored if the - /// coin type doesn't match - /// if ignore_if_bech32_hrp_mismatch == Some("rms"), but addresses have something different like "smr", no accounts - /// will be restored. + /// if ignore_if_bip_path_mismatch.is_some(), client options will not be restored + /// if ignore_if_bip_path_mismatch == Some(true), client options bip path and wallet will not be restored if the + /// bip path doesn't match + /// If a bech32 hrp is provided to ignore_if_bech32_hrp_mismatch, that doesn't match the one of the current address, + /// the wallet will not be restored. pub async fn restore_backup( &self, backup_path: PathBuf, stronghold_password: impl Into + Send, - ignore_if_coin_type_mismatch: Option, - ignore_if_bech32_hrp_mismatch: Option<&str>, + ignore_if_bip_path_mismatch: Option, + ignore_if_bech32_hrp_mismatch: Option, ) -> crate::wallet::Result<()> { let stronghold_password = stronghold_password.into(); @@ -256,11 +231,14 @@ impl Wallet { return Err(crate::wallet::Error::Backup("backup path doesn't exist")); } - let mut accounts = self.accounts.write().await; - // We don't want to overwrite possible existing accounts - if !accounts.is_empty() { + // Will be replaced by the restored wallet data + let mut wallet_data = self.data_mut().await; + + // We don't want to overwrite a possible existing wallet + // TODO not too sure about this, it used to check the presence of accounts, this is not 100% equivalent + if !wallet_data.outputs.is_empty() { return Err(crate::wallet::Error::Backup( - "can't restore backup when there are already accounts", + "can't restore backup when there is already a wallet", )); } @@ -273,25 +251,23 @@ impl Wallet { .password(stronghold_password.clone()) .build(backup_path.clone())?; - let (read_client_options, read_coin_type, read_secret_manager, read_accounts) = - read_data_from_stronghold_snapshot::(&new_stronghold).await?; + let (read_client_options, read_secret_manager, read_wallet_data) = + read_wallet_data_from_stronghold_snapshot::(&new_stronghold).await?; + + let read_bip_path = read_wallet_data.as_ref().and_then(|data| data.bip_path); - // If the coin type is not matching the current one, then the addresses in the accounts will also not be - // correct, so we will not restore them - let ignore_backup_values = ignore_if_coin_type_mismatch.map_or(false, |ignore| { + // If the bip path is not matching the current one, we may ignore the backup + let ignore_backup_values = ignore_if_bip_path_mismatch.map_or(false, |ignore| { if ignore { - read_coin_type.map_or(true, |read_coin_type| { - self.coin_type.load(Ordering::Relaxed) != read_coin_type - }) + // TODO: #1279 okay that if both are none we always load the backup values? + wallet_data.bip_path != read_bip_path } else { false } }); if !ignore_backup_values { - if let Some(read_coin_type) = read_coin_type { - self.coin_type.store(read_coin_type, Ordering::Relaxed); - } + (*wallet_data).bip_path = read_bip_path; } if let Some(mut read_secret_manager) = read_secret_manager { @@ -312,7 +288,7 @@ impl Wallet { drop(secret_manager); // Update Wallet with read data - if ignore_if_coin_type_mismatch.is_none() { + if ignore_if_bip_path_mismatch.is_none() { if let Some(read_client_options) = read_client_options { // If the nodes are from the same network as the current client options, then extend it self.set_client_options(read_client_options).await?; @@ -320,28 +296,14 @@ impl Wallet { } if !ignore_backup_values { - if let Some(read_accounts) = read_accounts { - let restore_accounts = ignore_if_bech32_hrp_mismatch.map_or(true, |expected_bech32_hrp| { + if let Some(read_wallet_data) = read_wallet_data { + let restore_wallet = ignore_if_bech32_hrp_mismatch.map_or(true, |expected_bech32_hrp| { // Only restore if bech32 hrps match - read_accounts.first().map_or(true, |account| { - account - .public_addresses - .first() - .expect("account needs to have a public address") - .address() - .hrp() - == expected_bech32_hrp - }) + read_wallet_data.address.hrp() == &expected_bech32_hrp }); - if restore_accounts { - let restored_account = try_join_all( - read_accounts - .into_iter() - .map(|a| Account::new(a, self.inner.clone()).boxed()), - ) - .await?; - *accounts = restored_account; + if restore_wallet { + *wallet_data = read_wallet_data; } } } @@ -362,12 +324,12 @@ impl Wallet { .expect("can't convert os string"), ) .with_client_options(self.client_options().await) - .with_coin_type(self.coin_type.load(Ordering::Relaxed)); + .with_bip_path(self.data().await.bip_path); + wallet_builder.save(&*self.storage_manager.read().await).await?; - // also save account to db - for account in accounts.iter() { - account.save(None).await?; - } + + // also save wallet data to db + self.save(Some(&wallet_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 5744e8f6c9..68c3eac448 100644 --- a/sdk/src/wallet/core/operations/stronghold_backup/stronghold_snapshot.rs +++ b/sdk/src/wallet/core/operations/stronghold_backup/stronghold_snapshot.rs @@ -1,22 +1,19 @@ // Copyright 2022 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use std::sync::atomic::Ordering; - use crate::{ client::{secret::SecretManagerConfig, storage::StorageAdapter, stronghold::StrongholdAdapter}, types::TryFromDto, wallet::{ - account::{AccountDetails, AccountDetailsDto}, + core::{WalletData, WalletDataDto}, migration::{latest_backup_migration_version, migrate, MIGRATION_VERSION_KEY}, ClientOptions, Error as WalletError, Wallet, }, }; pub(crate) const CLIENT_OPTIONS_KEY: &str = "client_options"; -pub(crate) const COIN_TYPE_KEY: &str = "coin_type"; pub(crate) const SECRET_MANAGER_KEY: &str = "secret_manager"; -pub(crate) const ACCOUNTS_KEY: &str = "accounts"; +pub(crate) const WALLET_DATA_KEY: &str = "wallet_data"; impl Wallet { pub(crate) async fn store_data_to_stronghold(&self, stronghold: &StrongholdAdapter) -> crate::wallet::Result<()> { @@ -28,66 +25,49 @@ impl Wallet { let client_options = self.client_options().await; stronghold.set(CLIENT_OPTIONS_KEY, &client_options).await?; - let coin_type = self.coin_type.load(Ordering::Relaxed); - stronghold.set_bytes(COIN_TYPE_KEY, &coin_type.to_le_bytes()).await?; - if let Some(secret_manager_dto) = self.secret_manager.read().await.to_config() { stronghold.set(SECRET_MANAGER_KEY, &secret_manager_dto).await?; } - let mut serialized_accounts = Vec::new(); - for account in self.accounts.read().await.iter() { - serialized_accounts.push(serde_json::to_value(&AccountDetailsDto::from( - &*account.details().await, - ))?); - } - - stronghold.set(ACCOUNTS_KEY, &serialized_accounts).await?; + let serialized_wallet_data = serde_json::to_value(&WalletDataDto::from(&*self.data.read().await))?; + stronghold.set(WALLET_DATA_KEY, &serialized_wallet_data).await?; Ok(()) } } -pub(crate) async fn read_data_from_stronghold_snapshot( +pub(crate) async fn read_wallet_data_from_stronghold_snapshot( stronghold: &StrongholdAdapter, -) -> crate::wallet::Result<( - Option, - Option, - Option, - Option>, -)> { +) -> crate::wallet::Result<(Option, Option, Option)> { migrate(stronghold).await?; // Get client_options let client_options = stronghold.get(CLIENT_OPTIONS_KEY).await?; - // Get coin_type - let coin_type_bytes = stronghold.get_bytes(COIN_TYPE_KEY).await?; - let coin_type = if let Some(coin_type_bytes) = coin_type_bytes { - let coin_type = u32::from_le_bytes( - coin_type_bytes - .try_into() - .map_err(|_| WalletError::Backup("invalid coin_type"))?, - ); - log::debug!("[restore_backup] restored coin_type: {coin_type}"); - Some(coin_type) - } else { - None - }; + // TODO #1279: remove + // // Get coin_type + // let coin_type_bytes = stronghold.get_bytes(COIN_TYPE_KEY).await?; + // let coin_type = if let Some(coin_type_bytes) = coin_type_bytes { + // let coin_type = u32::from_le_bytes( + // coin_type_bytes + // .try_into() + // .map_err(|_| WalletError::Backup("invalid coin_type"))?, + // ); + // log::debug!("[restore_backup] restored coin_type: {coin_type}"); + // Some(coin_type) + // } else { + // None + // }; // Get secret_manager let restored_secret_manager = stronghold.get(SECRET_MANAGER_KEY).await?; - // Get accounts - let restored_accounts = stronghold - .get::>(ACCOUNTS_KEY) + // Get wallet data + let restored_wallet_data = stronghold + .get::(WALLET_DATA_KEY) .await? - .map(|v| { - v.into_iter() - .map(AccountDetails::try_from_dto) - .collect::, _>>() - }) + .map(|dto| WalletData::try_from_dto(dto)) .transpose()?; - Ok((client_options, coin_type, restored_secret_manager, restored_accounts)) + Ok((client_options, restored_secret_manager, restored_wallet_data)) } diff --git a/sdk/src/wallet/core/operations/verify_integrity.rs b/sdk/src/wallet/core/operations/verify_integrity.rs deleted file mode 100644 index c80a7ba889..0000000000 --- a/sdk/src/wallet/core/operations/verify_integrity.rs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2021 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use crate::{client::secret::SecretManage, wallet::Wallet}; - -impl Wallet { - /// Checks if there is no missing account for example indexes [0, 1, 3] should panic (for now, later return error, - /// automatically fix?) Also checks for each account if there is a gap in an address list and no address is - /// duplicated - pub async fn verify_integrity(&self) -> crate::wallet::Result<()> { - log::debug!("[verify_integrity]"); - - let accounts = self.accounts.read().await; - - // check that no account is missing and they're ordered - // check that no address is missing and they're ordered - for (account_index, account) in accounts.iter().enumerate() { - assert_eq!(accounts[account_index].details().await.index(), &(account_index as u32)); - - let account = account.details().await; - let public_addresses = account.public_addresses(); - for (index, public_address) in public_addresses.iter().enumerate() { - assert_eq!(public_address.key_index, index as u32); - } - let internal_addresses = account.internal_addresses(); - for (index, internal_address) in internal_addresses.iter().enumerate() { - assert_eq!(internal_address.key_index, index as u32); - } - } - - Ok(()) - } -} diff --git a/sdk/src/wallet/error.rs b/sdk/src/wallet/error.rs index bffb97443d..c8d7c3b469 100644 --- a/sdk/src/wallet/error.rs +++ b/sdk/src/wallet/error.rs @@ -3,6 +3,7 @@ use std::fmt::Debug; +use crypto::keys::bip44::Bip44; use serde::{ ser::{SerializeMap, Serializer}, Serialize, @@ -14,15 +15,6 @@ use crate::types::block::{address::Bech32Address, payload::signed_transaction::T #[derive(Debug, thiserror::Error)] #[non_exhaustive] pub enum Error { - /// Account alias must be unique. - #[error("can't create account: account alias {0} already exists")] - AccountAliasAlreadyExists(String), - /// Account not found - #[error("account {0} not found")] - AccountNotFound(String), - /// Address not found in account - #[error("address {0} not found in account")] - AddressNotFoundInAccount(Bech32Address), /// Errors during backup creation or restoring #[error("backup failed {0}")] Backup(&'static str), @@ -35,6 +27,12 @@ pub enum Error { /// Client error. #[error("`{0}`")] Client(Box), + /// BIP44 coin type mismatch + #[error("BIP44 mismatch: {new_bip_path:?}, existing bip path is: {old_bip_path:?}")] + BipPathMismatch { + new_bip_path: Option, + old_bip_path: Option, + }, /// Funds are spread over too many outputs #[error("funds are spread over too many outputs {output_count}/{output_count_max}, consolidation required")] ConsolidationRequired { output_count: usize, output_count_max: u16 }, @@ -44,24 +42,20 @@ pub enum Error { /// Custom input error #[error("custom input error {0}")] CustomInput(String), - /// Failed to get remainder - #[error("failed to get remainder address")] - FailedToGetRemainder, /// Insufficient funds to send transaction. #[error("insufficient funds {available}/{required} available")] InsufficientFunds { available: u64, required: u64 }, - /// Invalid coin type, all accounts need to have the same coin type - #[error("invalid coin type for new account: {new_coin_type}, existing coin type is: {existing_coin_type}")] - InvalidCoinType { - new_coin_type: u32, - existing_coin_type: u32, - }, /// Invalid mnemonic error #[error("invalid mnemonic: {0}")] InvalidMnemonic(String), /// Invalid output kind. #[error("invalid output kind: {0}")] InvalidOutputKind(String), + /// Invalid Voting Power + #[cfg(feature = "participation")] + #[cfg_attr(docsrs, doc(cfg(feature = "participation")))] + #[error("invalid voting power")] + InvalidVotingPower, /// IO error. (storage, backup, restore) #[error("`{0}`")] Io(#[from] std::io::Error), @@ -74,6 +68,9 @@ pub enum Error { /// Minting failed #[error("minting failed {0}")] MintingFailed(String), + /// Missing BIP path. + #[error("missing BIP path")] + MissingBipPath, /// Missing parameter. #[error("missing parameter: {0}")] MissingParameter(&'static str), @@ -116,10 +113,9 @@ pub enum Error { #[cfg_attr(docsrs, doc(cfg(feature = "participation")))] #[error("voting error {0}")] Voting(String), - #[cfg(feature = "participation")] - #[cfg_attr(docsrs, doc(cfg(feature = "participation")))] - #[error("invalid voting power")] - InvalidVotingPower, + /// Address not the wallet address + #[error("address {0} is not the wallet address")] + WalletAddressMismatch(Bech32Address), } // Serialize type with Display error diff --git a/sdk/src/wallet/events/mod.rs b/sdk/src/wallet/events/mod.rs index 7054e488f9..e908a0d1f9 100644 --- a/sdk/src/wallet/events/mod.rs +++ b/sdk/src/wallet/events/mod.rs @@ -9,12 +9,12 @@ use std::{ fmt::{Debug, Formatter, Result}, }; -pub use self::types::{Event, WalletEvent, WalletEventType}; +pub use self::types::{WalletEvent, WalletEventType}; type Handler = Arc; pub struct EventEmitter { - handlers: HashMap>>, + handlers: HashMap>>, } impl EventEmitter { @@ -29,7 +29,7 @@ impl EventEmitter { /// multiple listeners for a single event. pub fn on(&mut self, events: impl IntoIterator, handler: F) where - F: Fn(&Event) + 'static + Send + Sync, + F: Fn(&WalletEvent) + 'static + Send + Sync, { let mut events = events.into_iter().peekable(); let handler = Arc::new(handler); @@ -68,7 +68,7 @@ impl EventEmitter { /// Invokes all listeners of `event`, passing a reference to `payload` as an /// argument to each of them. - pub fn emit(&self, account_index: u32, event: WalletEvent) { + pub fn emit(&self, event: WalletEvent) { let event_type = match &event { WalletEvent::NewOutput(_) => WalletEventType::NewOutput, WalletEvent::SpentOutput(_) => WalletEventType::SpentOutput, @@ -78,7 +78,6 @@ impl EventEmitter { #[cfg(feature = "ledger_nano")] WalletEvent::LedgerAddressGeneration(_) => WalletEventType::LedgerAddressGeneration, }; - let event = Event { account_index, event }; if let Some(handlers) = self.handlers.get(&event_type) { for handler in handlers { handler(&event); @@ -119,7 +118,7 @@ mod tests { types::{TransactionInclusionEvent, TransactionProgressEvent, WalletEvent, WalletEventType}, EventEmitter, }; - use crate::{types::block::payload::signed_transaction::TransactionId, wallet::account::types::InclusionState}; + use crate::{types::block::payload::signed_transaction::TransactionId, wallet::types::InclusionState}; #[test] fn events() { @@ -150,48 +149,40 @@ mod tests { }); // emit events - emitter.emit(0, WalletEvent::ConsolidationRequired); - emitter.emit( - 0, - WalletEvent::TransactionProgress(TransactionProgressEvent::SelectingInputs), - ); - emitter.emit( - 0, - WalletEvent::TransactionInclusion(TransactionInclusionEvent { - transaction_id: TransactionId::from_str( - "0x2289d9981fb23cc5f4f6c2742685eeb480f8476089888aa886a18232bad8198900000000", - ) - .expect("invalid tx id"), - inclusion_state: InclusionState::Confirmed, - }), - ); + emitter.emit(WalletEvent::ConsolidationRequired); + emitter.emit(WalletEvent::TransactionProgress( + TransactionProgressEvent::SelectingInputs, + )); + emitter.emit(WalletEvent::TransactionInclusion(TransactionInclusionEvent { + transaction_id: TransactionId::from_str( + "0x2289d9981fb23cc5f4f6c2742685eeb480f8476089888aa886a18232bad8198900000000", + ) + .expect("invalid tx id"), + inclusion_state: InclusionState::Confirmed, + })); assert_eq!(3, event_counter.load(Ordering::SeqCst)); // remove handlers of single event emitter.clear([WalletEventType::ConsolidationRequired]); // emit event of removed type - emitter.emit(0, WalletEvent::ConsolidationRequired); + emitter.emit(WalletEvent::ConsolidationRequired); assert_eq!(3, event_counter.load(Ordering::SeqCst)); // remove handlers of all events emitter.clear([]); // emit events - emitter.emit( - 0, - WalletEvent::TransactionProgress(TransactionProgressEvent::SelectingInputs), - ); - emitter.emit( - 0, - WalletEvent::TransactionInclusion(TransactionInclusionEvent { - transaction_id: TransactionId::from_str( - "0x2289d9981fb23cc5f4f6c2742685eeb480f8476089888aa886a18232bad8198900000000", - ) - .expect("invalid tx id"), - inclusion_state: InclusionState::Confirmed, - }), - ); + emitter.emit(WalletEvent::TransactionProgress( + TransactionProgressEvent::SelectingInputs, + )); + emitter.emit(WalletEvent::TransactionInclusion(TransactionInclusionEvent { + transaction_id: TransactionId::from_str( + "0x2289d9981fb23cc5f4f6c2742685eeb480f8476089888aa886a18232bad8198900000000", + ) + .expect("invalid tx id"), + inclusion_state: InclusionState::Confirmed, + })); assert_eq!(3, event_counter.load(Ordering::SeqCst)); // listen to a single event @@ -202,7 +193,7 @@ mod tests { }); for _ in 0..1_000_000 { - emitter.emit(0, WalletEvent::ConsolidationRequired); + emitter.emit(WalletEvent::ConsolidationRequired); } assert_eq!(1_000_003, event_counter.load(Ordering::SeqCst)); } diff --git a/sdk/src/wallet/events/types.rs b/sdk/src/wallet/events/types.rs index 3b0f034502..e01fba9bbe 100644 --- a/sdk/src/wallet/events/types.rs +++ b/sdk/src/wallet/events/types.rs @@ -13,18 +13,9 @@ use crate::{ payload::signed_transaction::{dto::SignedTransactionPayloadDto, TransactionId}, }, }, - wallet::account::types::{InclusionState, OutputDataDto}, + wallet::types::{InclusionState, OutputDataDto}, }; -#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Event { - /// Associated account index. - pub account_index: u32, - /// The event - pub event: WalletEvent, -} - #[derive(Clone, Debug, Eq, PartialEq)] #[non_exhaustive] pub enum WalletEvent { diff --git a/sdk/src/wallet/mod.rs b/sdk/src/wallet/mod.rs index 80b9f239b7..07160d4e38 100644 --- a/sdk/src/wallet/mod.rs +++ b/sdk/src/wallet/mod.rs @@ -3,13 +3,18 @@ //! The IOTA Wallet Library -/// [`Account`]: crate::wallet::Account -/// The account module. Interaction with an Account happens via an [`Account`]. -pub mod account; +/// Constants used for the wallet and wallet operations. +pub(crate) mod constants; /// The core module. pub mod core; #[cfg(any(feature = "stronghold", feature = "storage"))] pub mod migration; +/// The wallet operations like address generation, syncing and creating transactions. +pub(crate) mod operations; +/// Types used in a wallet and returned from methods. +pub mod types; +/// Methods to update the wallet state. +pub(crate) mod update; /// The ClientOptions to build the iota_client for interactions with the IOTA Tangle. pub use crate::client::ClientBuilder as ClientOptions; @@ -27,19 +32,94 @@ pub mod storage; /// The module for spawning tasks on a thread pub(crate) mod task; +use std::collections::HashSet; + +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "participation")] +pub use self::operations::participation::{ParticipationEventWithNodes, ParticipationOverview}; +use self::types::TransactionWithMetadata; pub use self::{ - account::{ - operations::transaction::high_level::{ - minting::{create_native_token::CreateNativeTokenParams, mint_nfts::MintNftParams}, - send::SendParams, - send_native_tokens::SendNativeTokensParams, - send_nft::SendNftParams, - }, - Account, - }, core::{Wallet, WalletBuilder}, error::Error, + operations::{ + output_claiming::OutputsToClaim, + output_consolidation::ConsolidationParams, + syncing::{ + options::{AccountSyncOptions, NftSyncOptions, WalletSyncOptions}, + SyncOptions, + }, + transaction::{ + high_level::{ + create_account::CreateAccountParams, + minting::{ + create_native_token::{ + CreateNativeTokenParams, CreateNativeTokenTransactionDto, + PreparedCreateNativeTokenTransactionDto, + }, + mint_nfts::MintNftParams, + }, + send::SendParams, + send_native_tokens::SendNativeTokensParams, + send_nft::SendNftParams, + }, + prepare_output::{Assets, Features, OutputParams, ReturnStrategy, StorageDeposit, Unlocks}, + RemainderValueStrategy, TransactionOptions, + }, + }, + types::OutputDataDto, +}; +use crate::{ + types::{ + api::core::OutputWithMetadataResponse, + block::{ + output::{AccountId, FoundryId, NftId}, + payload::signed_transaction::{SignedTransactionPayload, TransactionId}, + }, + }, + wallet::types::InclusionState, }; /// The wallet Result type. pub type Result = std::result::Result; + +/// Options to filter outputs +#[derive(Debug, Default, Clone, Serialize, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct FilterOptions { + /// Filter all outputs where the booked milestone index is below the specified timestamp + pub lower_bound_booked_timestamp: Option, + /// Filter all outputs where the booked milestone index is above the specified timestamp + pub upper_bound_booked_timestamp: Option, + /// Filter all outputs for the provided types (Basic = 3, Account = 4, Foundry = 5, NFT = 6). + pub output_types: Option>, + /// Return all account outputs matching these IDs. + pub account_ids: Option>, + /// Return all foundry outputs matching these IDs. + pub foundry_ids: Option>, + /// Return all nft outputs matching these IDs. + pub nft_ids: Option>, +} + +pub(crate) fn build_transaction_from_payload_and_inputs( + tx_id: TransactionId, + tx_payload: SignedTransactionPayload, + inputs: Vec, +) -> crate::wallet::Result { + Ok(TransactionWithMetadata { + payload: tx_payload.clone(), + block_id: inputs.first().map(|i| *i.metadata.block_id()), + inclusion_state: InclusionState::Confirmed, + timestamp: 0, + // TODO use slot index since milestone_timestamp_spent is gone + // inputs + // .first() + // .and_then(|i| i.metadata.milestone_timestamp_spent.map(|t| t as u128 * 1000)) + // .unwrap_or_else(|| crate::utils::unix_timestamp_now().as_millis()), + transaction_id: tx_id, + network_id: tx_payload.transaction().network_id(), + incoming: true, + note: None, + inputs, + }) +} diff --git a/sdk/src/wallet/operations/balance.rs b/sdk/src/wallet/operations/balance.rs new file mode 100644 index 0000000000..7551a6d537 --- /dev/null +++ b/sdk/src/wallet/operations/balance.rs @@ -0,0 +1,317 @@ +// Copyright 2022 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use primitive_types::U256; + +use crate::{ + client::secret::SecretManage, + types::block::output::{unlock_condition::UnlockCondition, FoundryId, NativeTokensBuilder, Output, Rent}, + wallet::{ + core::WalletData, + operations::{helpers::time::can_output_be_unlocked_forever_from_now_on, output_claiming::OutputsToClaim}, + types::{Balance, NativeTokensBalance}, + Result, Wallet, + }, +}; + +impl Wallet +where + crate::wallet::Error: From, + crate::client::Error: From, +{ + /// Get the balance of the wallet. + pub async fn balance(&self) -> Result { + log::debug!("[BALANCE] balance"); + + let wallet_data = self.data().await; + + self.balance_inner(&wallet_data).await + } + + async fn balance_inner( + &self, + // addresses_with_unspent_outputs: impl Iterator + Send, + wallet_data: &WalletData, + ) -> Result { + let network_id = self.client().get_network_id().await?; + let rent_structure = self.client().get_rent_structure().await?; + let mut balance = Balance::default(); + let mut total_rent_amount = 0; + let mut total_native_tokens = NativeTokensBuilder::default(); + + #[cfg(feature = "participation")] + let voting_output = self.get_voting_output().await?; + + #[cfg(feature = "participation")] + { + if let Some(voting_output) = &voting_output { + if voting_output.output.as_basic().address() == wallet_data.address.inner() { + balance.base_coin.voting_power = voting_output.output.amount(); + } + } + } + + for (output_id, output_data) in &wallet_data.unspent_outputs { + // Check if output is from the network we're currently connected to + if output_data.network_id != network_id { + continue; + } + + let output = &output_data.output; + let rent = output.rent_cost(rent_structure); + + // Add account and foundry outputs here because they can't have a + // [`StorageDepositReturnUnlockCondition`] or time related unlock conditions + match output { + Output::Account(output) => { + // Add amount + balance.base_coin.total += output.amount(); + // Add storage deposit + balance.required_storage_deposit.account += rent; + if !wallet_data.locked_outputs.contains(output_id) { + total_rent_amount += rent; + } + // Add native tokens + total_native_tokens.add_native_tokens(output.native_tokens().clone())?; + + let account_id = output.account_id_non_null(output_id); + balance.accounts.push(account_id); + } + Output::Foundry(output) => { + // Add amount + balance.base_coin.total += output.amount(); + // Add storage deposit + balance.required_storage_deposit.foundry += rent; + if !wallet_data.locked_outputs.contains(output_id) { + total_rent_amount += rent; + } + // Add native tokens + total_native_tokens.add_native_tokens(output.native_tokens().clone())?; + + balance.foundries.push(output.id()); + } + _ => { + // If there is only an [AddressUnlockCondition], then we can spend the output at any time + // without restrictions + if let [UnlockCondition::Address(_)] = output + .unlock_conditions() + .expect("output needs to have unlock conditions") + .as_ref() + { + // add nft_id for nft outputs + if let Output::Nft(output) = &output { + let nft_id = output.nft_id_non_null(output_id); + balance.nfts.push(nft_id); + } + + // Add amount + balance.base_coin.total += output.amount(); + + // Add storage deposit + if output.is_basic() { + balance.required_storage_deposit.basic += rent; + if output + .native_tokens() + .map(|native_tokens| !native_tokens.is_empty()) + .unwrap_or(false) + && !wallet_data.locked_outputs.contains(output_id) + { + total_rent_amount += rent; + } + } else if output.is_nft() { + balance.required_storage_deposit.nft += rent; + if !wallet_data.locked_outputs.contains(output_id) { + total_rent_amount += rent; + } + } + + // Add native tokens + if let Some(native_tokens) = output.native_tokens() { + total_native_tokens.add_native_tokens(native_tokens.clone())?; + } + } else { + // if we have multiple unlock conditions for basic or nft outputs, then we can't + // spend the balance at the moment or in the future + + let wallet_address = self.address().await.into_inner(); + let slot_index = self.client().get_slot_index().await?; + let is_claimable = self.claimable_outputs(OutputsToClaim::All).await?.contains(output_id); + + // For outputs that are expired or have a timelock unlock condition, but no expiration + // unlock condition and we then can unlock them, then + // they can never be not available for us anymore + // and should be added to the balance + if is_claimable { + // check if output can be unlocked always from now on, in that case it should be + // added to the total amount + let output_can_be_unlocked_now_and_in_future = can_output_be_unlocked_forever_from_now_on( + // We use the addresses with unspent outputs, because other addresses of + // the account without unspent + // outputs can't be related to this output + wallet_data.address.inner(), + output, + slot_index, + ); + + if output_can_be_unlocked_now_and_in_future { + // If output has a StorageDepositReturnUnlockCondition, the amount of it should + // be subtracted, because this part + // needs to be sent back + let amount = output + .unlock_conditions() + .and_then(|u| u.storage_deposit_return()) + .map_or_else( + || output.amount(), + |sdr| { + if &wallet_address == sdr.return_address() { + // sending to ourself, we get the full amount + output.amount() + } else { + // Sending to someone else + output.amount() - sdr.amount() + } + }, + ); + + // add nft_id for nft outputs + if let Output::Nft(output) = &output { + let nft_id = output.nft_id_non_null(output_id); + balance.nfts.push(nft_id); + } + + // Add amount + balance.base_coin.total += amount; + + // Add storage deposit + if output.is_basic() { + balance.required_storage_deposit.basic += rent; + // Amount for basic outputs isn't added to total_rent_amount if there aren't + // native tokens, since we can + // spend it without burning. + if output + .native_tokens() + .map(|native_tokens| !native_tokens.is_empty()) + .unwrap_or(false) + && !wallet_data.locked_outputs.contains(output_id) + { + total_rent_amount += rent; + } + } else if output.is_nft() { + balance.required_storage_deposit.nft += rent; + if !wallet_data.locked_outputs.contains(output_id) { + total_rent_amount += rent; + } + } + + // Add native tokens + if let Some(native_tokens) = output.native_tokens() { + total_native_tokens.add_native_tokens(native_tokens.clone())?; + } + } else { + // only add outputs that can't be locked now and at any point in the future + balance.potentially_locked_outputs.insert(*output_id, true); + } + } else { + // Don't add expired outputs that can't ever be unlocked by us + if let Some(expiration) = output + .unlock_conditions() + .expect("output needs to have unlock conditions") + .expiration() + { + // Not expired, could get unlockable when it's expired, so we insert it + if slot_index < expiration.slot_index() { + balance.potentially_locked_outputs.insert(*output_id, false); + } + } else { + balance.potentially_locked_outputs.insert(*output_id, false); + } + } + } + } + } + // } + } + // } + + self.finish(balance, wallet_data, network_id, total_rent_amount, total_native_tokens) + } + + fn finish( + &self, + mut balance: Balance, + wallet_data: &WalletData, + network_id: u64, + total_rent_amount: u64, + total_native_tokens: NativeTokensBuilder, + ) -> Result { + // for `available` get locked_outputs, sum outputs amount and subtract from total_amount + log::debug!("[BALANCE] locked outputs: {:#?}", wallet_data.locked_outputs); + + let mut locked_amount = 0; + let mut locked_native_tokens = NativeTokensBuilder::default(); + + for locked_output in &wallet_data.locked_outputs { + // Skip potentially_locked_outputs, as their amounts aren't added to the balance + if balance.potentially_locked_outputs.contains_key(locked_output) { + continue; + } + if let Some(output_data) = wallet_data.unspent_outputs.get(locked_output) { + // Only check outputs that are in this network + if output_data.network_id == network_id { + locked_amount += output_data.output.amount(); + if let Some(native_tokens) = output_data.output.native_tokens() { + locked_native_tokens.add_native_tokens(native_tokens.clone())?; + } + } + } + } + + log::debug!( + "[BALANCE] total_amount: {}, locked_amount: {}, total_rent_amount: {}", + balance.base_coin.total, + locked_amount, + total_rent_amount, + ); + + locked_amount += total_rent_amount; + + for native_token in total_native_tokens.finish_set()? { + // Check if some amount is currently locked + let locked_native_token_amount = locked_native_tokens.iter().find_map(|(id, amount)| { + if id == native_token.token_id() { + Some(amount) + } else { + None + } + }); + + let metadata = wallet_data + .native_token_foundries + .get(&FoundryId::from(*native_token.token_id())) + .and_then(|foundry| foundry.immutable_features().metadata()) + .cloned(); + + balance.native_tokens.push(NativeTokensBalance { + token_id: *native_token.token_id(), + total: native_token.amount(), + available: native_token.amount() - *locked_native_token_amount.unwrap_or(&U256::from(0u8)), + metadata, + }) + } + + #[cfg(not(feature = "participation"))] + { + balance.base_coin.available = balance.base_coin.total.saturating_sub(locked_amount); + } + #[cfg(feature = "participation")] + { + balance.base_coin.available = balance + .base_coin + .total + .saturating_sub(locked_amount) + .saturating_sub(balance.base_coin.voting_power); + } + + Ok(balance) + } +} diff --git a/sdk/src/wallet/account/operations/helpers/mod.rs b/sdk/src/wallet/operations/helpers/mod.rs similarity index 100% rename from sdk/src/wallet/account/operations/helpers/mod.rs rename to sdk/src/wallet/operations/helpers/mod.rs diff --git a/sdk/src/wallet/account/operations/helpers/time.rs b/sdk/src/wallet/operations/helpers/time.rs similarity index 60% rename from sdk/src/wallet/account/operations/helpers/time.rs rename to sdk/src/wallet/operations/helpers/time.rs index 7bf893f45a..3321adfa83 100644 --- a/sdk/src/wallet/account/operations/helpers/time.rs +++ b/sdk/src/wallet/operations/helpers/time.rs @@ -3,16 +3,12 @@ use crate::{ types::block::{address::Address, output::Output, slot::SlotIndex}, - wallet::account::types::{AddressWithUnspentOutputs, OutputData}, + wallet::types::{AddressWithUnspentOutputs, OutputData}, }; -// Check if an output can be unlocked by one of the account addresses at the current time +// Check if an output can be unlocked by the wallet address at the current time pub(crate) fn can_output_be_unlocked_now( - // We use the addresses with unspent outputs, because other addresses of the account without unspent outputs can't - // be related to this output - // TODO disambiguate these two parameters when we are done with Account changes https://github.com/iotaledger/iota-sdk/issues/647 - account_addresses: &[AddressWithUnspentOutputs], - account_and_nft_addresses: &[Address], + wallet_address: &Address, output_data: &OutputData, slot_index: SlotIndex, ) -> crate::wallet::Result { @@ -26,18 +22,13 @@ pub(crate) fn can_output_be_unlocked_now( .output .required_and_unlocked_address(slot_index, &output_data.output_id)?; - Ok(account_addresses - .iter() - .any(|a| a.address.inner == required_unlock_address) - || account_and_nft_addresses.iter().any(|a| *a == required_unlock_address)) + Ok(wallet_address == &required_unlock_address) } // Check if an output can be unlocked by one of the account addresses at the current time and at any // point in the future pub(crate) fn can_output_be_unlocked_forever_from_now_on( - // We use the addresses with unspent outputs, because other addresses of the account without unspent outputs can't - // be related to this output - account_addresses: &[AddressWithUnspentOutputs], + wallet_address: &Address, output: &Output, slot_index: SlotIndex, ) -> bool { @@ -50,7 +41,7 @@ pub(crate) fn can_output_be_unlocked_forever_from_now_on( // the return address belongs to the account if let Some(expiration) = unlock_conditions.expiration() { if let Some(return_address) = expiration.return_address_expired(slot_index) { - if !account_addresses.iter().any(|a| a.address.inner == *return_address) { + if wallet_address != return_address { return false; }; } else { diff --git a/sdk/src/wallet/account/operations/mod.rs b/sdk/src/wallet/operations/mod.rs similarity index 73% rename from sdk/src/wallet/account/operations/mod.rs rename to sdk/src/wallet/operations/mod.rs index 7df08edf2e..73736f248d 100644 --- a/sdk/src/wallet/account/operations/mod.rs +++ b/sdk/src/wallet/operations/mod.rs @@ -1,9 +1,7 @@ // Copyright 2021 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -/// The module for the address generation -pub(crate) mod address_generation; -/// The module to get the accounts balance +/// The module to get the wallet's balance pub(crate) mod balance; /// Helper functions pub(crate) mod helpers; @@ -13,14 +11,12 @@ pub(crate) mod helpers; pub(crate) mod output_claiming; /// The module for the output consolidation pub(crate) mod output_consolidation; -/// The module to find additional addresses with unspent outputs -pub(crate) mod output_finder; /// The module for participation #[cfg(feature = "participation")] pub(crate) mod participation; /// The module for reissuing blocks or transactions pub(crate) mod reissue; -/// The module for synchronization of an account +/// The module for synchronization of the wallet pub(crate) mod syncing; /// The module for transactions pub(crate) mod transaction; diff --git a/sdk/src/wallet/account/operations/output_claiming.rs b/sdk/src/wallet/operations/output_claiming.rs similarity index 92% rename from sdk/src/wallet/account/operations/output_claiming.rs rename to sdk/src/wallet/operations/output_claiming.rs index c3d656b472..c36c37535a 100644 --- a/sdk/src/wallet/account/operations/output_claiming.rs +++ b/sdk/src/wallet/operations/output_claiming.rs @@ -16,9 +16,10 @@ use crate::{ }, slot::SlotIndex, }, - wallet::account::{ - operations::helpers::time::can_output_be_unlocked_now, types::TransactionWithMetadata, Account, OutputData, - TransactionOptions, + wallet::{ + operations::{helpers::time::can_output_be_unlocked_now, transaction::TransactionOptions}, + types::{OutputData, TransactionWithMetadata}, + Wallet, }, }; @@ -33,7 +34,7 @@ pub enum OutputsToClaim { All, } -impl Account +impl Wallet where crate::wallet::Error: From, crate::client::Error: From, @@ -46,19 +47,19 @@ where /// additional inputs pub async fn claimable_outputs(&self, outputs_to_claim: OutputsToClaim) -> crate::wallet::Result> { log::debug!("[OUTPUT_CLAIMING] claimable_outputs"); - let account_details = self.details().await; + let wallet_data = self.data().await; let slot_index = self.client().get_slot_index().await?; // Get outputs for the claim let mut output_ids_to_claim: HashSet = HashSet::new(); - for (output_id, output_data) in account_details + for (output_id, output_data) in wallet_data .unspent_outputs .iter() .filter(|(_, o)| o.output.is_basic() || o.output.is_nft()) { // Don't use outputs that are locked for other transactions - if !account_details.locked_outputs.contains(output_id) && account_details.outputs.contains_key(output_id) { + if !wallet_data.locked_outputs.contains(output_id) && wallet_data.outputs.contains_key(output_id) { if let Some(unlock_conditions) = output_data.output.unlock_conditions() { // If there is a single [UnlockCondition], then it's an // [AddressUnlockCondition] and we own it already without @@ -67,9 +68,7 @@ where && can_output_be_unlocked_now( // We use the addresses with unspent outputs, because other addresses of the // account without unspent outputs can't be related to this output - &account_details.addresses_with_unspent_outputs, - // outputs controlled by an account or nft are currently not considered - &[], + wallet_data.address.inner(), output_data, slot_index, )? @@ -130,11 +129,11 @@ where log::debug!("[OUTPUT_CLAIMING] get_basic_outputs_for_additional_inputs"); #[cfg(feature = "participation")] let voting_output = self.get_voting_output().await?; - let account_details = self.details().await; + let wallet_data = self.data().await; // Get basic outputs only with AddressUnlockCondition and no other unlock condition let mut basic_outputs: Vec = Vec::new(); - for (output_id, output_data) in &account_details.unspent_outputs { + for (output_id, output_data) in &wallet_data.unspent_outputs { #[cfg(feature = "participation")] if let Some(ref voting_output) = voting_output { // Remove voting output from inputs, because we don't want to spent it to claim something else. @@ -143,8 +142,8 @@ where } } // Don't use outputs that are locked for other transactions - if !account_details.locked_outputs.contains(output_id) { - if let Some(output) = account_details.outputs.get(output_id) { + if !wallet_data.locked_outputs.contains(output_id) { + if let Some(output) = wallet_data.outputs.get(output_id) { if let Output::Basic(basic_output) = &output.output { if basic_output.unlock_conditions().len() == 1 { // Store outputs with [`AddressUnlockCondition`] alone, because they could be used as @@ -205,12 +204,12 @@ where let rent_structure = self.client().get_rent_structure().await?; let token_supply = self.client().get_token_supply().await?; - let account_details = self.details().await; + let wallet_data = self.data().await; let mut outputs_to_claim = Vec::new(); for output_id in output_ids_to_claim { - if let Some(output_data) = account_details.unspent_outputs.get(&output_id) { - if !account_details.locked_outputs.contains(&output_id) { + if let Some(output_data) = wallet_data.unspent_outputs.get(&output_id) { + if !wallet_data.locked_outputs.contains(&output_id) { outputs_to_claim.push(output_data.clone()); } } @@ -222,12 +221,8 @@ where )); } - let first_account_address = account_details - .public_addresses - .first() - .ok_or(crate::wallet::Error::FailedToGetRemainder)? - .clone(); - drop(account_details); + let wallet_address = wallet_data.address.clone(); + drop(wallet_data); let mut additional_inputs_used = HashSet::new(); @@ -282,17 +277,13 @@ where // deposit for the remaining amount and possible NTs NftOutputBuilder::from(nft_output) .with_nft_id(nft_output.nft_id_non_null(&output_data.output_id)) - .with_unlock_conditions([AddressUnlockCondition::new( - first_account_address.address.inner.clone(), - )]) + .with_unlock_conditions([AddressUnlockCondition::new(wallet_address.clone())]) .finish_output(token_supply)? } else { NftOutputBuilder::from(nft_output) .with_minimum_storage_deposit(rent_structure) .with_nft_id(nft_output.nft_id_non_null(&output_data.output_id)) - .with_unlock_conditions([AddressUnlockCondition::new( - first_account_address.address.inner.clone(), - )]) + .with_unlock_conditions([AddressUnlockCondition::new(wallet_address.clone())]) // Set native tokens empty, we will collect them from all inputs later .with_native_tokens([]) .finish_output(token_supply)? @@ -384,7 +375,7 @@ where if available_amount - required_amount_for_nfts > 0 { outputs_to_send.push( BasicOutputBuilder::new_with_amount(available_amount - required_amount_for_nfts) - .add_unlock_condition(AddressUnlockCondition::new(first_account_address.address.inner.clone())) + .add_unlock_condition(AddressUnlockCondition::new(wallet_address)) .with_native_tokens(new_native_tokens.finish()?) .finish_output(token_supply)?, ); diff --git a/sdk/src/wallet/account/operations/output_consolidation.rs b/sdk/src/wallet/operations/output_consolidation.rs similarity index 88% rename from sdk/src/wallet/account/operations/output_consolidation.rs rename to sdk/src/wallet/operations/output_consolidation.rs index 6ebf1bdeb9..c38522b6f6 100644 --- a/sdk/src/wallet/account/operations/output_consolidation.rs +++ b/sdk/src/wallet/operations/output_consolidation.rs @@ -8,13 +8,22 @@ use crate::client::secret::{ledger_nano::LedgerSecretManager, DowncastSecretMana use crate::{ client::{api::PreparedTransactionData, secret::SecretManage}, types::block::{ - address::Bech32Address, + address::{Address, Bech32Address}, input::INPUT_COUNT_MAX, output::{ unlock_condition::AddressUnlockCondition, BasicOutputBuilder, NativeTokens, NativeTokensBuilder, Output, }, slot::SlotIndex, }, + wallet::{ + constants::DEFAULT_OUTPUT_CONSOLIDATION_THRESHOLD, + operations::{ + helpers::time::can_output_be_unlocked_now, output_claiming::get_new_native_token_count, + transaction::TransactionOptions, + }, + types::{OutputData, TransactionWithMetadata}, + Result, Wallet, + }, }; // Constants for the calculation of the amount of inputs we can use with a ledger nano @@ -27,16 +36,7 @@ const INPUT_SIZE: usize = 43; const MIN_OUTPUT_SIZE_IN_ESSENCE: usize = 46; #[cfg(feature = "ledger_nano")] -use crate::wallet::account::constants::DEFAULT_LEDGER_OUTPUT_CONSOLIDATION_THRESHOLD; -use crate::wallet::{ - account::{ - constants::DEFAULT_OUTPUT_CONSOLIDATION_THRESHOLD, - operations::{helpers::time::can_output_be_unlocked_now, output_claiming::get_new_native_token_count}, - types::{OutputData, TransactionWithMetadata}, - Account, AddressWithUnspentOutputs, TransactionOptions, - }, - Result, -}; +use crate::wallet::constants::DEFAULT_LEDGER_OUTPUT_CONSOLIDATION_THRESHOLD; #[derive(Clone, Debug, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -70,7 +70,7 @@ impl ConsolidationParams { } } -impl Account +impl Wallet where crate::wallet::Error: From, crate::client::Error: From, @@ -79,7 +79,7 @@ where &self, output_data: &OutputData, slot_index: SlotIndex, - account_addresses: &[AddressWithUnspentOutputs], + wallet_address: &Address, ) -> Result { Ok(if let Output::Basic(basic_output) = &output_data.output { let unlock_conditions = basic_output.unlock_conditions(); @@ -98,7 +98,7 @@ where return Ok(false); } - can_output_be_unlocked_now(account_addresses, &[], output_data, slot_index)? + can_output_be_unlocked_now(wallet_address, output_data, slot_index)? } else { false }) @@ -130,10 +130,11 @@ where let slot_index = self.client().get_slot_index().await?; let token_supply = self.client().get_token_supply().await?; let mut outputs_to_consolidate = Vec::new(); - let account_details = self.details().await; - let account_addresses = &account_details.addresses_with_unspent_outputs[..]; + let wallet_data = self.data().await; - for (output_id, output_data) in account_details.unspent_outputs() { + let wallet_address = &wallet_data.address; + + for (output_id, output_data) in &wallet_data.unspent_outputs { #[cfg(feature = "participation")] if let Some(ref voting_output) = voting_output { // Remove voting output from inputs, because we want to keep its features and not consolidate it. @@ -141,15 +142,15 @@ where continue; } } - let is_locked_output = account_details.locked_outputs.contains(output_id); - let should_consolidate_output = - self.should_consolidate_output(output_data, slot_index, account_addresses)?; + + let is_locked_output = wallet_data.locked_outputs.contains(output_id); + let should_consolidate_output = self.should_consolidate_output(output_data, slot_index, wallet_address)?; if !is_locked_output && should_consolidate_output { outputs_to_consolidate.push(output_data.clone()); } } - drop(account_details); + drop(wallet_data); #[allow(clippy::option_if_let_else)] let output_threshold = match params.output_threshold { @@ -157,8 +158,8 @@ where None => { #[cfg(feature = "ledger_nano")] { - use crate::wallet::account::SecretManager; - let secret_manager = self.wallet.secret_manager.read().await; + use crate::client::secret::SecretManager; + let secret_manager = self.secret_manager.read().await; if secret_manager .downcast::() .or_else(|| { @@ -197,8 +198,8 @@ where #[cfg(feature = "ledger_nano")] let max_inputs = { - use crate::wallet::account::SecretManager; - let secret_manager = self.wallet.secret_manager.read().await; + use crate::client::secret::SecretManager; + let secret_manager = self.secret_manager.read().await; if let Some(ledger) = secret_manager.downcast::().or_else(|| { secret_manager.downcast::().and_then(|s| { if let SecretManager::LedgerNano(n) = s { diff --git a/sdk/src/wallet/account/operations/participation/event.rs b/sdk/src/wallet/operations/participation/event.rs similarity index 83% rename from sdk/src/wallet/account/operations/participation/event.rs rename to sdk/src/wallet/operations/participation/event.rs index c0033cdfc4..841a527c21 100644 --- a/sdk/src/wallet/account/operations/participation/event.rs +++ b/sdk/src/wallet/operations/participation/event.rs @@ -8,15 +8,16 @@ use crate::{ types::api::plugins::participation::types::{ ParticipationEventId, ParticipationEventStatus, ParticipationEventType, }, - wallet::account::{ + wallet::{ operations::participation::ParticipationEventWithNodes, - types::participation::ParticipationEventRegistrationOptions, Account, + types::participation::ParticipationEventRegistrationOptions, Wallet, }, }; -impl Account +impl Wallet where crate::wallet::Error: From, + crate::client::Error: From, { /// Stores participation information for the given events locally and returns them all. /// @@ -60,12 +61,10 @@ where data: event_data, nodes: vec![options.node.clone()], }; - let account_index = self.details().await.index; - self.wallet - .storage_manager + self.storage_manager .read() .await - .insert_participation_event(account_index, event_with_node.clone()) + .insert_participation_event(event_with_node.clone()) .await?; registered_participation_events.insert(event_id, event_with_node.clone()); } @@ -75,13 +74,7 @@ where /// Removes a previously registered participation event from local storage. pub async fn deregister_participation_event(&self, id: &ParticipationEventId) -> crate::wallet::Result<()> { - let account_index = self.details().await.index; - self.wallet - .storage_manager - .read() - .await - .remove_participation_event(account_index, id) - .await?; + self.storage_manager.read().await.remove_participation_event(id).await?; Ok(()) } @@ -90,13 +83,11 @@ where &self, id: ParticipationEventId, ) -> crate::wallet::Result> { - let account_index = self.details().await.index; Ok(self - .wallet .storage_manager .read() .await - .get_participation_events(account_index) + .get_participation_events() .await? .get(&id) .cloned()) @@ -106,13 +97,7 @@ where pub async fn get_participation_events( &self, ) -> crate::wallet::Result> { - let account_index = self.details().await.index; - self.wallet - .storage_manager - .read() - .await - .get_participation_events(account_index) - .await + self.storage_manager.read().await.get_participation_events().await } /// Retrieves IDs of all events tracked by the client options node. diff --git a/sdk/src/wallet/account/operations/participation/mod.rs b/sdk/src/wallet/operations/participation/mod.rs similarity index 91% rename from sdk/src/wallet/account/operations/participation/mod.rs rename to sdk/src/wallet/operations/participation/mod.rs index 0eb247c087..499135b53b 100644 --- a/sdk/src/wallet/account/operations/participation/mod.rs +++ b/sdk/src/wallet/operations/participation/mod.rs @@ -25,15 +25,12 @@ use crate::{ }, block::output::{unlock_condition::UnlockCondition, Output, OutputId}, }, - wallet::{ - account::{Account, OutputData}, - task, Result, - }, + wallet::{task, types::OutputData, Result, Wallet}, }; /// An object containing an account's entire participation overview. #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AccountParticipationOverview { +pub struct ParticipationOverview { /// Output participations for events. pub participations: HashMap>, } @@ -49,25 +46,25 @@ pub struct ParticipationEventWithNodes { pub nodes: Vec, } -impl Account +impl Wallet where crate::wallet::Error: From, + crate::client::Error: From, { - /// Calculates the voting overview of an account. If event_ids are provided, only return outputs and tracked + /// Calculates the voting overview of a wallet. If event_ids are provided, only return outputs and tracked /// participations for them. pub async fn get_participation_overview( &self, event_ids: Option>, - ) -> Result { + ) -> Result { log::debug!("[get_participation_overview]"); // TODO: Could use the address endpoint in the future when https://github.com/iotaledger/inx-participation/issues/50 is done. let mut spent_cached_outputs = self - .wallet .storage_manager .read() .await - .get_cached_participation_output_status(self.details().await.index) + .get_cached_participation_output_status() .await?; let restored_spent_cached_outputs_len = spent_cached_outputs.len(); log::debug!( @@ -216,15 +213,14 @@ where ); // Only store updated data if new outputs got added if spent_cached_outputs.len() > restored_spent_cached_outputs_len { - self.wallet - .storage_manager + self.storage_manager .read() .await - .set_cached_participation_output_status(self.details().await.index, &spent_cached_outputs) + .set_cached_participation_output_status(&spent_cached_outputs) .await?; } - Ok(AccountParticipationOverview { participations }) + Ok(ParticipationOverview { participations }) } /// Returns the voting output ("PARTICIPATION" tag). @@ -245,14 +241,7 @@ where /// If event isn't found, the client from the account will be returned. pub(crate) async fn get_client_for_event(&self, id: &ParticipationEventId) -> crate::wallet::Result { log::debug!("[get_client_for_event]"); - let account_index = self.details().await.index; - let events = self - .wallet - .storage_manager - .read() - .await - .get_participation_events(account_index) - .await?; + let events = self.storage_manager.read().await.get_participation_events().await?; let event_with_nodes = match events.get(id) { Some(event_with_nodes) => event_with_nodes, @@ -277,14 +266,7 @@ where let latest_milestone_index = 0; // let latest_milestone_index = self.client().get_info().await?.node_info.status.latest_milestone.index; - let account_index = self.details().await.index; - let events = self - .wallet - .storage_manager - .read() - .await - .get_participation_events(account_index) - .await?; + let events = self.storage_manager.read().await.get_participation_events().await?; for participation in participations.participations.clone().iter() { if let Some(event_with_nodes) = events.get(&participation.event_id) { diff --git a/sdk/src/wallet/account/operations/participation/voting.rs b/sdk/src/wallet/operations/participation/voting.rs similarity index 98% rename from sdk/src/wallet/account/operations/participation/voting.rs rename to sdk/src/wallet/operations/participation/voting.rs index 59427f0a28..3c16361609 100644 --- a/sdk/src/wallet/account/operations/participation/voting.rs +++ b/sdk/src/wallet/operations/participation/voting.rs @@ -13,13 +13,10 @@ use crate::{ payload::TaggedDataPayload, }, }, - wallet::{ - account::{types::TransactionWithMetadata, Account, TransactionOptions}, - Result, - }, + wallet::{operations::transaction::TransactionOptions, types::TransactionWithMetadata, Result, Wallet}, }; -impl Account +impl Wallet where crate::wallet::Error: From, crate::client::Error: From, diff --git a/sdk/src/wallet/account/operations/participation/voting_power.rs b/sdk/src/wallet/operations/participation/voting_power.rs similarity index 93% rename from sdk/src/wallet/account/operations/participation/voting_power.rs rename to sdk/src/wallet/operations/participation/voting_power.rs index 068582c767..1d57151391 100644 --- a/sdk/src/wallet/account/operations/participation/voting_power.rs +++ b/sdk/src/wallet/operations/participation/voting_power.rs @@ -14,13 +14,10 @@ use crate::{ payload::TaggedDataPayload, }, }, - wallet::{ - account::{types::TransactionWithMetadata, Account, TransactionOptions}, - Error, Result, - }, + wallet::{operations::transaction::TransactionOptions, types::TransactionWithMetadata, Error, Result, Wallet}, }; -impl Account +impl Wallet where crate::wallet::Error: From, crate::client::Error: From, @@ -77,15 +74,7 @@ where } None => ( BasicOutputBuilder::new_with_amount(amount) - .add_unlock_condition(AddressUnlockCondition::new( - self.public_addresses() - .await - .into_iter() - .next() - .expect("account needs to have a public address") - .address - .inner, - )) + .add_unlock_condition(AddressUnlockCondition::new(self.address().await)) .add_feature(TagFeature::new(PARTICIPATION_TAG)?) .finish_output(token_supply)?, None, diff --git a/sdk/src/wallet/account/operations/reissue.rs b/sdk/src/wallet/operations/reissue.rs similarity index 94% rename from sdk/src/wallet/account/operations/reissue.rs rename to sdk/src/wallet/operations/reissue.rs index 2eff243a9f..d5ef412e1e 100644 --- a/sdk/src/wallet/account/operations/reissue.rs +++ b/sdk/src/wallet/operations/reissue.rs @@ -15,16 +15,13 @@ use crate::{ BlockId, }, }, - wallet::{ - account::{types::InclusionState, Account}, - Error, - }, + wallet::{types::InclusionState, Error, Wallet}, }; const DEFAULT_REISSUE_UNTIL_INCLUDED_INTERVAL: u64 = 1; const DEFAULT_REISSUE_UNTIL_INCLUDED_MAX_AMOUNT: u64 = 40; -impl Account +impl Wallet where Error: From, crate::client::Error: From, @@ -40,7 +37,7 @@ where log::debug!("[reissue_transaction_until_included]"); let protocol_parameters = self.client().get_protocol_parameters().await?; - let transaction = self.details().await.transactions.get(transaction_id).cloned(); + let transaction = self.data().await.transactions.get(transaction_id).cloned(); if let Some(transaction) = transaction { if transaction.inclusion_state == InclusionState::Confirmed { @@ -68,7 +65,7 @@ where .await? .sign_ed25519( &*self.get_secret_manager().read().await, - Bip44::new(self.wallet.coin_type()), + self.bip_path().await.ok_or(Error::MissingBipPath)?, ) .await? .id(&protocol_parameters), @@ -116,7 +113,7 @@ where .await? .sign_ed25519( &*self.get_secret_manager().read().await, - Bip44::new(self.wallet.coin_type()), + self.bip_path().await.ok_or(Error::MissingBipPath)?, ) .await?; block_ids.push(reissued_block.id(&protocol_parameters)); diff --git a/sdk/src/wallet/operations/syncing/addresses/mod.rs b/sdk/src/wallet/operations/syncing/addresses/mod.rs new file mode 100644 index 0000000000..2952ebb296 --- /dev/null +++ b/sdk/src/wallet/operations/syncing/addresses/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2021 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod output_ids; +mod outputs; diff --git a/sdk/src/wallet/account/operations/syncing/addresses/output_ids/account_foundry.rs b/sdk/src/wallet/operations/syncing/addresses/output_ids/account_foundry.rs similarity index 94% rename from sdk/src/wallet/account/operations/syncing/addresses/output_ids/account_foundry.rs rename to sdk/src/wallet/operations/syncing/addresses/output_ids/account_foundry.rs index d71ae8bad6..76246b9b14 100644 --- a/sdk/src/wallet/account/operations/syncing/addresses/output_ids/account_foundry.rs +++ b/sdk/src/wallet/operations/syncing/addresses/output_ids/account_foundry.rs @@ -16,15 +16,13 @@ use crate::{ ConvertTo, }, }, - wallet::{ - account::{Account, SyncOptions}, - task, - }, + wallet::{operations::syncing::SyncOptions, task, Wallet}, }; -impl Account +impl Wallet where crate::wallet::Error: From, + crate::client::Error: From, { /// Returns output ids of account outputs pub(crate) async fn get_account_and_foundry_output_ids( @@ -42,7 +40,7 @@ where .items; // Get all results - if sync_options.alias.foundry_outputs { + if sync_options.account.foundry_outputs { let foundry_output_ids = self.get_foundry_output_ids(&output_ids).await?; output_ids.extend(foundry_output_ids); } diff --git a/sdk/src/wallet/account/operations/syncing/addresses/output_ids/basic.rs b/sdk/src/wallet/operations/syncing/addresses/output_ids/basic.rs similarity index 95% rename from sdk/src/wallet/account/operations/syncing/addresses/output_ids/basic.rs rename to sdk/src/wallet/operations/syncing/addresses/output_ids/basic.rs index 54178ae70d..d709c49a22 100644 --- a/sdk/src/wallet/account/operations/syncing/addresses/output_ids/basic.rs +++ b/sdk/src/wallet/operations/syncing/addresses/output_ids/basic.rs @@ -4,10 +4,10 @@ use crate::{ client::{node_api::indexer::query_parameters::BasicOutputQueryParameters, secret::SecretManage}, types::block::{address::Bech32Address, output::OutputId, ConvertTo}, - wallet::Account, + wallet::Wallet, }; -impl Account +impl Wallet where crate::wallet::Error: From, { diff --git a/sdk/src/wallet/account/operations/syncing/addresses/output_ids/mod.rs b/sdk/src/wallet/operations/syncing/addresses/output_ids/mod.rs similarity index 79% rename from sdk/src/wallet/account/operations/syncing/addresses/output_ids/mod.rs rename to sdk/src/wallet/operations/syncing/addresses/output_ids/mod.rs index 0457a70e1f..c48e876b16 100644 --- a/sdk/src/wallet/account/operations/syncing/addresses/output_ids/mod.rs +++ b/sdk/src/wallet/operations/syncing/addresses/output_ids/mod.rs @@ -16,45 +16,41 @@ use crate::{ node_api::indexer::query_parameters::{FoundryOutputQueryParameters, OutputQueryParameters}, secret::SecretManage, }, - types::block::{ - address::{Address, Bech32Address}, - output::OutputId, - }, - wallet::account::{ + types::block::{address::Bech32Address, output::OutputId}, + wallet::{ constants::PARALLEL_REQUESTS_AMOUNT, operations::syncing::SyncOptions, - types::address::AddressWithUnspentOutputs, Account, + types::address::AddressWithUnspentOutputs, Wallet, }, }; -impl Account +impl Wallet where crate::wallet::Error: From, + crate::client::Error: From, { /// Returns output ids for outputs that are directly (Ed25519 address in AddressUnlockCondition) or indirectly /// (account/nft address in AddressUnlockCondition and the account/nft output is controlled with the Ed25519 /// address) connected to pub(crate) async fn get_output_ids_for_address( &self, - address: &Address, + address: &Bech32Address, sync_options: &SyncOptions, ) -> crate::wallet::Result> { - let bech32_address = Bech32Address::new(self.client().get_bech32_hrp().await?, address.clone()); - if sync_options.sync_only_most_basic_outputs { let output_ids = self - .get_basic_output_ids_with_address_unlock_condition_only(bech32_address.clone()) + .get_basic_output_ids_with_address_unlock_condition_only(address.clone()) .await?; return Ok(output_ids); } - // If interested in alias, basic, NFT and foundry outputs, get them all at once - if (address.is_ed25519() && sync_options.account.all_outputs()) + // If interested in account, basic, NFT and foundry outputs, get them all at once + if (address.is_ed25519() && sync_options.wallet.all_outputs()) || (address.is_nft() && sync_options.nft.all_outputs()) - || (address.is_account() && sync_options.alias.all_outputs()) + || (address.is_account() && sync_options.account.all_outputs()) { return Ok(self .client() - .output_ids(OutputQueryParameters::new().unlockable_by_address(bech32_address.clone())) + .output_ids(OutputQueryParameters::new().unlockable_by_address(address.clone())) .await? .items); } @@ -65,9 +61,9 @@ where #[cfg(not(target_family = "wasm"))] let mut tasks = Vec::new(); - if (address.is_ed25519() && sync_options.account.basic_outputs) + if (address.is_ed25519() && sync_options.wallet.basic_outputs) || (address.is_nft() && sync_options.nft.basic_outputs) - || (address.is_account() && sync_options.alias.basic_outputs) + || (address.is_account() && sync_options.account.basic_outputs) { // basic outputs #[cfg(target_family = "wasm")] @@ -82,7 +78,7 @@ where { tasks.push( async { - let bech32_address = bech32_address.clone(); + let bech32_address = address.clone(); let account = self.clone(); tokio::spawn(async move { account @@ -96,24 +92,21 @@ where } } - if (address.is_ed25519() && sync_options.account.nft_outputs) + if (address.is_ed25519() && sync_options.wallet.nft_outputs) || (address.is_nft() && sync_options.nft.nft_outputs) - || (address.is_account() && sync_options.alias.nft_outputs) + || (address.is_account() && sync_options.account.nft_outputs) { // nfts #[cfg(target_family = "wasm")] { - results.push( - self.get_nft_output_ids_with_any_unlock_condition(bech32_address.clone()) - .await, - ) + results.push(self.get_nft_output_ids_with_any_unlock_condition(address.clone()).await) } #[cfg(not(target_family = "wasm"))] { tasks.push( async { - let bech32_address = bech32_address.clone(); + let bech32_address = address.clone(); let account = self.clone(); tokio::spawn(async move { account @@ -127,15 +120,15 @@ where } } - if (address.is_ed25519() && sync_options.account.account_outputs) + if (address.is_ed25519() && sync_options.wallet.account_outputs) || (address.is_nft() && sync_options.nft.account_outputs) - || (address.is_account() && sync_options.alias.account_outputs) + || (address.is_account() && sync_options.account.account_outputs) { // accounts and foundries #[cfg(target_family = "wasm")] { results.push( - self.get_account_and_foundry_output_ids(bech32_address.clone(), sync_options) + self.get_account_and_foundry_output_ids(address.clone(), sync_options) .await, ) } @@ -144,7 +137,7 @@ where { tasks.push( async { - let bech32_address = bech32_address.clone(); + let bech32_address = address.clone(); let sync_options = sync_options.clone(); let account = self.clone(); tokio::spawn(async move { @@ -157,13 +150,13 @@ where .boxed(), ); } - } else if address.is_account() && sync_options.alias.foundry_outputs { + } else if address.is_account() && sync_options.account.foundry_outputs { // foundries #[cfg(target_family = "wasm")] { results.push(Ok(self .client() - .foundry_output_ids(FoundryOutputQueryParameters::new().account(bech32_address)) + .foundry_output_ids(FoundryOutputQueryParameters::new().account(address)) .await? .items)) } @@ -172,7 +165,7 @@ where { tasks.push( async { - let bech32_address = bech32_address.clone(); + let bech32_address = address.clone(); let client = self.client().clone(); tokio::spawn(async move { Ok(client @@ -200,12 +193,12 @@ where Ok(output_ids.into_iter().collect()) } - /// Get the current output ids for provided addresses and only returns addresses that have unspent outputs and + /// Get the current output ids and only returns addresses that have unspent outputs and /// return spent outputs separated pub(crate) async fn get_output_ids_for_addresses( &self, - options: &SyncOptions, addresses_with_unspent_outputs: Vec, + options: &SyncOptions, ) -> crate::wallet::Result<(Vec, Vec)> { log::debug!("[SYNC] start get_output_ids_for_addresses"); let address_output_ids_start_time = Instant::now(); @@ -213,6 +206,7 @@ where let mut addresses_with_outputs = Vec::new(); // spent outputs or account/nft/foundries that don't get synced anymore, because of other sync options let mut spent_or_not_anymore_synced_outputs = Vec::new(); + // We split the addresses into chunks so we don't get timeouts if we have thousands for addresses_chunk in &mut addresses_with_unspent_outputs .chunks(PARALLEL_REQUESTS_AMOUNT) @@ -235,12 +229,12 @@ where { let mut tasks = Vec::new(); for address in addresses_chunk { - let account = self.clone(); + let wallet = self.clone(); let sync_options = options.clone(); tasks.push(async move { tokio::spawn(async move { - let output_ids = account - .get_output_ids_for_address(&address.address.inner, &sync_options) + let output_ids = wallet + .get_output_ids_for_address(&address.address, &sync_options) .await?; crate::wallet::Result::Ok((address, output_ids)) }) @@ -255,8 +249,8 @@ where let (mut address, output_ids): (AddressWithUnspentOutputs, Vec) = res?; // only return addresses with outputs if !output_ids.is_empty() { - // outputs we had before, but now not anymore, got spent or are account/nft/foundries that don't get - // synced anymore because of other sync options + // outputs we had before, but now not anymore, got spent or are account/nft/foundries that don't + // get synced anymore because of other sync options for output_id in address.output_ids { if !output_ids.contains(&output_id) { spent_or_not_anymore_synced_outputs.push(output_id); @@ -265,8 +259,8 @@ where address.output_ids = output_ids; addresses_with_outputs.push(address); } else { - // outputs we had before, but now not anymore, got spent or are account/nft/foundries that don't get - // synced anymore because of other sync options + // outputs we had before, but now not anymore, got spent or are account/nft/foundries that don't + // get synced anymore because of other sync options spent_or_not_anymore_synced_outputs.extend(address.output_ids); } } diff --git a/sdk/src/wallet/account/operations/syncing/addresses/output_ids/nft.rs b/sdk/src/wallet/operations/syncing/addresses/output_ids/nft.rs similarity index 93% rename from sdk/src/wallet/account/operations/syncing/addresses/output_ids/nft.rs rename to sdk/src/wallet/operations/syncing/addresses/output_ids/nft.rs index af53f1f4b8..b445d4dfd1 100644 --- a/sdk/src/wallet/account/operations/syncing/addresses/output_ids/nft.rs +++ b/sdk/src/wallet/operations/syncing/addresses/output_ids/nft.rs @@ -4,10 +4,10 @@ use crate::{ client::{node_api::indexer::query_parameters::NftOutputQueryParameters, secret::SecretManage}, types::block::{address::Bech32Address, output::OutputId, ConvertTo}, - wallet::Account, + wallet::Wallet, }; -impl Account +impl Wallet where crate::wallet::Error: From, { diff --git a/sdk/src/wallet/account/operations/syncing/addresses/outputs.rs b/sdk/src/wallet/operations/syncing/addresses/outputs.rs similarity index 83% rename from sdk/src/wallet/account/operations/syncing/addresses/outputs.rs rename to sdk/src/wallet/operations/syncing/addresses/outputs.rs index 615cf2b89e..fadc42acd4 100644 --- a/sdk/src/wallet/account/operations/syncing/addresses/outputs.rs +++ b/sdk/src/wallet/operations/syncing/addresses/outputs.rs @@ -6,16 +6,17 @@ use instant::Instant; use crate::{ client::secret::SecretManage, wallet::{ - account::{ - constants::PARALLEL_REQUESTS_AMOUNT, types::address::AddressWithUnspentOutputs, Account, OutputData, - }, + constants::PARALLEL_REQUESTS_AMOUNT, task, + types::{address::AddressWithUnspentOutputs, OutputData}, + Wallet, }, }; -impl Account +impl Wallet where crate::wallet::Error: From, + crate::client::Error: From, { /// Get outputs from addresses pub(crate) async fn get_outputs_from_address_output_ids( @@ -35,12 +36,12 @@ where { let mut tasks = Vec::new(); for address in addresses_chunk { - let account = self.clone(); + let wallet = self.clone(); tasks.push(async move { task::spawn(async move { - let output_responses = account.get_outputs(address.output_ids.clone()).await?; + let output_responses = wallet.get_outputs(address.output_ids.clone()).await?; - let outputs = account + let outputs = wallet .output_response_to_output_data(output_responses, &address) .await?; crate::wallet::Result::Ok((address, outputs)) diff --git a/sdk/src/wallet/account/operations/syncing/foundries.rs b/sdk/src/wallet/operations/syncing/foundries.rs similarity index 83% rename from sdk/src/wallet/account/operations/syncing/foundries.rs rename to sdk/src/wallet/operations/syncing/foundries.rs index 96c5998d37..166c4fd64f 100644 --- a/sdk/src/wallet/account/operations/syncing/foundries.rs +++ b/sdk/src/wallet/operations/syncing/foundries.rs @@ -6,12 +6,13 @@ use std::collections::HashSet; use crate::{ client::secret::SecretManage, types::block::output::{FoundryId, Output}, - wallet::{task, Account}, + wallet::{task, Wallet}, }; -impl Account +impl Wallet where crate::wallet::Error: From, + crate::client::Error: From, { pub(crate) async fn request_and_store_foundry_outputs( &self, @@ -19,7 +20,7 @@ where ) -> crate::wallet::Result<()> { log::debug!("[SYNC] request_and_store_foundry_outputs"); - let mut foundries = self.details().await.native_token_foundries().clone(); + let mut foundries = self.data().await.native_token_foundries.clone(); let results = futures::future::try_join_all(foundry_ids.into_iter().filter(|f| !foundries.contains_key(f)).map( |foundry_id| { @@ -45,8 +46,8 @@ where } } - let mut account_details = self.details_mut().await; - account_details.native_token_foundries = foundries; + let mut wallet_data = self.data_mut().await; + wallet_data.native_token_foundries = foundries; Ok(()) } diff --git a/sdk/src/wallet/account/operations/syncing/mod.rs b/sdk/src/wallet/operations/syncing/mod.rs similarity index 75% rename from sdk/src/wallet/account/operations/syncing/mod.rs rename to sdk/src/wallet/operations/syncing/mod.rs index 92e31b358e..8757ff084e 100644 --- a/sdk/src/wallet/account/operations/syncing/mod.rs +++ b/sdk/src/wallet/operations/syncing/mod.rs @@ -13,17 +13,17 @@ pub use self::options::SyncOptions; use crate::{ client::secret::SecretManage, types::block::{ - address::{AccountAddress, Address, NftAddress, ToBech32Ext}, + address::{AccountAddress, Address, Bech32Address, NftAddress, ToBech32Ext}, output::{FoundryId, Output, OutputId, OutputMetadata}, }, - wallet::account::{ + wallet::{ constants::MIN_SYNC_INTERVAL, - types::{AddressWithUnspentOutputs, OutputData}, - Account, Balance, + types::{AddressWithUnspentOutputs, Balance, OutputData}, + Wallet, }, }; -impl Account +impl Wallet where crate::wallet::Error: From, crate::client::Error: From, @@ -33,9 +33,8 @@ where pub async fn set_default_sync_options(&self, options: SyncOptions) -> crate::wallet::Result<()> { #[cfg(feature = "storage")] { - let index = *self.details().await.index(); - let storage_manager = self.wallet.storage_manager.read().await; - storage_manager.set_default_sync_options(index, &options).await?; + let storage_manager = self.storage_manager.read().await; + storage_manager.set_default_sync_options(&options).await?; } *self.default_sync_options.lock().await = options; @@ -47,7 +46,7 @@ where self.default_sync_options.lock().await.clone() } - /// Sync the account by fetching new information from the nodes. Will also reissue pending transactions + /// Sync the wallet by fetching new information from the nodes. Will also reissue pending transactions /// if necessary. A custom default can be set using set_default_sync_options. pub async fn sync(&self, options: Option) -> crate::wallet::Result { let options = match options { @@ -67,8 +66,8 @@ where "[SYNC] synced within the latest {} ms, only calculating balance", MIN_SYNC_INTERVAL ); - // Calculate the balance because if we created a transaction in the meantime, the amount for the inputs is - // not available anymore + // Calculate the balance because if we created a transaction in the meantime, the amount for the inputs + // is not available anymore return self.balance().await; } @@ -96,14 +95,25 @@ where async fn sync_internal(&self, options: &SyncOptions) -> crate::wallet::Result<()> { log::debug!("[SYNC] sync_internal"); - let addresses_to_sync = self.get_addresses_to_sync(options).await?; - log::debug!("[SYNC] addresses_to_sync {}", addresses_to_sync.len()); + let wallet_address_with_unspent_outputs = AddressWithUnspentOutputs { + address: self.address().await, + output_ids: self + .unspent_outputs(None) + .await? + .into_iter() + .map(|data| data.output_id) + .collect::>(), + internal: false, + key_index: 0, + }; - let (spent_or_not_synced_output_ids, addresses_with_unspent_outputs, outputs_data): ( - Vec, + let address_to_sync = vec![wallet_address_with_unspent_outputs]; + + let (addresses_with_unspent_outputs, spent_or_not_synced_output_ids, outputs_data): ( Vec, + Vec, Vec, - ) = self.request_outputs_recursively(addresses_to_sync, options).await?; + ) = self.request_outputs_recursively(address_to_sync, options).await?; // Request possible spent outputs log::debug!("[SYNC] spent_or_not_synced_outputs: {spent_or_not_synced_output_ids:?}"); @@ -145,65 +155,68 @@ where self.request_and_store_foundry_outputs(native_token_foundry_ids).await?; } - // Updates account with balances, output ids, outputs - self.update_account( - addresses_with_unspent_outputs, - outputs_data, - spent_or_unsynced_output_metadata_map, - options, - ) - .await + // Updates wallet with balances, output ids, outputs + self.update_after_sync(outputs_data, spent_or_unsynced_output_metadata_map) + .await } - // First request all outputs directly related to the ed25519 addresses, then for each nft and account output we got, + // First request all outputs directly related to the wallet address, then for each nft and account output we got, // request all outputs that are related to their account/nft addresses in a loop until no new account or nft outputs - // is found + // are found. async fn request_outputs_recursively( &self, addresses_to_sync: Vec, options: &SyncOptions, - ) -> crate::wallet::Result<(Vec, Vec, Vec)> { + ) -> crate::wallet::Result<(Vec, Vec, Vec)> { // Cache the account and nft address with the related ed2559 address, so we can update the account address with // the new output ids - let mut new_account_and_nft_addresses = HashMap::new(); - let (mut spent_or_not_synced_output_ids, mut addresses_with_unspent_outputs, mut outputs_data) = - (Vec::new(), Vec::new(), Vec::new()); + + let mut new_account_and_nft_addresses: HashMap = HashMap::new(); + let mut spent_or_not_synced_output_ids = Vec::new(); + let mut addresses_with_unspent_outputs = Vec::new(); + let mut outputs_data = Vec::new(); + + let bech32_hrp = self.client().get_bech32_hrp().await?; loop { let new_outputs_data = if new_account_and_nft_addresses.is_empty() { - // Get outputs for addresses and add them also the the addresses_with_unspent_outputs - let (addresses_with_output_ids, spent_or_not_synced_output_ids_inner) = self - .get_output_ids_for_addresses(options, addresses_to_sync.clone()) + // Get outputs for the addresses and add them also the the addresses_with_unspent_outputs + let (unspent_output_ids, spent_or_not_synced_output_ids_inner) = self + .get_output_ids_for_addresses(addresses_to_sync.clone(), options) .await?; + spent_or_not_synced_output_ids = spent_or_not_synced_output_ids_inner; + // Get outputs for addresses and add them also the the addresses_with_unspent_outputs - let (addresses_with_unspent_outputs_inner, outputs_data_inner) = self - .get_outputs_from_address_output_ids(addresses_with_output_ids) - .await?; + let (addresses_with_unspent_outputs_inner, outputs_data_inner) = + self.get_outputs_from_address_output_ids(unspent_output_ids).await?; + addresses_with_unspent_outputs = addresses_with_unspent_outputs_inner; outputs_data.extend(outputs_data_inner.clone()); outputs_data_inner } else { - let bech32_hrp = self.client().get_bech32_hrp().await?; let mut new_outputs_data = Vec::new(); - for (account_or_nft_address, ed25519_address) in new_account_and_nft_addresses { + for (account_or_nft_address, output_address) in &new_account_and_nft_addresses { let output_ids = self - .get_output_ids_for_address(&account_or_nft_address, options) + .get_output_ids_for_address( + &Bech32Address::new(bech32_hrp, account_or_nft_address.clone()), + options, + ) .await?; // Update address with unspent outputs let address_with_unspent_outputs = addresses_with_unspent_outputs .iter_mut() - .find(|a| a.address.inner == ed25519_address) + .find(|address| address.address.inner() == output_address) .ok_or_else(|| { - crate::wallet::Error::AddressNotFoundInAccount(ed25519_address.to_bech32(bech32_hrp)) + crate::wallet::Error::WalletAddressMismatch(output_address.clone().to_bech32(bech32_hrp)) })?; address_with_unspent_outputs.output_ids.extend(output_ids.clone()); let new_outputs_data_inner = self.get_outputs(output_ids).await?; let outputs_data_inner = self - .output_response_to_output_data(new_outputs_data_inner, address_with_unspent_outputs) + .output_response_to_output_data(new_outputs_data_inner, &address_with_unspent_outputs) .await?; outputs_data.extend(outputs_data_inner.clone()); @@ -213,7 +226,8 @@ where }; // Clear, so we only get new addresses - new_account_and_nft_addresses = HashMap::new(); + new_account_and_nft_addresses.clear(); + // Add new account and nft addresses for output_data in new_outputs_data { match output_data.output { @@ -242,12 +256,13 @@ where // synced afterwards, so we filter these unspent outputs here. Maybe the spent_or_not_synced_output_ids can be // calculated more efficient in the future, by comparing the new and old outputs only at this point. Then this // retain isn't needed anymore. + let unspent_output_ids: HashSet = HashSet::from_iter(outputs_data.iter().map(|o| o.output_id)); spent_or_not_synced_output_ids.retain(|o| !unspent_output_ids.contains(o)); Ok(( - spent_or_not_synced_output_ids, addresses_with_unspent_outputs, + spent_or_not_synced_output_ids, outputs_data, )) } diff --git a/sdk/src/wallet/account/operations/syncing/options.rs b/sdk/src/wallet/operations/syncing/options.rs similarity index 94% rename from sdk/src/wallet/account/operations/syncing/options.rs rename to sdk/src/wallet/operations/syncing/options.rs index ae52169ee2..658685f23c 100644 --- a/sdk/src/wallet/account/operations/syncing/options.rs +++ b/sdk/src/wallet/operations/syncing/options.rs @@ -40,13 +40,12 @@ pub struct SyncOptions { /// Checks pending transactions and reissues them if necessary. #[serde(default = "default_sync_pending_transactions")] pub sync_pending_transactions: bool, - /// Specifies what outputs should be synced for the ed25519 addresses from the account. + /// Specifies what outputs should be synced for the ed25519 addresses from the wallet. #[serde(default)] - pub account: AccountSyncOptions, + pub wallet: WalletSyncOptions, /// Specifies what outputs should be synced for the address of an account output. #[serde(default)] - // TODO Rename when we are done with Account changes https://github.com/iotaledger/iota-sdk/issues/647. - pub alias: AliasSyncOptions, + pub account: AccountSyncOptions, /// Specifies what outputs should be synced for the address of an nft output. #[serde(default)] pub nft: NftSyncOptions, @@ -91,8 +90,8 @@ impl Default for SyncOptions { address_start_index_internal: default_address_start_index(), sync_incoming_transactions: default_sync_incoming_transactions(), sync_pending_transactions: default_sync_pending_transactions(), + wallet: WalletSyncOptions::default(), account: AccountSyncOptions::default(), - alias: AliasSyncOptions::default(), nft: NftSyncOptions::default(), sync_only_most_basic_outputs: default_sync_only_most_basic_outputs(), sync_native_token_foundries: default_sync_native_token_foundries(), @@ -101,16 +100,16 @@ impl Default for SyncOptions { } } -/// Sync options for Ed25519 addresses from the account +/// Sync options for Ed25519 addresses from the wallet #[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] #[serde(default, rename_all = "camelCase")] -pub struct AccountSyncOptions { +pub struct WalletSyncOptions { pub basic_outputs: bool, pub nft_outputs: bool, pub account_outputs: bool, } -impl Default for AccountSyncOptions { +impl Default for WalletSyncOptions { fn default() -> Self { Self { basic_outputs: true, @@ -120,7 +119,7 @@ impl Default for AccountSyncOptions { } } -impl AccountSyncOptions { +impl WalletSyncOptions { pub(crate) fn all_outputs(&self) -> bool { self.basic_outputs && self.nft_outputs && self.account_outputs } @@ -129,14 +128,14 @@ impl AccountSyncOptions { /// Sync options for addresses from account outputs #[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] #[serde(default, rename_all = "camelCase")] -pub struct AliasSyncOptions { +pub struct AccountSyncOptions { pub basic_outputs: bool, pub nft_outputs: bool, pub account_outputs: bool, pub foundry_outputs: bool, } -impl Default for AliasSyncOptions { +impl Default for AccountSyncOptions { // Sync only foundries fn default() -> Self { Self { @@ -148,7 +147,7 @@ impl Default for AliasSyncOptions { } } -impl AliasSyncOptions { +impl AccountSyncOptions { pub(crate) fn all_outputs(&self) -> bool { self.basic_outputs && self.nft_outputs && self.account_outputs && self.foundry_outputs } diff --git a/sdk/src/wallet/account/operations/syncing/outputs.rs b/sdk/src/wallet/operations/syncing/outputs.rs similarity index 83% rename from sdk/src/wallet/account/operations/syncing/outputs.rs rename to sdk/src/wallet/operations/syncing/outputs.rs index a0de9f0462..ce4e3c3928 100644 --- a/sdk/src/wallet/account/operations/syncing/outputs.rs +++ b/sdk/src/wallet/operations/syncing/outputs.rs @@ -16,14 +16,16 @@ use crate::{ }, }, wallet::{ - account::{build_transaction_from_payload_and_inputs, types::OutputData, Account, AddressWithUnspentOutputs}, - task, + build_transaction_from_payload_and_inputs, task, + types::{AddressWithUnspentOutputs, OutputData}, + Wallet, }, }; -impl Account +impl Wallet where crate::wallet::Error: From, + crate::client::Error: From, { /// Convert OutputWithMetadataResponse to OutputData with the network_id added pub(crate) async fn output_response_to_output_data( @@ -34,23 +36,27 @@ where log::debug!("[SYNC] convert output_responses"); // store outputs with network_id let network_id = self.client().get_network_id().await?; - let account_details = self.details().await; + let wallet_data = self.data().await; Ok(outputs_with_meta .into_iter() .map(|output_with_meta| { // check if we know the transaction that created this output and if we created it (if we store incoming // transactions separated, then this check wouldn't be required) - let remainder = account_details + let remainder = wallet_data .transactions .get(output_with_meta.metadata().transaction_id()) .map_or(false, |tx| !tx.incoming); - // BIP 44 (HD wallets) and 4218 is the registered index for IOTA https://github.com/satoshilabs/slips/blob/master/slip-0044.md - let chain = Bip44::new(account_details.coin_type) - .with_account(account_details.index) - .with_change(associated_address.internal as _) - .with_address_index(associated_address.key_index); + let chain = wallet_data.bip_path.map_or(None, |bip_path| { + // BIP 44 (HD wallets) and 4218 is the registered index for IOTA https://github.com/satoshilabs/slips/blob/master/slip-0044.md + Some( + Bip44::new(bip_path.coin_type) + .with_account(bip_path.account) + .with_change(associated_address.internal as _) + .with_address_index(associated_address.key_index), + ) + }); OutputData { output_id: output_with_meta.metadata().output_id().to_owned(), @@ -60,7 +66,7 @@ where address: associated_address.address.inner.clone(), network_id, remainder, - chain: Some(chain), + chain, } }) .collect()) @@ -77,10 +83,10 @@ where let mut outputs = Vec::new(); let mut unknown_outputs = Vec::new(); let mut unspent_outputs = Vec::new(); - let mut account_details = self.details_mut().await; + let mut wallet_data = self.data_mut().await; for output_id in output_ids { - match account_details.outputs.get_mut(&output_id) { + match wallet_data.outputs.get_mut(&output_id) { // set unspent Some(output_data) => { output_data.is_spent = false; @@ -96,10 +102,10 @@ where // known output is unspent, so insert it to the unspent outputs again, because if it was an // account/nft/foundry output it could have been removed when syncing without them for (output_id, output_data) in unspent_outputs { - account_details.unspent_outputs.insert(output_id, output_data); + wallet_data.unspent_outputs.insert(output_id, output_data); } - drop(account_details); + drop(wallet_data); if !unknown_outputs.is_empty() { outputs.extend(self.client().get_outputs_with_metadata(&unknown_outputs).await?); @@ -122,15 +128,13 @@ where ) -> crate::wallet::Result<()> { log::debug!("[SYNC] request_incoming_transaction_data"); - let account_details = self.details().await; + let wallet_data = self.data().await; transaction_ids.retain(|transaction_id| { - !(account_details.transactions.contains_key(transaction_id) - || account_details.incoming_transactions.contains_key(transaction_id) - || account_details - .inaccessible_incoming_transactions - .contains(transaction_id)) + !(wallet_data.transactions.contains_key(transaction_id) + || wallet_data.incoming_transactions.contains_key(transaction_id) + || wallet_data.inaccessible_incoming_transactions.contains(transaction_id)) }); - drop(account_details); + drop(wallet_data); // Limit parallel requests to 100, to avoid timeouts let results = @@ -184,19 +188,15 @@ where .await?; // Update account with new transactions - let mut account_details = self.details_mut().await; + let mut wallet_data = self.data_mut().await; for (transaction_id, txn) in results.into_iter().flatten() { if let Some(transaction) = txn { - account_details - .incoming_transactions - .insert(transaction_id, transaction); + wallet_data.incoming_transactions.insert(transaction_id, transaction); } else { log::debug!("[SYNC] adding {transaction_id} to inaccessible_incoming_transactions"); // Save transactions that weren't found by the node to avoid requesting them endlessly. // Will be cleared when new client options are provided. - account_details - .inaccessible_incoming_transactions - .insert(transaction_id); + wallet_data.inaccessible_incoming_transactions.insert(transaction_id); } } diff --git a/sdk/src/wallet/account/operations/syncing/transactions.rs b/sdk/src/wallet/operations/syncing/transactions.rs similarity index 92% rename from sdk/src/wallet/account/operations/syncing/transactions.rs rename to sdk/src/wallet/operations/syncing/transactions.rs index ce855c8ed7..15febb3bfb 100644 --- a/sdk/src/wallet/account/operations/syncing/transactions.rs +++ b/sdk/src/wallet/operations/syncing/transactions.rs @@ -7,9 +7,10 @@ use crate::{ api::core::TransactionState, block::{input::Input, output::OutputId, BlockId}, }, - wallet::account::{ + wallet::{ + core::WalletData, types::{InclusionState, TransactionWithMetadata}, - Account, AccountDetails, + Wallet, }, }; @@ -18,7 +19,7 @@ use crate::{ // also revalidate that the locked outputs needs to be there, maybe there was a conflict or the transaction got // confirmed, then they should get removed -impl Account +impl Wallet where crate::wallet::Error: From, crate::client::Error: From, @@ -29,13 +30,13 @@ where /// be synced again pub(crate) async fn sync_pending_transactions(&self) -> crate::wallet::Result { log::debug!("[SYNC] sync pending transactions"); - let account_details = self.details().await; + let wallet_data = self.data().await; // only set to true if a transaction got confirmed for which we don't have an output // (transaction_output.is_none()) let mut confirmed_unknown_output = false; - if account_details.pending_transactions.is_empty() { + if wallet_data.pending_transactions.is_empty() { return Ok(confirmed_unknown_output); } @@ -48,9 +49,9 @@ where let mut output_ids_to_unlock = Vec::new(); let mut transactions_to_reissue = Vec::new(); - for transaction_id in &account_details.pending_transactions { + for transaction_id in &wallet_data.pending_transactions { log::debug!("[SYNC] sync pending transaction {transaction_id}"); - let transaction = account_details + let transaction = wallet_data .transactions .get(transaction_id) // panic during development to easier detect if something is wrong, should be handled different later @@ -64,14 +65,14 @@ where // check if we have an output (remainder, if not sending to an own address) that got created by this // transaction, if that's the case, then the transaction got confirmed - let transaction_output = account_details + let transaction_output = wallet_data .outputs .keys() .find(|o| o.transaction_id() == transaction_id); if let Some(transaction_output) = transaction_output { // Save to unwrap, we just got the output - let confirmed_output_data = account_details.outputs.get(transaction_output).expect("output exists"); + let confirmed_output_data = wallet_data.outputs.get(transaction_output).expect("output exists"); log::debug!( "[SYNC] confirmed transaction {transaction_id} in block {}", confirmed_output_data.metadata.block_id() @@ -90,7 +91,7 @@ where let mut input_got_spent = false; for input in transaction.payload.transaction().inputs() { let Input::Utxo(input) = input; - if let Some(input) = account_details.outputs.get(input.output_id()) { + if let Some(input) = wallet_data.outputs.get(input.output_id()) { if input.is_spent { input_got_spent = true; } @@ -152,7 +153,7 @@ where // no need to reissue if one input got spent if input_got_spent { process_transaction_with_unknown_state( - &account_details, + &wallet_data, transaction, &mut updated_transactions, &mut output_ids_to_unlock, @@ -171,7 +172,7 @@ where // no need to reissue if one input got spent if input_got_spent { process_transaction_with_unknown_state( - &account_details, + &wallet_data, transaction, &mut updated_transactions, &mut output_ids_to_unlock, @@ -197,7 +198,7 @@ where } } } - drop(account_details); + drop(wallet_data); for mut transaction in transactions_to_reissue { log::debug!("[SYNC] reissue transaction"); @@ -207,7 +208,7 @@ where } // updates account with balances, output ids, outputs - self.update_account_with_transactions(updated_transactions, spent_output_ids, output_ids_to_unlock) + self.update_with_transactions(updated_transactions, spent_output_ids, output_ids_to_unlock) .await?; Ok(confirmed_unknown_output) @@ -235,7 +236,7 @@ fn updated_transaction_and_outputs( // When a transaction got pruned, the inputs and outputs are also not available, then this could mean that it was // confirmed and the created outputs got also already spent and pruned or the inputs got spent in another transaction fn process_transaction_with_unknown_state( - account: &AccountDetails, + wallet_data: &WalletData, mut transaction: TransactionWithMetadata, updated_transactions: &mut Vec, output_ids_to_unlock: &mut Vec, @@ -243,7 +244,7 @@ fn process_transaction_with_unknown_state( let mut all_inputs_spent = true; for input in transaction.payload.transaction().inputs() { let Input::Utxo(input) = input; - if let Some(output_data) = account.outputs.get(input.output_id()) { + if let Some(output_data) = wallet_data.outputs.get(input.output_id()) { if !output_data.metadata.is_spent() { // unspent output needs to be made available again output_ids_to_unlock.push(*input.output_id()); diff --git a/sdk/src/wallet/account/operations/transaction/build_transaction.rs b/sdk/src/wallet/operations/transaction/build_transaction.rs similarity index 95% rename from sdk/src/wallet/account/operations/transaction/build_transaction.rs rename to sdk/src/wallet/operations/transaction/build_transaction.rs index 4fdf996c9a..a390abebc7 100644 --- a/sdk/src/wallet/account/operations/transaction/build_transaction.rs +++ b/sdk/src/wallet/operations/transaction/build_transaction.rs @@ -12,10 +12,10 @@ use crate::{ input::{Input, UtxoInput}, payload::signed_transaction::Transaction, }, - wallet::account::{operations::transaction::TransactionOptions, Account}, + wallet::{operations::transaction::TransactionOptions, Wallet}, }; -impl Account +impl Wallet where crate::wallet::Error: From, { diff --git a/sdk/src/wallet/account/operations/transaction/high_level/burning_melting/melt_native_token.rs b/sdk/src/wallet/operations/transaction/high_level/burning_melting/melt_native_token.rs similarity index 95% rename from sdk/src/wallet/account/operations/transaction/high_level/burning_melting/melt_native_token.rs rename to sdk/src/wallet/operations/transaction/high_level/burning_melting/melt_native_token.rs index 7ade02fbfc..e17227e932 100644 --- a/sdk/src/wallet/account/operations/transaction/high_level/burning_melting/melt_native_token.rs +++ b/sdk/src/wallet/operations/transaction/high_level/burning_melting/melt_native_token.rs @@ -10,12 +10,13 @@ use crate::{ TokenScheme, }, wallet::{ - account::{operations::transaction::TransactionWithMetadata, types::OutputData, Account, TransactionOptions}, - Error, + operations::transaction::TransactionOptions, + types::{OutputData, TransactionWithMetadata}, + Error, Wallet, }, }; -impl Account +impl Wallet where crate::wallet::Error: From, crate::client::Error: From, @@ -94,7 +95,7 @@ where let mut existing_account_output_data = None; let mut existing_foundry_output = None; - for (output_id, output_data) in self.details().await.unspent_outputs().iter() { + for (output_id, output_data) in self.data().await.unspent_outputs.iter() { match &output_data.output { Output::Account(output) => { if output.account_id_non_null(output_id) == account_id { diff --git a/sdk/src/wallet/account/operations/transaction/high_level/burning_melting/mod.rs b/sdk/src/wallet/operations/transaction/high_level/burning_melting/mod.rs similarity index 94% rename from sdk/src/wallet/account/operations/transaction/high_level/burning_melting/mod.rs rename to sdk/src/wallet/operations/transaction/high_level/burning_melting/mod.rs index 2d7d200ff9..809a4ee45e 100644 --- a/sdk/src/wallet/account/operations/transaction/high_level/burning_melting/mod.rs +++ b/sdk/src/wallet/operations/transaction/high_level/burning_melting/mod.rs @@ -3,15 +3,12 @@ use crate::{ client::api::{input_selection::Burn, PreparedTransactionData}, - wallet::{ - account::{types::TransactionWithMetadata, TransactionOptions}, - Account, - }, + wallet::{operations::transaction::TransactionOptions, types::TransactionWithMetadata, Wallet}, }; pub(crate) mod melt_native_token; -impl Account { +impl Wallet { /// A generic function that can be used to burn native tokens, nfts, foundries and accounts. /// /// Note that burning **native tokens** doesn't require the foundry output which minted them, but will not increase diff --git a/sdk/src/wallet/account/operations/transaction/high_level/create_account.rs b/sdk/src/wallet/operations/transaction/high_level/create_account.rs similarity index 91% rename from sdk/src/wallet/account/operations/transaction/high_level/create_account.rs rename to sdk/src/wallet/operations/transaction/high_level/create_account.rs index 1eaeca7ca6..4b9b318f20 100644 --- a/sdk/src/wallet/account/operations/transaction/high_level/create_account.rs +++ b/sdk/src/wallet/operations/transaction/high_level/create_account.rs @@ -12,15 +12,19 @@ use crate::{ }, }, utils::serde::option_prefix_hex_bytes, - wallet::account::{types::TransactionWithMetadata, Account, OutputData, TransactionOptions}, + wallet::{ + operations::transaction::TransactionOptions, + types::{OutputData, TransactionWithMetadata}, + Wallet, + }, }; /// Params `create_account_output()` #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CreateAccountParams { - /// Bech32 encoded address which will control the account. Default will use the first - /// ed25519 address of the wallet account + /// Bech32 encoded address which will control the account. Default will use the + /// ed25519 wallet address pub address: Option, /// Immutable account metadata #[serde(default, with = "option_prefix_hex_bytes")] @@ -30,7 +34,7 @@ pub struct CreateAccountParams { pub metadata: Option>, } -impl Account +impl Wallet where crate::wallet::Error: From, crate::client::Error: From, @@ -78,15 +82,7 @@ where self.client().bech32_hrp_matches(bech32_address.hrp()).await?; bech32_address.inner().clone() } - None => { - self.public_addresses() - .await - .into_iter() - .next() - .expect("first address is generated during account creation") - .address - .inner - } + None => self.address().await.inner().clone(), }; let mut account_output_builder = @@ -116,9 +112,9 @@ where /// Gets an existing account output. pub(crate) async fn get_account_output(&self, account_id: Option) -> Option<(AccountId, OutputData)> { log::debug!("[get_account_output]"); - self.details() + self.data() .await - .unspent_outputs() + .unspent_outputs .values() .find_map(|output_data| match &output_data.output { Output::Account(account_output) => { diff --git a/sdk/src/wallet/account/operations/transaction/high_level/minting/create_native_token.rs b/sdk/src/wallet/operations/transaction/high_level/minting/create_native_token.rs similarity index 98% rename from sdk/src/wallet/account/operations/transaction/high_level/minting/create_native_token.rs rename to sdk/src/wallet/operations/transaction/high_level/minting/create_native_token.rs index 7daaa703d7..9c08563832 100644 --- a/sdk/src/wallet/account/operations/transaction/high_level/minting/create_native_token.rs +++ b/sdk/src/wallet/operations/transaction/high_level/minting/create_native_token.rs @@ -16,9 +16,10 @@ use crate::{ AccountOutputBuilder, FoundryId, FoundryOutputBuilder, Output, SimpleTokenScheme, TokenId, TokenScheme, }, }, - wallet::account::{ + wallet::{ + operations::transaction::TransactionOptions, types::{TransactionWithMetadata, TransactionWithMetadataDto}, - Account, TransactionOptions, + Wallet, }, }; @@ -85,7 +86,7 @@ impl From<&PreparedCreateNativeTokenTransaction> for PreparedCreateNativeTokenTr } } -impl Account +impl Wallet where crate::wallet::Error: From, crate::client::Error: From, diff --git a/sdk/src/wallet/account/operations/transaction/high_level/minting/mint_native_token.rs b/sdk/src/wallet/operations/transaction/high_level/minting/mint_native_token.rs similarity index 92% rename from sdk/src/wallet/account/operations/transaction/high_level/minting/mint_native_token.rs rename to sdk/src/wallet/operations/transaction/high_level/minting/mint_native_token.rs index 5f3e7f0f83..19627bb6c4 100644 --- a/sdk/src/wallet/account/operations/transaction/high_level/minting/mint_native_token.rs +++ b/sdk/src/wallet/operations/transaction/high_level/minting/mint_native_token.rs @@ -8,13 +8,10 @@ use crate::{ types::block::output::{ AccountOutputBuilder, FoundryOutputBuilder, Output, SimpleTokenScheme, TokenId, TokenScheme, }, - wallet::{ - account::{types::TransactionWithMetadata, Account, TransactionOptions}, - Error, - }, + wallet::{operations::transaction::TransactionOptions, types::TransactionWithMetadata, Error, Wallet}, }; -impl Account +impl Wallet where crate::wallet::Error: From, crate::client::Error: From, @@ -60,9 +57,9 @@ where log::debug!("[TRANSACTION] mint_native_token"); let mint_amount = mint_amount.into(); - let account_details = self.details().await; + let wallet_data = self.data().await; let token_supply = self.client().get_token_supply().await?; - let existing_foundry_output = account_details.unspent_outputs().values().find(|output_data| { + let existing_foundry_output = wallet_data.unspent_outputs.values().find(|output_data| { if let Output::Foundry(output) = &output_data.output { TokenId::new(*output.id()) == token_id } else { @@ -85,7 +82,7 @@ where } // Get the account output that controls the foundry output - let existing_account_output = account_details.unspent_outputs().values().find(|output_data| { + let existing_account_output = wallet_data.unspent_outputs.values().find(|output_data| { if let Output::Account(output) = &output_data.output { output.account_id_non_null(&output_data.output_id) == **foundry_output.account_address() } else { @@ -99,7 +96,7 @@ where return Err(Error::MintingFailed("account output is not available".to_string())); }; - drop(account_details); + drop(wallet_data); let account_output = if let Output::Account(account_output) = existing_account_output.output { account_output diff --git a/sdk/src/wallet/account/operations/transaction/high_level/minting/mint_nfts.rs b/sdk/src/wallet/operations/transaction/high_level/minting/mint_nfts.rs similarity index 93% rename from sdk/src/wallet/account/operations/transaction/high_level/minting/mint_nfts.rs rename to sdk/src/wallet/operations/transaction/high_level/minting/mint_nfts.rs index 63c02d1614..acd9b6807b 100644 --- a/sdk/src/wallet/account/operations/transaction/high_level/minting/mint_nfts.rs +++ b/sdk/src/wallet/operations/transaction/high_level/minting/mint_nfts.rs @@ -16,8 +16,8 @@ use crate::{ ConvertTo, }, wallet::{ - account::{operations::transaction::TransactionWithMetadata, Account, TransactionOptions}, - Error as WalletError, + operations::transaction::{TransactionOptions, TransactionWithMetadata}, + Wallet, }, }; @@ -109,7 +109,7 @@ impl MintNftParams { } } -impl Account +impl Wallet where crate::wallet::Error: From, crate::client::Error: From, @@ -163,7 +163,7 @@ where log::debug!("[TRANSACTION] prepare_mint_nfts"); let rent_structure = self.client().get_rent_structure().await?; let token_supply = self.client().get_token_supply().await?; - let account_addresses = self.addresses().await; + let wallet_address = self.address().await.into_inner(); let mut outputs = Vec::new(); for MintNftParams { @@ -178,14 +178,9 @@ where let address = match address { Some(address) => { self.client().bech32_hrp_matches(address.hrp()).await?; - address + address.inner().clone() } - // todo other error message - None => account_addresses - .first() - .ok_or(WalletError::FailedToGetRemainder)? - .address - .clone(), + None => wallet_address.clone(), }; // NftId needs to be set to 0 for the creation diff --git a/sdk/src/wallet/account/operations/transaction/high_level/minting/mod.rs b/sdk/src/wallet/operations/transaction/high_level/minting/mod.rs similarity index 100% rename from sdk/src/wallet/account/operations/transaction/high_level/minting/mod.rs rename to sdk/src/wallet/operations/transaction/high_level/minting/mod.rs diff --git a/sdk/src/wallet/account/operations/transaction/high_level/mod.rs b/sdk/src/wallet/operations/transaction/high_level/mod.rs similarity index 100% rename from sdk/src/wallet/account/operations/transaction/high_level/mod.rs rename to sdk/src/wallet/operations/transaction/high_level/mod.rs diff --git a/sdk/src/wallet/account/operations/transaction/high_level/send.rs b/sdk/src/wallet/operations/transaction/high_level/send.rs similarity index 93% rename from sdk/src/wallet/account/operations/transaction/high_level/send.rs rename to sdk/src/wallet/operations/transaction/high_level/send.rs index 6732705fcc..dc055c1c06 100644 --- a/sdk/src/wallet/account/operations/transaction/high_level/send.rs +++ b/sdk/src/wallet/operations/transaction/high_level/send.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use crate::{ client::{api::PreparedTransactionData, secret::SecretManage}, types::block::{ - address::Bech32Address, + address::{Bech32Address, ToBech32Ext}, output::{ unlock_condition::{ AddressUnlockCondition, ExpirationUnlockCondition, StorageDepositReturnUnlockCondition, @@ -19,11 +19,9 @@ use crate::{ }, utils::serde::string, wallet::{ - account::{ - constants::DEFAULT_EXPIRATION_SLOTS, operations::transaction::TransactionWithMetadata, Account, - TransactionOptions, - }, - Error, + constants::DEFAULT_EXPIRATION_SLOTS, + operations::transaction::{TransactionOptions, TransactionWithMetadata}, + Error, Wallet, }, }; @@ -78,7 +76,7 @@ impl SendParams { } } -impl Account +impl Wallet where crate::wallet::Error: From, crate::client::Error: From, @@ -144,11 +142,9 @@ where let rent_structure = self.client().get_rent_structure().await?; let token_supply = self.client().get_token_supply().await?; - let account_addresses = self.addresses().await; - let default_return_address = account_addresses - .into_iter() - .next() - .ok_or(Error::FailedToGetRemainder)?; + let wallet_address = self.address().await; + + let default_return_address = wallet_address.to_bech32(self.client().get_bech32_hrp().await?); let slot_index = self.client().get_slot_index().await?; @@ -172,7 +168,7 @@ where Ok::<_, Error>(return_address) }) .transpose()? - .unwrap_or_else(|| default_return_address.address.clone()); + .unwrap_or_else(|| default_return_address.clone()); // Get the minimum required amount for an output assuming it does not need a storage deposit. let output = BasicOutputBuilder::new_with_minimum_storage_deposit(rent_structure) diff --git a/sdk/src/wallet/account/operations/transaction/high_level/send_native_tokens.rs b/sdk/src/wallet/operations/transaction/high_level/send_native_tokens.rs similarity index 93% rename from sdk/src/wallet/account/operations/transaction/high_level/send_native_tokens.rs rename to sdk/src/wallet/operations/transaction/high_level/send_native_tokens.rs index 2b1a66a1ed..55ef81733a 100644 --- a/sdk/src/wallet/account/operations/transaction/high_level/send_native_tokens.rs +++ b/sdk/src/wallet/operations/transaction/high_level/send_native_tokens.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; use crate::{ client::{api::PreparedTransactionData, secret::SecretManage}, types::block::{ - address::Bech32Address, + address::{Bech32Address, ToBech32Ext}, output::{ unlock_condition::{ AddressUnlockCondition, ExpirationUnlockCondition, StorageDepositReturnUnlockCondition, @@ -19,11 +19,9 @@ use crate::{ ConvertTo, }, wallet::{ - account::{ - constants::DEFAULT_EXPIRATION_SLOTS, operations::transaction::TransactionWithMetadata, Account, - TransactionOptions, - }, - Error, Result, + constants::DEFAULT_EXPIRATION_SLOTS, + operations::transaction::{TransactionOptions, TransactionWithMetadata}, + Error, Result, Wallet, }, }; @@ -80,7 +78,7 @@ impl SendNativeTokensParams { } } -impl Account +impl Wallet where crate::wallet::Error: From, crate::client::Error: From, @@ -134,11 +132,8 @@ where let rent_structure = self.client().get_rent_structure().await?; let token_supply = self.client().get_token_supply().await?; - let account_addresses = self.addresses().await; - let default_return_address = account_addresses - .into_iter() - .next() - .ok_or(Error::FailedToGetRemainder)?; + let wallet_address = self.address().await; + let default_return_address = wallet_address.to_bech32(self.client().get_bech32_hrp().await?); let slot_index = self.client().get_slot_index().await?; @@ -162,7 +157,7 @@ where Ok::<_, Error>(addr) }) .transpose()? - .unwrap_or_else(|| default_return_address.address.clone()); + .unwrap_or_else(|| default_return_address.clone()); let native_tokens = NativeTokens::from_vec( native_tokens diff --git a/sdk/src/wallet/account/operations/transaction/high_level/send_nft.rs b/sdk/src/wallet/operations/transaction/high_level/send_nft.rs similarity index 96% rename from sdk/src/wallet/account/operations/transaction/high_level/send_nft.rs rename to sdk/src/wallet/operations/transaction/high_level/send_nft.rs index f82386bfe6..72560a5599 100644 --- a/sdk/src/wallet/account/operations/transaction/high_level/send_nft.rs +++ b/sdk/src/wallet/operations/transaction/high_level/send_nft.rs @@ -11,7 +11,10 @@ use crate::{ output::{unlock_condition::AddressUnlockCondition, NftId, NftOutputBuilder, Output}, ConvertTo, }, - wallet::account::{operations::transaction::TransactionWithMetadata, Account, TransactionOptions}, + wallet::{ + operations::transaction::{TransactionOptions, TransactionWithMetadata}, + Wallet, + }, }; /// Params for `send_nft()` @@ -39,7 +42,7 @@ impl SendNftParams { } } -impl Account +impl Wallet where crate::wallet::Error: From, crate::client::Error: From, diff --git a/sdk/src/wallet/account/operations/transaction/input_selection.rs b/sdk/src/wallet/operations/transaction/input_selection.rs similarity index 85% rename from sdk/src/wallet/account/operations/transaction/input_selection.rs rename to sdk/src/wallet/operations/transaction/input_selection.rs index fb7dd9e0f1..71e437b38c 100644 --- a/sdk/src/wallet/account/operations/transaction/input_selection.rs +++ b/sdk/src/wallet/operations/transaction/input_selection.rs @@ -15,16 +15,18 @@ use crate::{ output::{Output, OutputId}, slot::SlotIndex, }, - wallet::account::{ - operations::helpers::time::can_output_be_unlocked_forever_from_now_on, Account, AccountDetails, OutputData, + wallet::{ + core::WalletData, operations::helpers::time::can_output_be_unlocked_forever_from_now_on, types::OutputData, + Wallet, }, }; -impl Account +impl Wallet where crate::wallet::Error: From, + crate::client::Error: From, { - /// Selects inputs for a transaction and locks them in the account, so they don't get used again + /// Selects inputs for a transaction and locks them in the wallet, so they don't get used again pub(crate) async fn select_inputs( &self, outputs: Vec, @@ -38,26 +40,18 @@ where #[cfg(feature = "participation")] let voting_output = self.get_voting_output().await?; // lock so the same inputs can't be selected in multiple transactions - let mut account_details = self.details_mut().await; + let mut wallet_data = self.data_mut().await; let protocol_parameters = self.client().get_protocol_parameters().await?; #[cfg(feature = "events")] - self.emit( - account_details.index, - WalletEvent::TransactionProgress(TransactionProgressEvent::SelectingInputs), - ) + self.emit(WalletEvent::TransactionProgress( + TransactionProgressEvent::SelectingInputs, + )) .await; let slot_index = self.client().get_slot_index().await?; #[allow(unused_mut)] - let mut forbidden_inputs = account_details.locked_outputs.clone(); - - let addresses = account_details - .public_addresses() - .iter() - .chain(account_details.internal_addresses().iter()) - .map(|address| address.address.as_ref().clone()) - .collect::>(); + let mut forbidden_inputs = wallet_data.locked_outputs.clone(); // Prevent consuming the voting output if not actually wanted #[cfg(feature = "participation")] @@ -73,8 +67,8 @@ where // Filter inputs to not include inputs that require additional outputs for storage deposit return or could be // still locked. let available_outputs_signing_data = filter_inputs( - &account_details, - account_details.unspent_outputs.values(), + &wallet_data, + wallet_data.unspent_outputs.values(), slot_index, custom_inputs.as_ref(), mandatory_inputs.as_ref(), @@ -85,7 +79,7 @@ where if let Some(custom_inputs) = custom_inputs { // Check that no input got already locked for input in custom_inputs.iter() { - if account_details.locked_outputs.contains(input) { + if wallet_data.locked_outputs.contains(input) { return Err(crate::wallet::Error::CustomInput(format!( "provided custom input {input} is already used in another transaction", ))); @@ -95,7 +89,7 @@ where let mut input_selection = InputSelection::new( available_outputs_signing_data, outputs, - addresses, + Some(wallet_data.address.clone().into_inner()), protocol_parameters.clone(), ) .with_required_inputs(custom_inputs) @@ -113,14 +107,14 @@ where // lock outputs so they don't get used by another transaction for output in &selected_transaction_data.inputs { - account_details.locked_outputs.insert(*output.output_id()); + wallet_data.locked_outputs.insert(*output.output_id()); } return Ok(selected_transaction_data); } else if let Some(mandatory_inputs) = mandatory_inputs { // Check that no input got already locked for input in mandatory_inputs.iter() { - if account_details.locked_outputs.contains(input) { + if wallet_data.locked_outputs.contains(input) { return Err(crate::wallet::Error::CustomInput(format!( "provided custom input {input} is already used in another transaction", ))); @@ -130,7 +124,7 @@ where let mut input_selection = InputSelection::new( available_outputs_signing_data, outputs, - addresses, + Some(wallet_data.address.clone().into_inner()), protocol_parameters.clone(), ) .with_required_inputs(mandatory_inputs) @@ -148,12 +142,12 @@ where // lock outputs so they don't get used by another transaction for output in &selected_transaction_data.inputs { - account_details.locked_outputs.insert(*output.output_id()); + wallet_data.locked_outputs.insert(*output.output_id()); } // lock outputs so they don't get used by another transaction for output in &selected_transaction_data.inputs { - account_details.locked_outputs.insert(*output.output_id()); + wallet_data.locked_outputs.insert(*output.output_id()); } return Ok(selected_transaction_data); @@ -162,7 +156,7 @@ where let mut input_selection = InputSelection::new( available_outputs_signing_data, outputs, - addresses, + Some(wallet_data.address.clone().into_inner()), protocol_parameters.clone(), ) .with_forbidden_inputs(forbidden_inputs); @@ -195,7 +189,7 @@ where // lock outputs so they don't get used by another transaction for output in &selected_transaction_data.inputs { log::debug!("[TRANSACTION] locking: {}", output.output_id()); - account_details.locked_outputs.insert(*output.output_id()); + wallet_data.locked_outputs.insert(*output.output_id()); } Ok(selected_transaction_data) @@ -222,7 +216,7 @@ where /// | [Address, StorageDepositReturn, expired Expiration] | yes | #[allow(clippy::too_many_arguments)] fn filter_inputs( - account: &AccountDetails, + wallet_data: &WalletData, available_outputs: Values<'_, OutputId, OutputData>, slot_index: SlotIndex, custom_inputs: Option<&HashSet>, @@ -241,7 +235,7 @@ fn filter_inputs( let output_can_be_unlocked_now_and_in_future = can_output_be_unlocked_forever_from_now_on( // We use the addresses with unspent outputs, because other addresses of the // account without unspent outputs can't be related to this output - &account.addresses_with_unspent_outputs, + &wallet_data.address.inner, &output_data.output, slot_index, ); @@ -252,7 +246,7 @@ fn filter_inputs( } } - if let Some(available_input) = output_data.input_signing_data(account, slot_index)? { + if let Some(available_input) = output_data.input_signing_data(wallet_data, slot_index)? { available_outputs_signing_data.push(available_input); } } diff --git a/sdk/src/wallet/account/operations/transaction/mod.rs b/sdk/src/wallet/operations/transaction/mod.rs similarity index 92% rename from sdk/src/wallet/account/operations/transaction/mod.rs rename to sdk/src/wallet/operations/transaction/mod.rs index 693526df20..8d00c142de 100644 --- a/sdk/src/wallet/account/operations/transaction/mod.rs +++ b/sdk/src/wallet/operations/transaction/mod.rs @@ -24,13 +24,13 @@ use crate::{ payload::signed_transaction::SignedTransactionPayload, }, }, - wallet::account::{ + wallet::{ types::{InclusionState, TransactionWithMetadata}, - Account, + Wallet, }, }; -impl Account +impl Wallet where crate::wallet::Error: From, crate::client::Error: From, @@ -95,7 +95,7 @@ where .await } - /// Signs a transaction, submit it to a node and store it in the account + /// Signs a transaction, submit it to a node and store it in the wallet pub async fn sign_and_submit_transaction( &self, prepared_transaction_data: PreparedTransactionData, @@ -116,7 +116,7 @@ where .await } - /// Validates the transaction, submit it to a node and store it in the account + /// Validates the transaction, submit it to a node and store it in the wallet pub async fn submit_and_store_transaction( &self, signed_transaction_data: SignedTransactionData, @@ -179,14 +179,15 @@ where inputs, }; - let mut account_details = self.details_mut().await; + let mut wallet_data = self.data_mut().await; - account_details.transactions.insert(transaction_id, transaction.clone()); - account_details.pending_transactions.insert(transaction_id); + wallet_data.transactions.insert(transaction_id, transaction.clone()); + wallet_data.pending_transactions.insert(transaction_id); #[cfg(feature = "storage")] { - log::debug!("[TRANSACTION] storing account {}", account_details.index()); - self.save(Some(&account_details)).await?; + // TODO: maybe better to use the wallt address as identifier now? + log::debug!("[TRANSACTION] storing wallet"); + self.save(Some(&wallet_data)).await?; } Ok(transaction) @@ -194,10 +195,10 @@ where // unlock outputs async fn unlock_inputs(&self, inputs: &[InputSigningData]) -> crate::wallet::Result<()> { - let mut account_details = self.details_mut().await; + let mut wallet_data = self.data_mut().await; for input_signing_data in inputs { let output_id = input_signing_data.output_id(); - account_details.locked_outputs.remove(output_id); + wallet_data.locked_outputs.remove(output_id); log::debug!( "[TRANSACTION] Unlocked output {} because of transaction error", output_id diff --git a/sdk/src/wallet/account/operations/transaction/options.rs b/sdk/src/wallet/operations/transaction/options.rs similarity index 95% rename from sdk/src/wallet/account/operations/transaction/options.rs rename to sdk/src/wallet/operations/transaction/options.rs index a41072122c..7be4a75b9e 100644 --- a/sdk/src/wallet/account/operations/transaction/options.rs +++ b/sdk/src/wallet/operations/transaction/options.rs @@ -35,8 +35,6 @@ pub struct TransactionOptions { pub enum RemainderValueStrategy { /// Keep the remainder value on the source address. ReuseAddress, - /// Move the remainder value to a change address. - ChangeAddress, /// Move the remainder value to any specified address. CustomAddress(Address), } diff --git a/sdk/src/wallet/account/operations/transaction/prepare_output.rs b/sdk/src/wallet/operations/transaction/prepare_output.rs similarity index 93% rename from sdk/src/wallet/account/operations/transaction/prepare_output.rs rename to sdk/src/wallet/operations/transaction/prepare_output.rs index fc9b596cf6..70c2e3a612 100644 --- a/sdk/src/wallet/account/operations/transaction/prepare_output.rs +++ b/sdk/src/wallet/operations/transaction/prepare_output.rs @@ -20,12 +20,14 @@ use crate::{ Error, }, utils::serde::string, - wallet::account::{ - operations::transaction::RemainderValueStrategy, types::OutputData, Account, TransactionOptions, + wallet::{ + operations::transaction::{RemainderValueStrategy, TransactionOptions}, + types::OutputData, + Wallet, }, }; -impl Account +impl Wallet where crate::wallet::Error: From, crate::client::Error: From, @@ -285,35 +287,15 @@ where ) -> crate::wallet::Result
{ let transaction_options = transaction_options.into(); - let remainder_address = match &transaction_options { - Some(options) => { - match &options.remainder_value_strategy { - RemainderValueStrategy::ReuseAddress => { - // select_inputs will select an address from the inputs if it's none - None - } - RemainderValueStrategy::ChangeAddress => { - let remainder_address = self.generate_remainder_address().await?; - Some(remainder_address.address.inner) - } - RemainderValueStrategy::CustomAddress(address) => Some(address.clone()), - } + Ok(if let Some(options) = &transaction_options { + match &options.remainder_value_strategy { + // TODO is this correct? It was None before the accounts removal + RemainderValueStrategy::ReuseAddress => self.address().await.into_inner(), + RemainderValueStrategy::CustomAddress(address) => address.clone(), } - None => None, - }; - let remainder_address = match remainder_address { - Some(address) => address, - None => { - self.addresses() - .await - .into_iter() - .next() - .ok_or(crate::wallet::Error::FailedToGetRemainder)? - .address - .inner - } - }; - Ok(remainder_address) + } else { + self.address().await.into_inner() + }) } } diff --git a/sdk/src/wallet/account/operations/transaction/prepare_transaction.rs b/sdk/src/wallet/operations/transaction/prepare_transaction.rs similarity index 73% rename from sdk/src/wallet/account/operations/transaction/prepare_transaction.rs rename to sdk/src/wallet/operations/transaction/prepare_transaction.rs index b6c6d67304..833657c311 100644 --- a/sdk/src/wallet/account/operations/transaction/prepare_transaction.rs +++ b/sdk/src/wallet/operations/transaction/prepare_transaction.rs @@ -14,13 +14,13 @@ use crate::{ input::INPUT_COUNT_RANGE, output::{Output, OUTPUT_COUNT_RANGE}, }, - wallet::account::{ + wallet::{ operations::transaction::{RemainderValueStrategy, TransactionOptions}, - Account, + Wallet, }, }; -impl Account +impl Wallet where crate::wallet::Error: From, crate::client::Error: From, @@ -71,35 +71,12 @@ where } } - let remainder_address = match &options { - Some(options) => { - match &options.remainder_value_strategy { - RemainderValueStrategy::ReuseAddress => { - // select_inputs will select an address from the inputs if it's none - None - } - RemainderValueStrategy::ChangeAddress => { - let remainder_address = self.generate_remainder_address().await?; - #[cfg(feature = "events")] - { - let account_index = self.details().await.index; - self.emit( - account_index, - WalletEvent::TransactionProgress( - TransactionProgressEvent::GeneratingRemainderDepositAddress(AddressData { - address: remainder_address.address.clone(), - }), - ), - ) - .await; - } - Some(remainder_address.address.inner) - } - RemainderValueStrategy::CustomAddress(address) => Some(address.clone()), - } - } - None => None, - }; + let remainder_address = options + .as_ref() + .and_then(|options| match &options.remainder_value_strategy { + RemainderValueStrategy::ReuseAddress => None, + RemainderValueStrategy::CustomAddress(address) => Some(address.clone()), + }); let selected_transaction_data = self .select_inputs( diff --git a/sdk/src/wallet/account/operations/transaction/sign_transaction.rs b/sdk/src/wallet/operations/transaction/sign_transaction.rs similarity index 74% rename from sdk/src/wallet/account/operations/transaction/sign_transaction.rs rename to sdk/src/wallet/operations/transaction/sign_transaction.rs index e8f6742c58..2421bda089 100644 --- a/sdk/src/wallet/account/operations/transaction/sign_transaction.rs +++ b/sdk/src/wallet/operations/transaction/sign_transaction.rs @@ -19,10 +19,10 @@ use crate::{ }, secret::SecretManage, }, - wallet::account::{operations::transaction::SignedTransactionPayload, Account}, + wallet::{operations::transaction::SignedTransactionPayload, Wallet}, }; -impl Account +impl Wallet where crate::wallet::Error: From, crate::client::Error: From, @@ -35,16 +35,15 @@ where log::debug!("[TRANSACTION] sign_transaction"); log::debug!("[TRANSACTION] prepared_transaction_data {prepared_transaction_data:?}"); #[cfg(feature = "events")] - self.emit( - self.details().await.index, - WalletEvent::TransactionProgress(TransactionProgressEvent::SigningTransaction), - ) + self.emit(WalletEvent::TransactionProgress( + TransactionProgressEvent::SigningTransaction, + )) .await; #[cfg(all(feature = "events", feature = "ledger_nano"))] { - use crate::wallet::account::SecretManager; - let secret_manager = self.wallet.secret_manager.read().await; + use crate::client::secret::SecretManager; + let secret_manager = self.secret_manager.read().await; if let Some(ledger) = secret_manager.downcast::().or_else(|| { secret_manager.downcast::().and_then(|s| { if let SecretManager::LedgerNano(n) = s { @@ -57,20 +56,18 @@ where let ledger_nano_status = ledger.get_ledger_nano_status().await; if let Some(buffer_size) = ledger_nano_status.buffer_size() { if needs_blind_signing(prepared_transaction_data, buffer_size) { - self.emit( - self.details().await.index, - WalletEvent::TransactionProgress(TransactionProgressEvent::PreparedTransactionSigningHash( + self.emit(WalletEvent::TransactionProgress( + TransactionProgressEvent::PreparedTransactionSigningHash( prepared_transaction_data.transaction.signing_hash().to_string(), - )), - ) + ), + )) .await; } else { - self.emit( - self.details().await.index, - WalletEvent::TransactionProgress(TransactionProgressEvent::PreparedTransaction(Box::new( - PreparedTransactionDataDto::from(prepared_transaction_data), + self.emit(WalletEvent::TransactionProgress( + TransactionProgressEvent::PreparedTransaction(Box::new(PreparedTransactionDataDto::from( + prepared_transaction_data, ))), - ) + )) .await; } } @@ -78,7 +75,6 @@ where } let unlocks = match self - .wallet .secret_manager .read() .await diff --git a/sdk/src/wallet/account/operations/transaction/submit_transaction.rs b/sdk/src/wallet/operations/transaction/submit_transaction.rs similarity index 72% rename from sdk/src/wallet/account/operations/transaction/submit_transaction.rs rename to sdk/src/wallet/operations/transaction/submit_transaction.rs index 28b4bc015b..bf93b3d298 100644 --- a/sdk/src/wallet/account/operations/transaction/submit_transaction.rs +++ b/sdk/src/wallet/operations/transaction/submit_transaction.rs @@ -8,10 +8,10 @@ use crate::wallet::events::types::{TransactionProgressEvent, WalletEvent}; use crate::{ client::secret::{SecretManage, SignBlock}, types::block::{payload::Payload, BlockId}, - wallet::account::{operations::transaction::SignedTransactionPayload, Account}, + wallet::{operations::transaction::SignedTransactionPayload, Error, Wallet}, }; -impl Account +impl Wallet where crate::wallet::Error: From, crate::client::Error: From, @@ -22,8 +22,6 @@ where transaction_payload: SignedTransactionPayload, ) -> crate::wallet::Result { log::debug!("[TRANSACTION] send_payload"); - #[cfg(feature = "events")] - let account_index = self.details().await.index; let block = self .client() @@ -31,16 +29,13 @@ where .await? .sign_ed25519( &*self.get_secret_manager().read().await, - Bip44::new(self.wallet.coin_type()), + self.bip_path().await.ok_or(Error::MissingBipPath)?, ) .await?; #[cfg(feature = "events")] - self.emit( - account_index, - WalletEvent::TransactionProgress(TransactionProgressEvent::Broadcasting), - ) - .await; + self.emit(WalletEvent::TransactionProgress(TransactionProgressEvent::Broadcasting)) + .await; let block_id = self.client().post_block(&block).await?; log::debug!("[TRANSACTION] submitted block {}", block_id); Ok(block_id) diff --git a/sdk/src/wallet/storage/constants.rs b/sdk/src/wallet/storage/constants.rs index ae75d94881..e50e1a3f15 100644 --- a/sdk/src/wallet/storage/constants.rs +++ b/sdk/src/wallet/storage/constants.rs @@ -15,18 +15,15 @@ pub const fn default_storage_path() -> &'static str { DEFAULT_STORAGE_PATH } -pub(crate) const WALLET_INDEXATION_KEY: &str = "iota-wallet-account-manager"; - -pub(crate) const SECRET_MANAGER_KEY: &str = "secret_manager"; - -pub(crate) const ACCOUNTS_INDEXATION_KEY: &str = "iota-wallet-accounts"; -pub(crate) const ACCOUNT_INDEXATION_KEY: &str = "iota-wallet-account-"; - -pub(crate) const ACCOUNT_SYNC_OPTIONS: &str = "sync-options"; - pub(crate) const DATABASE_SCHEMA_VERSION: u8 = 1; pub(crate) const DATABASE_SCHEMA_VERSION_KEY: &str = "database-schema-version"; +pub(crate) const WALLET_DATA_KEY: &str = "wallet-data"; +pub(crate) const WALLET_BUILDER_KEY: &str = "wallet-builder"; +pub(crate) const WALLET_SYNC_OPTIONS: &str = "wallet-sync-options"; + +pub(crate) const SECRET_MANAGER_KEY: &str = "secret-manager"; + #[cfg(feature = "participation")] pub(crate) const PARTICIPATION_EVENTS: &str = "participation-events"; #[cfg(feature = "participation")] diff --git a/sdk/src/wallet/storage/manager.rs b/sdk/src/wallet/storage/manager.rs index b8a9c30560..4da7b634e4 100644 --- a/sdk/src/wallet/storage/manager.rs +++ b/sdk/src/wallet/storage/manager.rs @@ -1,15 +1,15 @@ // Copyright 2021 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use futures::{StreamExt, TryStreamExt}; use zeroize::Zeroizing; use crate::{ client::storage::StorageAdapter, types::TryFromDto, wallet::{ - account::{AccountDetails, AccountDetailsDto, SyncOptions}, + core::{WalletData, WalletDataDto}, migration::migrate, + operations::syncing::SyncOptions, storage::{constants::*, DynStorageAdapter, Storage}, }, }; @@ -18,8 +18,6 @@ use crate::{ #[derive(Debug)] pub(crate) struct StorageManager { pub(crate) storage: Storage, - // account indexes for accounts in the database - account_indexes: Vec, } impl StorageManager { @@ -46,70 +44,30 @@ impl StorageManager { .await?; }; - let account_indexes = storage.get(ACCOUNTS_INDEXATION_KEY).await?.unwrap_or_default(); - - let storage_manager = Self { - storage, - account_indexes, - }; + let storage_manager = Self { storage }; Ok(storage_manager) } - pub(crate) async fn get_accounts(&mut self) -> crate::wallet::Result> { - if let Some(account_indexes) = self.get(ACCOUNTS_INDEXATION_KEY).await? { - if self.account_indexes.is_empty() { - self.account_indexes = account_indexes; - } + pub(crate) async fn load_wallet_data(&mut self) -> crate::wallet::Result> { + if let Some(dto) = self.get::(WALLET_DATA_KEY).await? { + Ok(Some(WalletData::try_from_dto(dto)?)) } else { - return Ok(Vec::new()); + Ok(None) } - - futures::stream::iter(&self.account_indexes) - .filter_map(|account_index| async { - let account_index = *account_index; - let key = format!("{ACCOUNT_INDEXATION_KEY}{account_index}"); - self.get::(&key).await.transpose() - }) - .map(|res| AccountDetails::try_from_dto(res?)) - .try_collect::>() - .await } - pub(crate) async fn save_account(&mut self, account: &AccountDetails) -> crate::wallet::Result<()> { - // Only add account index if not already present - if !self.account_indexes.contains(account.index()) { - self.account_indexes.push(*account.index()); - } - - self.set(ACCOUNTS_INDEXATION_KEY, &self.account_indexes).await?; - self.set( - &format!("{ACCOUNT_INDEXATION_KEY}{}", account.index()), - &AccountDetailsDto::from(account), - ) - .await - } - - pub(crate) async fn remove_account(&mut self, account_index: u32) -> crate::wallet::Result<()> { - self.delete(&format!("{ACCOUNT_INDEXATION_KEY}{account_index}")).await?; - self.account_indexes.retain(|a| a != &account_index); - self.set(ACCOUNTS_INDEXATION_KEY, &self.account_indexes).await + pub(crate) async fn save_wallet_data(&mut self, wallet_data: &WalletData) -> crate::wallet::Result<()> { + self.set(WALLET_DATA_KEY, &WalletDataDto::from(wallet_data)).await } - pub(crate) async fn set_default_sync_options( - &self, - account_index: u32, - sync_options: &SyncOptions, - ) -> crate::wallet::Result<()> { - let key = format!("{ACCOUNT_INDEXATION_KEY}{account_index}-{ACCOUNT_SYNC_OPTIONS}"); + pub(crate) async fn set_default_sync_options(&self, sync_options: &SyncOptions) -> crate::wallet::Result<()> { + let key = format!("{WALLET_DATA_KEY}-{WALLET_SYNC_OPTIONS}"); self.set(&key, &sync_options).await } - pub(crate) async fn get_default_sync_options( - &self, - account_index: u32, - ) -> crate::wallet::Result> { - let key = format!("{ACCOUNT_INDEXATION_KEY}{account_index}-{ACCOUNT_SYNC_OPTIONS}"); + pub(crate) async fn get_default_sync_options(&self) -> crate::wallet::Result> { + let key = format!("{WALLET_DATA_KEY}-{WALLET_SYNC_OPTIONS}"); self.get(&key).await } } @@ -164,23 +122,19 @@ mod tests { } #[tokio::test] - async fn save_remove_account() { + async fn save_load_wallet_data() { let mut storage_manager = StorageManager::new(Memory::default(), None).await.unwrap(); - assert!(storage_manager.get_accounts().await.unwrap().is_empty()); - - let account_details = AccountDetails::mock(); + assert!(storage_manager.load_wallet_data().await.unwrap().is_none()); - storage_manager.save_account(&account_details).await.unwrap(); - let accounts = storage_manager.get_accounts().await.unwrap(); - assert_eq!(accounts.len(), 1); - assert_eq!(accounts[0].alias(), "Alice"); + let wallet_data = WalletData::mock(); - storage_manager.remove_account(0).await.unwrap(); - assert!(storage_manager.get_accounts().await.unwrap().is_empty()); + storage_manager.save_wallet_data(&wallet_data).await.unwrap(); + let wallet = storage_manager.load_wallet_data().await.unwrap(); + assert!(matches!(wallet, Some(data) if data.alias == Some("Alice".to_string()))); } #[tokio::test] - async fn save_get_wallet_data() { + async fn save_load_wallet_builder() { let storage_manager = StorageManager::new(Memory::default(), None).await.unwrap(); assert!( WalletBuilder::::load(&storage_manager) diff --git a/sdk/src/wallet/storage/participation.rs b/sdk/src/wallet/storage/participation.rs index c5851d4614..1bd07332a1 100644 --- a/sdk/src/wallet/storage/participation.rs +++ b/sdk/src/wallet/storage/participation.rs @@ -11,7 +11,7 @@ use crate::{ block::output::OutputId, }, wallet::{ - account::operations::participation::ParticipationEventWithNodes, + operations::participation::ParticipationEventWithNodes, storage::constants::{PARTICIPATION_CACHED_OUTPUTS, PARTICIPATION_EVENTS}, }, }; @@ -19,40 +19,29 @@ use crate::{ impl StorageManager { pub(crate) async fn insert_participation_event( &self, - account_index: u32, event_with_nodes: ParticipationEventWithNodes, ) -> crate::wallet::Result<()> { log::debug!("insert_participation_event {}", event_with_nodes.id); let mut events = self .storage - .get::>(&format!( - "{PARTICIPATION_EVENTS}{account_index}" - )) + .get::>(&format!("{PARTICIPATION_EVENTS}")) .await? .unwrap_or_default(); events.insert(event_with_nodes.id, event_with_nodes); - self.storage - .set(&format!("{PARTICIPATION_EVENTS}{account_index}"), &events) - .await?; + self.storage.set(&format!("{PARTICIPATION_EVENTS}"), &events).await?; Ok(()) } - pub(crate) async fn remove_participation_event( - &self, - account_index: u32, - id: &ParticipationEventId, - ) -> crate::wallet::Result<()> { + pub(crate) async fn remove_participation_event(&self, id: &ParticipationEventId) -> crate::wallet::Result<()> { log::debug!("remove_participation_event {id}"); let mut events = match self .storage - .get::>(&format!( - "{PARTICIPATION_EVENTS}{account_index}" - )) + .get::>(&format!("{PARTICIPATION_EVENTS}")) .await? { Some(events) => events, @@ -61,38 +50,31 @@ impl StorageManager { events.remove(id); - self.storage - .set(&format!("{PARTICIPATION_EVENTS}{account_index}"), &events) - .await?; + self.storage.set(&format!("{PARTICIPATION_EVENTS}"), &events).await?; Ok(()) } pub(crate) async fn get_participation_events( &self, - account_index: u32, ) -> crate::wallet::Result> { log::debug!("get_participation_events"); Ok(self .storage - .get(&format!("{PARTICIPATION_EVENTS}{account_index}")) + .get(&format!("{PARTICIPATION_EVENTS}")) .await? .unwrap_or_default()) } pub(crate) async fn set_cached_participation_output_status( &self, - account_index: u32, outputs_participation: &HashMap, ) -> crate::wallet::Result<()> { log::debug!("set_cached_participation"); self.storage - .set( - &format!("{PARTICIPATION_CACHED_OUTPUTS}{account_index}"), - outputs_participation, - ) + .set(&format!("{PARTICIPATION_CACHED_OUTPUTS}"), outputs_participation) .await?; Ok(()) @@ -100,13 +82,12 @@ impl StorageManager { pub(crate) async fn get_cached_participation_output_status( &self, - account_index: u32, ) -> crate::wallet::Result> { log::debug!("get_cached_participation"); Ok(self .storage - .get(&format!("{PARTICIPATION_CACHED_OUTPUTS}{account_index}")) + .get(&format!("{PARTICIPATION_CACHED_OUTPUTS}")) .await? .unwrap_or_default()) } @@ -122,16 +103,16 @@ mod tests { #[tokio::test] async fn insert_get_remove_participation_event() { let storage_manager = StorageManager::new(Memory::default(), None).await.unwrap(); - assert!(storage_manager.get_participation_events(0).await.unwrap().is_empty()); + assert!(storage_manager.get_participation_events().await.unwrap().is_empty()); let event_with_nodes = ParticipationEventWithNodes::mock(); let event_with_nodes_id = event_with_nodes.id; storage_manager - .insert_participation_event(0, event_with_nodes.clone()) + .insert_participation_event(event_with_nodes.clone()) .await .unwrap(); - let participation_events = storage_manager.get_participation_events(0).await.unwrap(); + let participation_events = storage_manager.get_participation_events().await.unwrap(); let mut expected = HashMap::new(); expected.insert(event_with_nodes_id, event_with_nodes); @@ -139,10 +120,10 @@ mod tests { assert_eq!(participation_events, expected); storage_manager - .remove_participation_event(0, &event_with_nodes_id) + .remove_participation_event(&event_with_nodes_id) .await .unwrap(); - assert!(storage_manager.get_participation_events(0).await.unwrap().is_empty()); + assert!(storage_manager.get_participation_events().await.unwrap().is_empty()); } #[tokio::test] @@ -150,7 +131,7 @@ mod tests { let storage_manager = StorageManager::new(Memory::default(), None).await.unwrap(); assert!( storage_manager - .get_cached_participation_output_status(0) + .get_cached_participation_output_status() .await .unwrap() .is_empty() @@ -166,12 +147,12 @@ mod tests { .collect::>(); storage_manager - .set_cached_participation_output_status(0, &outputs_participation) + .set_cached_participation_output_status(&outputs_participation) .await .unwrap(); assert_eq!( - storage_manager.get_cached_participation_output_status(0).await.unwrap(), + storage_manager.get_cached_participation_output_status().await.unwrap(), outputs_participation ); } diff --git a/sdk/src/wallet/account/types/address.rs b/sdk/src/wallet/types/address.rs similarity index 100% rename from sdk/src/wallet/account/types/address.rs rename to sdk/src/wallet/types/address.rs diff --git a/sdk/src/wallet/account/types/balance.rs b/sdk/src/wallet/types/balance.rs similarity index 98% rename from sdk/src/wallet/account/types/balance.rs rename to sdk/src/wallet/types/balance.rs index 6709ebc2b2..180ae01bd3 100644 --- a/sdk/src/wallet/account/types/balance.rs +++ b/sdk/src/wallet/types/balance.rs @@ -12,8 +12,8 @@ use crate::{ utils::serde::string, }; -/// The balance of an account, returned from [`crate::wallet::account::Account::sync()`] and -/// [`crate::wallet::account::Account::balance()`]. +/// The balance of the wallet, returned from [`crate::wallet::core::Wallet::sync()`] and +/// [`crate::wallet::core::Wallet::balance()`]. #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, Getters)] #[serde(rename_all = "camelCase")] #[getset(get = "pub")] diff --git a/sdk/src/wallet/account/types/mod.rs b/sdk/src/wallet/types/mod.rs similarity index 75% rename from sdk/src/wallet/account/types/mod.rs rename to sdk/src/wallet/types/mod.rs index fbb9edf36a..4ecef57081 100644 --- a/sdk/src/wallet/account/types/mod.rs +++ b/sdk/src/wallet/types/mod.rs @@ -1,7 +1,7 @@ // Copyright 2021 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -/// Address types used in the account +/// Address types used in the wallet pub(crate) mod address; pub(crate) mod balance; #[cfg(feature = "participation")] @@ -10,7 +10,7 @@ pub mod participation; use std::str::FromStr; use crypto::keys::bip44::Bip44; -use serde::{Deserialize, Deserializer, Serialize}; +use serde::{Deserialize, Serialize}; pub use self::{ address::{AddressWithUnspentOutputs, Bip44Address}, @@ -30,7 +30,7 @@ use crate::{ TryFromDto, }, utils::serde::bip44::option_bip44, - wallet::account::AccountDetails, + wallet::core::WalletData, }; /// An output with metadata @@ -43,7 +43,7 @@ pub struct OutputData { pub output: Output, /// If an output is spent pub is_spent: bool, - /// Associated account address. + /// Associated wallet address. pub address: Address, /// Network ID pub network_id: u64, @@ -55,7 +55,7 @@ pub struct OutputData { impl OutputData { pub fn input_signing_data( &self, - account: &AccountDetails, + wallet_data: &WalletData, slot_index: SlotIndex, ) -> crate::wallet::Result> { let (unlock_address, _unlocked_account_or_nft_address) = @@ -64,17 +64,10 @@ impl OutputData { let chain = if unlock_address == self.address { self.chain } else if let Address::Ed25519(_) = unlock_address { - if let Some(address) = account - .addresses_with_unspent_outputs - .iter() - .find(|a| a.address.inner == unlock_address) - { - Some( - Bip44::new(account.coin_type) - .with_account(account.index) - .with_change(address.internal as _) - .with_address_index(address.key_index), - ) + if wallet_data.address.inner() == &unlock_address { + // TODO #1279: do we need a check to make sure that `wallet_data.address` and `wallet_data.bip_path` are + // never conflicting? + wallet_data.bip_path } else { return Ok(None); } @@ -277,74 +270,3 @@ impl FromStr for OutputKind { Ok(kind) } } - -/// The account identifier. -#[derive(Debug, Clone, Serialize, Eq, PartialEq, Hash)] -#[serde(untagged)] -#[non_exhaustive] -pub enum AccountIdentifier { - /// Account alias as identifier. - Alias(String), - /// An index identifier. - Index(u32), -} - -// Custom deserialize because the index could also be encoded as String -impl<'de> Deserialize<'de> for AccountIdentifier { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - use serde::de::Error; - use serde_json::Value; - let v = Value::deserialize(deserializer)?; - Ok(match v.as_u64() { - Some(number) => { - let index: u32 = - u32::try_from(number).map_err(|_| D::Error::custom("account index is greater than u32::MAX"))?; - Self::Index(index) - } - None => { - let alias_or_index_str = v - .as_str() - .ok_or_else(|| D::Error::custom("account identifier is not a number or string"))?; - Self::from(alias_or_index_str) - } - }) - } -} - -// When the identifier is a string. -impl From<&str> for AccountIdentifier { - fn from(value: &str) -> Self { - u32::from_str(value).map_or_else(|_| Self::Alias(value.to_string()), Self::Index) - } -} - -impl From for AccountIdentifier { - fn from(value: String) -> Self { - Self::from(value.as_str()) - } -} - -impl From<&String> for AccountIdentifier { - fn from(value: &String) -> Self { - Self::from(value.as_str()) - } -} - -// When the identifier is an index. -impl From for AccountIdentifier { - fn from(value: u32) -> Self { - Self::Index(value) - } -} - -impl core::fmt::Display for AccountIdentifier { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::Alias(alias) => alias.fmt(f), - Self::Index(index) => index.fmt(f), - } - } -} diff --git a/sdk/src/wallet/account/types/participation.rs b/sdk/src/wallet/types/participation.rs similarity index 100% rename from sdk/src/wallet/account/types/participation.rs rename to sdk/src/wallet/types/participation.rs diff --git a/sdk/src/wallet/update.rs b/sdk/src/wallet/update.rs new file mode 100644 index 0000000000..77ce6f8622 --- /dev/null +++ b/sdk/src/wallet/update.rs @@ -0,0 +1,208 @@ +// Copyright 2022 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::HashMap; + +use crate::{ + client::secret::SecretManage, + types::{ + api::core::OutputWithMetadataResponse, + block::output::{OutputId, OutputMetadata}, + }, + wallet::{ + types::{InclusionState, OutputData, TransactionWithMetadata}, + Wallet, + }, +}; +#[cfg(feature = "events")] +use crate::{ + types::block::payload::signed_transaction::dto::SignedTransactionPayloadDto, + wallet::{ + events::types::{NewOutputEvent, SpentOutputEvent, TransactionInclusionEvent, WalletEvent}, + types::OutputDataDto, + }, +}; + +impl Wallet +where + crate::wallet::Error: From, + crate::client::Error: From, +{ + /// Set the alias for the wallet. + pub async fn set_alias(&self, alias: &str) -> crate::wallet::Result<()> { + let mut wallet_data = self.data_mut().await; + wallet_data.alias = Some(alias.to_string()); + #[cfg(feature = "storage")] + self.save(Some(&wallet_data)).await?; + Ok(()) + } + + /// Update wallet with newly synced data and emit events for outputs. + pub(crate) async fn update_after_sync( + &self, + unspent_outputs: Vec, + spent_or_unsynced_output_metadata_map: HashMap>, + ) -> crate::wallet::Result<()> { + log::debug!("[SYNC] Update wallet with new synced transactions"); + + let network_id = self.client().get_network_id().await?; + let mut wallet_data = self.data_mut().await; + + // Update spent outputs + for (output_id, output_metadata_response_opt) in spent_or_unsynced_output_metadata_map { + // If we got the output response and it's still unspent, skip it + if let Some(output_metadata_response) = output_metadata_response_opt { + if output_metadata_response.is_spent() { + wallet_data.unspent_outputs.remove(&output_id); + if let Some(output_data) = wallet_data.outputs.get_mut(&output_id) { + output_data.metadata = output_metadata_response; + } + } else { + // not spent, just not synced, skip + continue; + } + } + + if let Some(output) = wallet_data.outputs.get(&output_id) { + // Could also be outputs from other networks after we switched the node, so we check that first + if output.network_id == network_id { + log::debug!("[SYNC] Spent output {}", output_id); + wallet_data.locked_outputs.remove(&output_id); + wallet_data.unspent_outputs.remove(&output_id); + // Update spent data fields + if let Some(output_data) = wallet_data.outputs.get_mut(&output_id) { + output_data.metadata.set_spent(true); + output_data.is_spent = true; + #[cfg(feature = "events")] + { + self.emit(WalletEvent::SpentOutput(Box::new(SpentOutputEvent { + output: OutputDataDto::from(&*output_data), + }))) + .await; + } + } + } + } + } + + // Add new synced outputs + for output_data in unspent_outputs { + // Insert output, if it's unknown emit the NewOutputEvent + if wallet_data + .outputs + .insert(output_data.output_id, output_data.clone()) + .is_none() + { + #[cfg(feature = "events")] + { + let transaction = wallet_data + .incoming_transactions + .get(output_data.output_id.transaction_id()); + self.emit(WalletEvent::NewOutput(Box::new(NewOutputEvent { + output: OutputDataDto::from(&output_data), + transaction: transaction + .as_ref() + .map(|tx| SignedTransactionPayloadDto::from(&tx.payload)), + transaction_inputs: transaction.as_ref().map(|tx| { + tx.inputs + .clone() + .into_iter() + .map(OutputWithMetadataResponse::from) + .collect() + }), + }))) + .await; + } + }; + if !output_data.is_spent { + wallet_data.unspent_outputs.insert(output_data.output_id, output_data); + } + } + + #[cfg(feature = "storage")] + { + log::debug!("[SYNC] storing wallet with new synced data"); + self.save(Some(&wallet_data)).await?; + } + Ok(()) + } + + /// Update wallet with newly synced transactions. + pub(crate) async fn update_with_transactions( + &self, + updated_transactions: Vec, + spent_output_ids: Vec, + output_ids_to_unlock: Vec, + ) -> crate::wallet::Result<()> { + log::debug!("[SYNC] Update wallet with new synced transactions"); + + let mut wallet_data = self.data_mut().await; + + for transaction in updated_transactions { + match transaction.inclusion_state { + InclusionState::Confirmed | InclusionState::Conflicting | InclusionState::UnknownPruned => { + let transaction_id = transaction.payload.transaction().id(); + wallet_data.pending_transactions.remove(&transaction_id); + log::debug!( + "[SYNC] inclusion_state of {transaction_id} changed to {:?}", + transaction.inclusion_state + ); + #[cfg(feature = "events")] + { + self.emit(WalletEvent::TransactionInclusion(TransactionInclusionEvent { + transaction_id, + inclusion_state: transaction.inclusion_state, + })) + .await; + } + } + _ => {} + } + wallet_data + .transactions + .insert(transaction.payload.transaction().id(), transaction.clone()); + } + + for output_to_unlock in &spent_output_ids { + if let Some(output) = wallet_data.outputs.get_mut(output_to_unlock) { + output.is_spent = true; + } + wallet_data.locked_outputs.remove(output_to_unlock); + wallet_data.unspent_outputs.remove(output_to_unlock); + log::debug!("[SYNC] Unlocked spent output {}", output_to_unlock); + } + + for output_to_unlock in &output_ids_to_unlock { + wallet_data.locked_outputs.remove(output_to_unlock); + log::debug!( + "[SYNC] Unlocked unspent output {} because of a conflicting transaction", + output_to_unlock + ); + } + + #[cfg(feature = "storage")] + { + log::debug!("[SYNC] storing wallet with new synced transactions"); + self.save(Some(&wallet_data)).await?; + } + Ok(()) + } + + /// Update the wallet address with a possible new Bech32 HRP and clear the inaccessible incoming transactions. + pub(crate) async fn update_bech32_hrp(&self) -> crate::wallet::Result<()> { + let bech32_hrp = self.client().get_bech32_hrp().await?; + log::debug!("updating wallet data with new bech32 hrp: {}", bech32_hrp); + let mut wallet_data = self.data_mut().await; + wallet_data.address.hrp = bech32_hrp; + + wallet_data.inaccessible_incoming_transactions.clear(); + + #[cfg(feature = "storage")] + { + log::debug!("[save] wallet data with updated bech32 hrp",); + self.save(Some(&wallet_data)).await?; + } + + Ok(()) + } +} diff --git a/sdk/tests/wallet/account_recovery.rs b/sdk/tests/wallet/account_recovery.rs deleted file mode 100644 index 9113d188ba..0000000000 --- a/sdk/tests/wallet/account_recovery.rs +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright 2022 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -// Tests for recovering accounts from mnemonic without a backup - -use std::time::Duration; - -use iota_sdk::{ - client::{ - api::GetAddressesOptions, - constants::SHIMMER_COIN_TYPE, - secret::{mnemonic::MnemonicSecretManager, SecretManager}, - Client, - }, - wallet::Result, -}; -use pretty_assertions::assert_eq; - -use crate::wallet::common::{make_wallet, setup, tear_down}; - -#[ignore] -#[tokio::test] -async fn account_recovery_empty() -> Result<()> { - let storage_path = "test-storage/account_recovery_empty"; - setup(storage_path)?; - - let wallet = make_wallet(storage_path, None, None).await?; - let accounts = wallet.recover_accounts(0, 2, 2, None).await?; - - // accounts should be empty if no account was created before and no account was found with balance - assert_eq!(0, accounts.len()); - tear_down(storage_path) -} - -#[ignore] -#[tokio::test] -async fn account_recovery_existing_accounts() -> Result<()> { - let storage_path = "test-storage/account_recovery_existing_accounts"; - setup(storage_path)?; - - let wallet = make_wallet(storage_path, None, None).await?; - - // create two accounts - wallet.create_account().finish().await?; - wallet.create_account().finish().await?; - - let accounts = wallet.recover_accounts(0, 2, 2, None).await?; - - // accounts should still be ordered - for (index, account) in accounts.iter().enumerate() { - assert_eq!(&(index as u32), account.details().await.index()); - } - // accounts should be 2 because we created 2 accounts before and no new account was found with balance - assert_eq!(2, accounts.len()); - tear_down(storage_path) -} - -#[ignore] -#[tokio::test] -async fn account_recovery_with_balance_and_empty_addresses() -> Result<()> { - let storage_path = "test-storage/account_recovery_with_balance_and_empty_addresses"; - setup(storage_path)?; - - let mnemonic = Client::generate_mnemonic()?; - let client = Client::builder() - .with_node(crate::wallet::common::NODE_LOCAL)? - .finish() - .await?; - - let secret_manager = SecretManager::Mnemonic(MnemonicSecretManager::try_from_mnemonic(mnemonic.clone())?); - - let addresses = secret_manager - .generate_ed25519_addresses( - GetAddressesOptions::from_client(&client) - .await? - .with_coin_type(SHIMMER_COIN_TYPE) - .with_account_index(2) - .with_range(2..3), - ) - .await?; - - // Add funds to the address with account index 2 and address key_index 2, so recover works - iota_sdk::client::request_funds_from_faucet(crate::wallet::common::FAUCET_URL, &addresses[0]).await?; - - // Wait for faucet transaction - tokio::time::sleep(Duration::new(10, 0)).await; - - let wallet = make_wallet(storage_path, Some(mnemonic), None).await?; - - let accounts = wallet.recover_accounts(0, 3, 2, None).await?; - - // accounts should still be ordered - for (index, account) in accounts.iter().enumerate() { - assert_eq!(&(index as u32), account.details().await.index()); - } - // accounts should be 3 because account with index 2 has balance - assert_eq!(3, accounts.len()); - - let account_with_balance = accounts[2].details().await; - // should have 3 addresses - assert_eq!(3, account_with_balance.public_addresses().len()); - tear_down(storage_path) -} diff --git a/sdk/tests/wallet/accounts.rs b/sdk/tests/wallet/accounts.rs deleted file mode 100644 index cfa755691c..0000000000 --- a/sdk/tests/wallet/accounts.rs +++ /dev/null @@ -1,192 +0,0 @@ -// Copyright 2022 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use iota_sdk::wallet::Result; -use pretty_assertions::assert_eq; -#[cfg(feature = "stronghold")] -use { - iota_sdk::client::{ - constants::SHIMMER_COIN_TYPE, - secret::{stronghold::StrongholdSecretManager, SecretManager}, - }, - iota_sdk::wallet::{ClientOptions, Wallet}, -}; - -use crate::wallet::common::{make_wallet, setup, tear_down}; - -#[tokio::test] -async fn account_ordering() -> Result<()> { - let storage_path = "test-storage/account_ordering"; - setup(storage_path)?; - - let wallet = make_wallet(storage_path, None, None).await?; - - for _ in 0..100 { - let _account = wallet.create_account().finish().await?; - } - std::fs::remove_dir_all("test-storage/account_ordering").ok(); - #[cfg(debug_assertions)] - wallet.verify_integrity().await?; - tear_down(storage_path) -} - -#[cfg(feature = "storage")] -#[tokio::test] -async fn remove_latest_account() -> Result<()> { - let storage_path = "test-storage/remove_latest_account"; - setup(storage_path)?; - - let recreated_account_index = { - let wallet = make_wallet(storage_path, None, None).await?; - - // Create two accounts. - let first_account = wallet.create_account().finish().await?; - let _second_account = wallet.create_account().finish().await?; - assert!(wallet.get_accounts().await.unwrap().len() == 2); - - // Remove `second_account`. - wallet - .remove_latest_account() - .await - .expect("cannot remove latest account"); - - // Check if the `second_account` was removed successfully. - let accounts = wallet.get_accounts().await.unwrap(); - assert!(accounts.len() == 1); - assert_eq!( - *accounts.get(0).unwrap().details().await.index(), - *first_account.details().await.index() - ); - - // Remove `first_account`. - wallet - .remove_latest_account() - .await - .expect("cannot remove latest account"); - - // Check if the `first_account` was removed successfully. All accounts should be removed. - let accounts = wallet.get_accounts().await.unwrap(); - assert!(accounts.is_empty()); - - // Try remove another time (even if there is nothing to remove). - wallet - .remove_latest_account() - .await - .expect("cannot remove latest account"); - - let accounts = wallet.get_accounts().await.unwrap(); - assert!(accounts.is_empty()); - - // Recreate a new account and return their index. - - let recreated_account = wallet.create_account().finish().await?; - assert_eq!(wallet.get_accounts().await.unwrap().len(), 1); - let recreated_account_index = *recreated_account.details().await.index(); - - recreated_account_index - }; - - // Restore dropped `Wallet` from above. - let wallet = make_wallet(storage_path, None, None).await?; - - let accounts = wallet.get_accounts().await.unwrap(); - - // Check if accounts with `recreated_account_index` exist. - assert_eq!(accounts.len(), 1); - assert_eq!( - *accounts.get(0).unwrap().details().await.index(), - recreated_account_index - ); - - #[cfg(debug_assertions)] - wallet.verify_integrity().await?; - - tear_down(storage_path) -} - -#[tokio::test] -async fn account_alias_already_exists() -> Result<()> { - let storage_path = "test-storage/account_alias_already_exists"; - setup(storage_path)?; - - let wallet = make_wallet(storage_path, None, None).await?; - let _account = wallet.create_account().with_alias("Alice").finish().await?; - assert!(&wallet.create_account().with_alias("Alice").finish().await.is_err()); - assert!(&wallet.create_account().with_alias("alice").finish().await.is_err()); - assert!(&wallet.create_account().with_alias("ALICE").finish().await.is_err()); - // Other alias works - assert!(&wallet.create_account().with_alias("Bob").finish().await.is_ok()); - - tear_down(storage_path) -} - -#[tokio::test] -async fn account_rename_alias() -> Result<()> { - let storage_path = "test-storage/account_rename_alias"; - setup(storage_path)?; - - let wallet = make_wallet(storage_path, None, None).await?; - let account = wallet.create_account().with_alias("Alice").finish().await?; - - assert_eq!(account.alias().await, "Alice".to_string()); - assert_eq!(account.details().await.alias(), "Alice"); - - // rename account - account.set_alias("Bob").await?; - - assert_eq!(account.alias().await, "Bob".to_string()); - assert_eq!(account.details().await.alias(), "Bob"); - - tear_down(storage_path) -} - -#[tokio::test] -async fn account_first_address_exists() -> Result<()> { - let storage_path = "test-storage/account_first_address_exists"; - setup(storage_path)?; - - let wallet = make_wallet(storage_path, None, None).await?; - let account = wallet.create_account().with_alias("Alice").finish().await?; - - // When the account is generated, the first public address also gets generated and added to it - assert_eq!(account.addresses().await.len(), 1); - // First address is a public address - assert_eq!(account.addresses().await.first().unwrap().internal(), &false); - - tear_down(storage_path) -} - -#[cfg(feature = "stronghold")] -#[tokio::test] -async fn account_creation_stronghold() -> Result<()> { - iota_stronghold::engine::snapshot::try_set_encrypt_work_factor(0).unwrap(); - - let storage_path = "test-storage/account_creation_stronghold"; - setup(storage_path)?; - - let client_options = ClientOptions::new().with_node("http://localhost:14265")?; - let mnemonic = crypto::keys::bip39::Mnemonic::from("inhale gorilla deny three celery song category owner lottery rent author wealth penalty crawl hobby obtain glad warm early rain clutch slab august bleak".to_owned()); - - // Create directory before, because stronghold would panic otherwise - std::fs::create_dir_all(storage_path).ok(); - let stronghold_secret_manager = StrongholdSecretManager::builder() - .password("some_hopefully_secure_password".to_owned()) - .build("test-storage/account_creation_stronghold/test.stronghold")?; - stronghold_secret_manager.store_mnemonic(mnemonic).await?; - let secret_manager = SecretManager::Stronghold(stronghold_secret_manager); - - #[allow(unused_mut)] - let mut wallet_builder = Wallet::builder() - .with_secret_manager(secret_manager) - .with_client_options(client_options) - .with_coin_type(SHIMMER_COIN_TYPE); - #[cfg(feature = "storage")] - { - wallet_builder = wallet_builder.with_storage_path(storage_path); - } - let wallet = wallet_builder.finish().await?; - - let _account = wallet.create_account().finish().await?; - - tear_down(storage_path) -} diff --git a/sdk/tests/wallet/address_generation.rs b/sdk/tests/wallet/address_generation.rs index 7c2258a6b7..eccd3aa430 100644 --- a/sdk/tests/wallet/address_generation.rs +++ b/sdk/tests/wallet/address_generation.rs @@ -3,6 +3,7 @@ #[cfg(feature = "stronghold")] use crypto::keys::bip39::Mnemonic; +use crypto::keys::bip44::Bip44; #[cfg(feature = "stronghold")] use iota_sdk::client::secret::stronghold::StrongholdSecretManager; #[cfg(feature = "ledger_nano")] @@ -34,7 +35,7 @@ async fn wallet_address_generation_mnemonic() -> Result<()> { let mut wallet_builder = Wallet::builder() .with_secret_manager(SecretManager::Mnemonic(secret_manager)) .with_client_options(client_options) - .with_coin_type(IOTA_COIN_TYPE); + .with_bip_path(Bip44::new(IOTA_COIN_TYPE)); #[cfg(feature = "storage")] { @@ -73,7 +74,7 @@ async fn wallet_address_generation_stronghold() -> Result<()> { let mut wallet_builder = Wallet::builder() .with_secret_manager(SecretManager::Stronghold(secret_manager)) .with_client_options(client_options) - .with_coin_type(IOTA_COIN_TYPE); + .with_bip_path(Bip44::new(IOTA_COIN_TYPE)); #[cfg(feature = "storage")] { wallet_builder = wallet_builder.with_storage_path(storage_path); @@ -106,7 +107,7 @@ async fn wallet_address_generation_ledger() -> Result<()> { let mut wallet_builder = Wallet::builder() .with_secret_manager(SecretManager::LedgerNano(secret_manager)) .with_client_options(client_options) - .with_coin_type(IOTA_COIN_TYPE); + .with_bip_path(Bip44::new(IOTA_COIN_TYPE)); #[cfg(feature = "storage")] { @@ -129,7 +130,7 @@ async fn wallet_address_generation_ledger() -> Result<()> { wallet .listen([WalletEventType::LedgerAddressGeneration], move |event| { - if let WalletEvent::LedgerAddressGeneration(address) = &event.event { + if let WalletEvent::LedgerAddressGeneration(address) = event { sender .try_send(address.address.clone()) .expect("too many LedgerAddressGeneration events"); @@ -176,30 +177,30 @@ async fn wallet_address_generation_ledger() -> Result<()> { tear_down(storage_path) } -#[tokio::test] -async fn wallet_address_generation_placeholder() -> Result<()> { - let storage_path = "test-storage/wallet_address_generation_placeholder"; - setup(storage_path)?; - - let client_options = ClientOptions::new().with_node(NODE_LOCAL)?; - - #[allow(unused_mut)] - let mut wallet_builder = Wallet::builder() - .with_secret_manager(SecretManager::Placeholder) - .with_client_options(client_options) - .with_coin_type(IOTA_COIN_TYPE); - - #[cfg(feature = "storage")] - { - wallet_builder = wallet_builder.with_storage_path(storage_path); - } - let wallet = wallet_builder.finish().await?; - - if let Err(Error::Client(error)) = wallet.generate_ed25519_address(0, 0, None).await { - assert!(matches!(*error, ClientError::PlaceholderSecretManager)) - } else { - panic!("expected PlaceholderSecretManager") - } - - tear_down(storage_path) -} +// #[tokio::test] +// async fn wallet_address_generation_placeholder() -> Result<()> { +// let storage_path = "test-storage/wallet_address_generation_placeholder"; +// setup(storage_path)?; + +// let client_options = ClientOptions::new().with_node(NODE_LOCAL)?; + +// #[allow(unused_mut)] +// let mut wallet_builder = Wallet::builder() +// .with_secret_manager(SecretManager::Placeholder) +// .with_client_options(client_options) +// .with_bip_path(Bip44::new(IOTA_COIN_TYPE)); + +// #[cfg(feature = "storage")] +// { +// wallet_builder = wallet_builder.with_storage_path(storage_path); +// } +// let wallet = wallet_builder.finish().await?; + +// if let Err(Error::Client(error)) = wallet.generate_ed25519_address(0, 0, None).await { +// assert!(matches!(*error, ClientError::PlaceholderSecretManager)) +// } else { +// panic!("expected PlaceholderSecretManager") +// } + +// tear_down(storage_path) +// } diff --git a/sdk/tests/wallet/backup_restore.rs b/sdk/tests/wallet/backup_restore.rs index e4b283e82e..7fd0ceec97 100644 --- a/sdk/tests/wallet/backup_restore.rs +++ b/sdk/tests/wallet/backup_restore.rs @@ -1,571 +1,577 @@ // Copyright 2022 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use std::path::PathBuf; - -use crypto::keys::bip39::Mnemonic; -use iota_sdk::{ - client::{ - constants::{IOTA_COIN_TYPE, SHIMMER_COIN_TYPE}, - node_manager::node::{Node, NodeDto}, - secret::{mnemonic::MnemonicSecretManager, stronghold::StrongholdSecretManager, SecretManager}, - }, - wallet::{ClientOptions, Result, Wallet}, -}; -use pretty_assertions::assert_eq; -use url::Url; - -use crate::wallet::common::{setup, tear_down, NODE_LOCAL, NODE_OTHER}; - -// Backup and restore with Stronghold -#[tokio::test] -async fn backup_and_restore() -> Result<()> { - iota_stronghold::engine::snapshot::try_set_encrypt_work_factor(0).unwrap(); - - let storage_path = "test-storage/backup_and_restore"; - setup(storage_path)?; - - let client_options = ClientOptions::new().with_node(NODE_LOCAL)?; - - let stronghold_password = "some_hopefully_secure_password".to_owned(); - - // Create directory if not existing, because stronghold panics otherwise - std::fs::create_dir_all(storage_path).ok(); - let stronghold = StrongholdSecretManager::builder() - .password(stronghold_password.clone()) - .build("test-storage/backup_and_restore/1.stronghold")?; - - stronghold.store_mnemonic(Mnemonic::from("inhale gorilla deny three celery song category owner lottery rent author wealth penalty crawl hobby obtain glad warm early rain clutch slab august bleak".to_string())).await.unwrap(); - - let wallet = Wallet::builder() - .with_secret_manager(SecretManager::Stronghold(stronghold)) - .with_client_options(client_options.clone()) - .with_coin_type(SHIMMER_COIN_TYPE) - .with_storage_path("test-storage/backup_and_restore/1") - .finish() - .await?; - - let account = wallet.create_account().with_alias("Alice").finish().await?; - - wallet - .backup( - PathBuf::from("test-storage/backup_and_restore/backup.stronghold"), - stronghold_password.clone(), - ) - .await?; - - // restore from backup - - let stronghold = StrongholdSecretManager::builder().build("test-storage/backup_and_restore/2.stronghold")?; - - let restore_wallet = Wallet::builder() - .with_storage_path("test-storage/backup_and_restore/2") - .with_secret_manager(SecretManager::Stronghold(stronghold)) - .with_client_options(ClientOptions::new().with_node(NODE_OTHER)?) - // Build with a different coin type, to check if it gets replaced by the one from the backup - .with_coin_type(IOTA_COIN_TYPE) - .finish() - .await?; - - // Wrong password fails - restore_wallet - .restore_backup( - PathBuf::from("test-storage/backup_and_restore/backup.stronghold"), - "wrong password".to_owned(), - None, - None, - ) - .await - .unwrap_err(); - - // Correct password works, even after trying with a wrong one before - restore_wallet - .restore_backup( - PathBuf::from("test-storage/backup_and_restore/backup.stronghold"), - stronghold_password, - None, - None, - ) - .await?; - - // Validate restored data - - // Restored coin type is used - let new_account = restore_wallet.create_account().finish().await?; - assert_eq!(new_account.details().await.coin_type(), &SHIMMER_COIN_TYPE); - - // compare restored client options - let client_options = restore_wallet.client_options().await; - let node_dto = NodeDto::Node(Node::from(Url::parse(NODE_LOCAL).unwrap())); - assert!(client_options.node_manager_builder.nodes.contains(&node_dto)); - - // Get account - let recovered_account = restore_wallet.get_account("Alice").await?; - assert_eq!(account.addresses().await, recovered_account.addresses().await); - - // secret manager is the same - assert_eq!( - account.generate_ed25519_addresses(1, None).await?, - recovered_account.generate_ed25519_addresses(1, None).await? - ); - tear_down(storage_path) -} - -// Backup and restore with Stronghold and MnemonicSecretManager -#[tokio::test] -async fn backup_and_restore_mnemonic_secret_manager() -> Result<()> { - iota_stronghold::engine::snapshot::try_set_encrypt_work_factor(0).unwrap(); - - let storage_path = "test-storage/backup_and_restore_mnemonic_secret_manager"; - setup(storage_path)?; - - let client_options = ClientOptions::new().with_node(NODE_LOCAL)?; - - let secret_manager = MnemonicSecretManager::try_from_mnemonic( - "inhale gorilla deny three celery song category owner lottery rent author wealth penalty crawl hobby obtain glad warm early rain clutch slab august bleak".to_owned(), - )?; - - let wallet = Wallet::builder() - .with_secret_manager(SecretManager::Mnemonic(secret_manager)) - .with_client_options(client_options.clone()) - .with_coin_type(SHIMMER_COIN_TYPE) - .with_storage_path("test-storage/backup_and_restore_mnemonic_secret_manager/1") - .finish() - .await?; - - let account = wallet.create_account().with_alias("Alice").finish().await?; - - let stronghold_password = "some_hopefully_secure_password".to_owned(); - - // Create directory if not existing, because stronghold panics otherwise - std::fs::create_dir_all(storage_path).ok(); - wallet - .backup( - PathBuf::from("test-storage/backup_and_restore_mnemonic_secret_manager/backup.stronghold"), - stronghold_password.clone(), - ) - .await?; - - // restore from backup - - let secret_manager = MnemonicSecretManager::try_from_mnemonic( - "inhale gorilla deny three celery song category owner lottery rent author wealth penalty crawl hobby obtain glad warm early rain clutch slab august bleak".to_owned(), - )?; - - let restore_wallet = Wallet::builder() - .with_storage_path("test-storage/backup_and_restore_mnemonic_secret_manager/2") - .with_secret_manager(SecretManager::Mnemonic(secret_manager)) - // Build with a different coin type, to check if it gets replaced by the one from the backup - .with_coin_type(IOTA_COIN_TYPE) - .with_client_options(ClientOptions::new().with_node(NODE_OTHER)?) - .finish() - .await?; - - restore_wallet - .restore_backup( - PathBuf::from("test-storage/backup_and_restore_mnemonic_secret_manager/backup.stronghold"), - stronghold_password, - None, - None, - ) - .await?; - - // Validate restored data - - // Restored coin type is used - let new_account = restore_wallet.create_account().finish().await?; - assert_eq!(new_account.details().await.coin_type(), &SHIMMER_COIN_TYPE); - - // compare restored client options - let client_options = restore_wallet.client_options().await; - let node_dto = NodeDto::Node(Node::from(Url::parse(NODE_LOCAL).unwrap())); - assert!(client_options.node_manager_builder.nodes.contains(&node_dto)); - - // Get account - let recovered_account = restore_wallet.get_account("Alice").await?; - assert_eq!(account.addresses().await, recovered_account.addresses().await); - - // secret manager is the same - assert_eq!( - account.generate_ed25519_addresses(1, None).await?, - recovered_account.generate_ed25519_addresses(1, None).await? - ); - tear_down(storage_path) -} - -// Backup and restore with Stronghold -#[tokio::test] -async fn backup_and_restore_different_coin_type() -> Result<()> { - iota_stronghold::engine::snapshot::try_set_encrypt_work_factor(0).unwrap(); - - let storage_path = "test-storage/backup_and_restore_different_coin_type"; - setup(storage_path)?; - - let client_options = ClientOptions::new().with_node(NODE_LOCAL)?; - - let stronghold_password = "some_hopefully_secure_password".to_owned(); - - // Create directory if not existing, because stronghold panics otherwise - std::fs::create_dir_all(storage_path).ok(); - let stronghold = StrongholdSecretManager::builder() - .password(stronghold_password.clone()) - .build("test-storage/backup_and_restore_different_coin_type/1.stronghold")?; - - stronghold.store_mnemonic(Mnemonic::from("inhale gorilla deny three celery song category owner lottery rent author wealth penalty crawl hobby obtain glad warm early rain clutch slab august bleak".to_string())).await.unwrap(); - - let wallet = Wallet::builder() - .with_secret_manager(SecretManager::Stronghold(stronghold)) - .with_client_options(client_options.clone()) - .with_coin_type(SHIMMER_COIN_TYPE) - .with_storage_path("test-storage/backup_and_restore_different_coin_type/1") - .finish() - .await?; - - // Create one account - wallet.create_account().with_alias("Alice").finish().await?; - - wallet - .backup( - PathBuf::from("test-storage/backup_and_restore_different_coin_type/backup.stronghold"), - stronghold_password.clone(), - ) - .await?; - - // restore from backup - - let stronghold = - StrongholdSecretManager::builder().build("test-storage/backup_and_restore_different_coin_type/2.stronghold")?; - - let restore_wallet = Wallet::builder() - .with_storage_path("test-storage/backup_and_restore_different_coin_type/2") - .with_secret_manager(SecretManager::Stronghold(stronghold)) - .with_client_options(ClientOptions::new().with_node(NODE_OTHER)?) - // Build with a different coin type, to check if it gets replaced by the one from the backup - .with_coin_type(IOTA_COIN_TYPE) - .finish() - .await?; - - // restore with ignore_if_coin_type_mismatch: Some(true) to not overwrite the coin type - restore_wallet - .restore_backup( - PathBuf::from("test-storage/backup_and_restore_different_coin_type/backup.stronghold"), - stronghold_password, - Some(true), - None, - ) - .await?; - - // Validate restored data - - // No accounts restored, because the coin type was different - assert!(restore_wallet.get_accounts().await?.is_empty()); - - // Restored coin type is not used and it's still the same one - let new_account = restore_wallet.create_account().finish().await?; - assert_eq!(new_account.details().await.coin_type(), &IOTA_COIN_TYPE); - // secret manager is the same - assert_eq!( - new_account.first_address_bech32().await, - "smr1qrpwecegav7eh0z363ca69laxej64rrt4e3u0rtycyuh0mam3vq3ulygj9p" - ); - - // compare restored client options - let client_options = restore_wallet.client_options().await; - let node_dto = NodeDto::Node(Node::from(Url::parse(NODE_OTHER).unwrap())); - assert!(client_options.node_manager_builder.nodes.contains(&node_dto)); - - tear_down(storage_path) -} - -// Backup and restore with Stronghold -#[tokio::test] -async fn backup_and_restore_same_coin_type() -> Result<()> { - iota_stronghold::engine::snapshot::try_set_encrypt_work_factor(0).unwrap(); - - let storage_path = "test-storage/backup_and_restore_same_coin_type"; - setup(storage_path)?; - - let client_options = ClientOptions::new().with_node(NODE_LOCAL)?; - - let stronghold_password = "some_hopefully_secure_password".to_owned(); - - // Create directory if not existing, because stronghold panics otherwise - std::fs::create_dir_all(storage_path).ok(); - let stronghold = StrongholdSecretManager::builder() - .password(stronghold_password.clone()) - .build("test-storage/backup_and_restore_same_coin_type/1.stronghold")?; - - stronghold.store_mnemonic(Mnemonic::from("inhale gorilla deny three celery song category owner lottery rent author wealth penalty crawl hobby obtain glad warm early rain clutch slab august bleak".to_string())).await.unwrap(); - - let wallet = Wallet::builder() - .with_secret_manager(SecretManager::Stronghold(stronghold)) - .with_client_options(client_options.clone()) - .with_coin_type(SHIMMER_COIN_TYPE) - .with_storage_path("test-storage/backup_and_restore_same_coin_type/1") - .finish() - .await?; - - // Create one account - let account_before_backup = wallet.create_account().with_alias("Alice").finish().await?; - - wallet - .backup( - PathBuf::from("test-storage/backup_and_restore_same_coin_type/backup.stronghold"), - stronghold_password.clone(), - ) - .await?; - - // restore from backup - - let stronghold = - StrongholdSecretManager::builder().build("test-storage/backup_and_restore_same_coin_type/2.stronghold")?; - - let restore_wallet = Wallet::builder() - .with_storage_path("test-storage/backup_and_restore_same_coin_type/2") - .with_secret_manager(SecretManager::Stronghold(stronghold)) - .with_client_options(ClientOptions::new().with_node(NODE_OTHER)?) - // Build with same coin type - .with_coin_type(SHIMMER_COIN_TYPE) - .finish() - .await?; - - // restore with ignore_if_coin_type_mismatch: Some(true) to not overwrite the coin type - restore_wallet - .restore_backup( - PathBuf::from("test-storage/backup_and_restore_same_coin_type/backup.stronghold"), - stronghold_password, - Some(true), - None, - ) - .await?; - - // Validate restored data - - // The account is restored, because the coin type is the same - let restored_accounts = restore_wallet.get_accounts().await?; - assert_eq!(restored_accounts.len(), 1); - - // addresses are still there - assert_eq!( - restored_accounts[0].addresses().await, - account_before_backup.addresses().await - ); - - // compare client options, they are not restored - let client_options = restore_wallet.client_options().await; - let node_dto = NodeDto::Node(Node::from(Url::parse(NODE_OTHER).unwrap())); - assert!(client_options.node_manager_builder.nodes.contains(&node_dto)); - - tear_down(storage_path) -} - -// Backup and restore with Stronghold -#[tokio::test] -async fn backup_and_restore_different_coin_type_dont_ignore() -> Result<()> { - iota_stronghold::engine::snapshot::try_set_encrypt_work_factor(0).unwrap(); - - let storage_path = "test-storage/backup_and_restore_different_coin_type_dont_ignore"; - setup(storage_path)?; - - let client_options = ClientOptions::new().with_node(NODE_OTHER)?; - - let stronghold_password = "some_hopefully_secure_password".to_owned(); - - // Create directory if not existing, because stronghold panics otherwise - std::fs::create_dir_all(storage_path).ok(); - let stronghold = StrongholdSecretManager::builder() - .password(stronghold_password.clone()) - .build("test-storage/backup_and_restore_different_coin_type_dont_ignore/1.stronghold")?; - - stronghold.store_mnemonic(Mnemonic::from("inhale gorilla deny three celery song category owner lottery rent author wealth penalty crawl hobby obtain glad warm early rain clutch slab august bleak".to_string())).await.unwrap(); - - let wallet = Wallet::builder() - .with_secret_manager(SecretManager::Stronghold(stronghold)) - .with_client_options(client_options.clone()) - .with_coin_type(SHIMMER_COIN_TYPE) - .with_storage_path("test-storage/backup_and_restore_different_coin_type_dont_ignore/1") - .finish() - .await?; - - // Create one account - let account = wallet.create_account().with_alias("Alice").finish().await?; - - wallet - .backup( - PathBuf::from("test-storage/backup_and_restore_different_coin_type_dont_ignore/backup.stronghold"), - stronghold_password.clone(), - ) - .await?; - - // restore from backup - - let stronghold = StrongholdSecretManager::builder() - .build("test-storage/backup_and_restore_different_coin_type_dont_ignore/2.stronghold")?; - - let restore_wallet = Wallet::builder() - .with_storage_path("test-storage/backup_and_restore_different_coin_type_dont_ignore/2") - .with_secret_manager(SecretManager::Stronghold(stronghold)) - .with_client_options(ClientOptions::new().with_node(NODE_LOCAL)?) - // Build with a different coin type, to check if it gets replaced by the one from the backup - .with_coin_type(IOTA_COIN_TYPE) - .finish() - .await?; - - // restore with ignore_if_coin_type_mismatch: Some(true) to not overwrite the coin type - restore_wallet - .restore_backup( - PathBuf::from("test-storage/backup_and_restore_different_coin_type_dont_ignore/backup.stronghold"), - stronghold_password, - Some(false), - None, - ) - .await?; - - // Validate restored data - - // No accounts restored, because the coin type was different - let restored_account = restore_wallet.get_account("Alice").await?; - assert_eq!( - account.first_address_bech32().await, - restored_account.first_address_bech32().await, - ); - - // Restored coin type is used - let new_account = restore_wallet.create_account().finish().await?; - assert_eq!(new_account.details().await.coin_type(), &SHIMMER_COIN_TYPE); - // secret manager is restored - assert_eq!( - new_account.first_address_bech32().await, - "smr1qzvjvjyqxgfx4f0m3xhn2rj24e03dwsmjz082735y3wx88v2gudu2afedhu" - ); - - // compare client options, they are not restored - let client_options = restore_wallet.client_options().await; - let node_dto = NodeDto::Node(Node::from(Url::parse(NODE_LOCAL).unwrap())); - assert!(client_options.node_manager_builder.nodes.contains(&node_dto)); - - tear_down(storage_path) -} - -#[tokio::test] -async fn backup_and_restore_bech32_hrp_mismatch() -> Result<()> { - iota_stronghold::engine::snapshot::try_set_encrypt_work_factor(0).unwrap(); - - let storage_path = "test-storage/backup_and_restore_bech32_hrp_mismatch"; - setup(storage_path)?; - - let client_options = ClientOptions::new().with_node(NODE_LOCAL)?; - - let stronghold_password = "some_hopefully_secure_password".to_owned(); - - // Create directory if not existing, because stronghold panics otherwise - std::fs::create_dir_all(storage_path).ok(); - let stronghold = StrongholdSecretManager::builder() - .password(stronghold_password.clone()) - .build("test-storage/backup_and_restore_bech32_hrp_mismatch/1.stronghold")?; - - stronghold.store_mnemonic(Mnemonic::from("inhale gorilla deny three celery song category owner lottery rent author wealth penalty crawl hobby obtain glad warm early rain clutch slab august bleak".to_string())).await.unwrap(); - - let wallet = Wallet::builder() - .with_secret_manager(SecretManager::Stronghold(stronghold)) - .with_client_options(client_options.clone()) - .with_coin_type(SHIMMER_COIN_TYPE) - .with_storage_path("test-storage/backup_and_restore_bech32_hrp_mismatch/1") - .finish() - .await?; - - let account = wallet.create_account().with_alias("Alice").finish().await?; - - wallet - .backup( - PathBuf::from("test-storage/backup_and_restore_bech32_hrp_mismatch/backup.stronghold"), - stronghold_password.clone(), - ) - .await?; - - // restore from backup - - let stronghold = - StrongholdSecretManager::builder().build("test-storage/backup_and_restore_bech32_hrp_mismatch/2.stronghold")?; - - let restore_wallet = Wallet::builder() - .with_storage_path("test-storage/backup_and_restore_bech32_hrp_mismatch/2") - .with_secret_manager(SecretManager::Stronghold(stronghold)) - .with_client_options(ClientOptions::new().with_node(NODE_OTHER)?) - // Build with a different coin type, to check if it gets replaced by the one from the backup - .with_coin_type(IOTA_COIN_TYPE) - .finish() - .await?; - - restore_wallet - .restore_backup( - PathBuf::from("test-storage/backup_and_restore_bech32_hrp_mismatch/backup.stronghold"), - stronghold_password, - None, - Some(iota_sdk::types::block::address::Hrp::from_str_unchecked("otherhrp")), - ) - .await?; - - // Validate restored data - - // compare restored client options - let client_options = restore_wallet.client_options().await; - let node_dto = NodeDto::Node(Node::from(Url::parse(NODE_LOCAL).unwrap())); - assert!(client_options.node_manager_builder.nodes.contains(&node_dto)); - - // No restored accounts because the bech32 hrp was different - let restored_accounts = restore_wallet.get_accounts().await?; - assert!(restored_accounts.is_empty()); - - // Restored coin type is used - let new_account = restore_wallet.create_account().finish().await?; - assert_eq!(new_account.details().await.coin_type(), &SHIMMER_COIN_TYPE); - - // secret manager is the same - assert_eq!( - account.generate_ed25519_addresses(1, None).await?, - new_account.generate_ed25519_addresses(1, None).await? - ); - tear_down(storage_path) -} - -// Restore a Stronghold snapshot without secret manager data -#[tokio::test] -async fn restore_no_secret_manager_data() -> Result<()> { - iota_stronghold::engine::snapshot::try_set_encrypt_work_factor(0).unwrap(); - - let storage_path = "test-storage/restore_no_secret_manager_data"; - setup(storage_path)?; - - let stronghold = StrongholdSecretManager::builder().build(storage_path.to_string() + "/wallet.stronghold")?; - - let restore_wallet = Wallet::builder() - .with_storage_path(storage_path) - .with_secret_manager(SecretManager::Stronghold(stronghold)) - .with_client_options(ClientOptions::new().with_node(NODE_LOCAL)?) - .with_coin_type(IOTA_COIN_TYPE) - .finish() - .await?; - - let stronghold_password = "some_hopefully_secure_password".to_owned(); - - restore_wallet - .restore_backup( - PathBuf::from("./tests/wallet/fixtures/no_secret_manager_data.stronghold"), - stronghold_password.clone(), - None, - None, - ) - .await?; - - restore_wallet.set_stronghold_password(stronghold_password).await?; - - // Backup is restored also without any secret manager data inside and the seed is available - // Backup was created with mnemonic: "inhale gorilla deny three celery song category owner lottery rent author - // wealth penalty crawl hobby obtain glad warm early rain clutch slab august bleak" - assert_eq!( - restore_wallet.generate_ed25519_address(0, 0, None).await?.to_string(), - "0xc2ece328eb3d9bbc51d471dd17fd3665aa8c6bae63c78d64c13977efbb8b011e" - ); - tear_down(storage_path) -} +// use std::path::PathBuf; + +// use crypto::keys::bip39::Mnemonic; +// use iota_sdk::{ +// client::{ +// api::GetAddressesOptions, +// constants::{IOTA_COIN_TYPE, SHIMMER_COIN_TYPE}, +// node_manager::node::{Node, NodeDto}, +// secret::{mnemonic::MnemonicSecretManager, stronghold::StrongholdSecretManager, SecretManager}, +// }, +// crypto::keys::bip44::Bip44, +// wallet::{ClientOptions, Result, Wallet}, +// }; +// use pretty_assertions::assert_eq; +// use url::Url; + +// use crate::wallet::common::{setup, tear_down, NODE_LOCAL, NODE_OTHER}; + +// // Backup and restore with Stronghold +// #[tokio::test] +// async fn backup_and_restore() -> Result<()> { +// iota_stronghold::engine::snapshot::try_set_encrypt_work_factor(0).unwrap(); + +// let storage_path = "test-storage/backup_and_restore"; +// setup(storage_path)?; + +// let client_options = ClientOptions::new().with_node(NODE_LOCAL)?; + +// let stronghold_password = "some_hopefully_secure_password".to_owned(); + +// // Create directory if not existing, because stronghold panics otherwise +// std::fs::create_dir_all(storage_path).ok(); +// let stronghold = StrongholdSecretManager::builder() +// .password(stronghold_password.clone()) +// .build("test-storage/backup_and_restore/1.stronghold")?; + +// stronghold.store_mnemonic(Mnemonic::from("inhale gorilla deny three celery song category owner lottery rent author wealth penalty crawl hobby obtain glad warm early rain clutch slab august bleak".to_string())).await.unwrap(); + +// let wallet = Wallet::builder() +// .with_secret_manager(SecretManager::Stronghold(stronghold)) +// .with_client_options(client_options.clone()) +// .with_bip_path(Bip44::new(SHIMMER_COIN_TYPE)) +// .with_storage_path("test-storage/backup_and_restore/1") +// .finish() +// .await?; + +// wallet +// .backup( +// PathBuf::from("test-storage/backup_and_restore/backup.stronghold"), +// stronghold_password.clone(), +// ) +// .await?; + +// // restore from backup + +// let stronghold = StrongholdSecretManager::builder().build("test-storage/backup_and_restore/2.stronghold")?; + +// let restored_wallet = Wallet::builder() +// .with_storage_path("test-storage/backup_and_restore/2") +// .with_secret_manager(SecretManager::Stronghold(stronghold)) +// .with_client_options(ClientOptions::new().with_node(NODE_OTHER)?) +// // Build with a different coin type, to check if it gets replaced by the one from the backup +// .with_bip_path(Bip44::new(IOTA_COIN_TYPE)) +// .finish() +// .await?; + +// // Wrong password fails +// restored_wallet +// .restore_backup( +// PathBuf::from("test-storage/backup_and_restore/backup.stronghold"), +// "wrong password".to_owned(), +// None, +// None, +// ) +// .await +// .unwrap_err(); + +// // Correct password works, even after trying with a wrong one before +// restored_wallet +// .restore_backup( +// PathBuf::from("test-storage/backup_and_restore/backup.stronghold"), +// stronghold_password, +// None, +// None, +// ) +// .await?; + +// // Validate restored data + +// // Restored coin type is used +// assert_eq!(restored_wallet.bip_path().await.unwrap().coin_type, SHIMMER_COIN_TYPE); + +// // compare restored client options +// let client_options = restored_wallet.client_options().await; +// let node_dto = NodeDto::Node(Node::from(Url::parse(NODE_LOCAL).unwrap())); +// assert!(client_options.node_manager_builder.nodes.contains(&node_dto)); + +// assert_eq!(wallet.address().await, restored_wallet.address().await); + +// // secret manager is the same +// assert_eq!( +// wallet +// .get_secret_manager() +// .read() +// .await +// .generate_ed25519_addresses(GetAddressesOptions { +// coin_type: SHIMMER_COIN_TYPE, +// range: 0..1, +// ..Default::default() +// }) +// .await?, +// restored_wallet +// .get_secret_manager() +// .read() +// .await +// .generate_ed25519_addresses(GetAddressesOptions { +// coin_type: SHIMMER_COIN_TYPE, +// range: 0..1, +// ..Default::default() +// }) +// .await?, +// ); +// tear_down(storage_path) +// } + +// // Backup and restore with Stronghold and MnemonicSecretManager +// #[tokio::test] +// async fn backup_and_restore_mnemonic_secret_manager() -> Result<()> { +// iota_stronghold::engine::snapshot::try_set_encrypt_work_factor(0).unwrap(); + +// let storage_path = "test-storage/backup_and_restore_mnemonic_secret_manager"; +// setup(storage_path)?; + +// let client_options = ClientOptions::new().with_node(NODE_LOCAL)?; + +// let secret_manager = MnemonicSecretManager::try_from_mnemonic( +// "inhale gorilla deny three celery song category owner lottery rent author wealth penalty crawl hobby obtain +// glad warm early rain clutch slab august bleak".to_owned(), )?; + +// let wallet = Wallet::builder() +// .with_secret_manager(SecretManager::Mnemonic(secret_manager)) +// .with_client_options(client_options.clone()) +// .with_coin_type(SHIMMER_COIN_TYPE) +// .with_storage_path("test-storage/backup_and_restore_mnemonic_secret_manager/1") +// .finish() +// .await?; + +// let stronghold_password = "some_hopefully_secure_password".to_owned(); + +// // Create directory if not existing, because stronghold panics otherwise +// std::fs::create_dir_all(storage_path).ok(); +// wallet +// .backup( +// PathBuf::from("test-storage/backup_and_restore_mnemonic_secret_manager/backup.stronghold"), +// stronghold_password.clone(), +// ) +// .await?; + +// // restore from backup + +// let secret_manager = MnemonicSecretManager::try_from_mnemonic( +// "inhale gorilla deny three celery song category owner lottery rent author wealth penalty crawl hobby obtain +// glad warm early rain clutch slab august bleak".to_owned(), )?; + +// let restore_wallet = Wallet::builder() +// .with_storage_path("test-storage/backup_and_restore_mnemonic_secret_manager/2") +// .with_secret_manager(SecretManager::Mnemonic(secret_manager)) +// // Build with a different coin type, to check if it gets replaced by the one from the backup +// .with_coin_type(IOTA_COIN_TYPE) +// .with_client_options(ClientOptions::new().with_node(NODE_OTHER)?) +// .finish() +// .await?; + +// restore_wallet +// .restore_backup( +// PathBuf::from("test-storage/backup_and_restore_mnemonic_secret_manager/backup.stronghold"), +// stronghold_password, +// None, +// None, +// ) +// .await?; + +// // Validate restored data + +// // Restored coin type is used +// let new_wallet = restore_wallet; +// assert_eq!(new_wallet.data().await.coin_type(), &SHIMMER_COIN_TYPE); + +// // compare restored client options +// let client_options = restore_wallet.client_options().await; +// let node_dto = NodeDto::Node(Node::from(Url::parse(NODE_LOCAL).unwrap())); +// assert!(client_options.node_manager_builder.nodes.contains(&node_dto)); + +// // Get wallet +// let recovered_wallet = restore_wallet; +// assert_eq!(wallet.address().await, recovered_wallet.address().await); + +// // secret manager is the same +// assert_eq!( +// wallet.generate_ed25519_addresses(1, None).await?, +// recovered_wallet.generate_ed25519_addresses(1, None).await? +// ); +// tear_down(storage_path) +// } + +// // Backup and restore with Stronghold +// #[tokio::test] +// async fn backup_and_restore_different_coin_type() -> Result<()> { +// iota_stronghold::engine::snapshot::try_set_encrypt_work_factor(0).unwrap(); + +// let storage_path = "test-storage/backup_and_restore_different_coin_type"; +// setup(storage_path)?; + +// let client_options = ClientOptions::new().with_node(NODE_LOCAL)?; + +// let stronghold_password = "some_hopefully_secure_password".to_owned(); + +// // Create directory if not existing, because stronghold panics otherwise +// std::fs::create_dir_all(storage_path).ok(); +// let stronghold = StrongholdSecretManager::builder() +// .password(stronghold_password.clone()) +// .build("test-storage/backup_and_restore_different_coin_type/1.stronghold")?; + +// stronghold.store_mnemonic(Mnemonic::from("inhale gorilla deny three celery song category owner lottery rent author wealth penalty crawl hobby obtain glad warm early rain clutch slab august bleak".to_string())).await.unwrap(); + +// let wallet = Wallet::builder() +// .with_secret_manager(SecretManager::Stronghold(stronghold)) +// .with_client_options(client_options.clone()) +// .with_coin_type(SHIMMER_COIN_TYPE) +// .with_storage_path("test-storage/backup_and_restore_different_coin_type/1") +// .finish() +// .await?; + +// wallet +// .backup( +// PathBuf::from("test-storage/backup_and_restore_different_coin_type/backup.stronghold"), +// stronghold_password.clone(), +// ) +// .await?; + +// // restore from backup + +// let stronghold = +// StrongholdSecretManager::builder().build("test-storage/backup_and_restore_different_coin_type/2.stronghold")? +// ; + +// let restore_wallet = Wallet::builder() +// .with_storage_path("test-storage/backup_and_restore_different_coin_type/2") +// .with_secret_manager(SecretManager::Stronghold(stronghold)) +// .with_client_options(ClientOptions::new().with_node(NODE_OTHER)?) +// // Build with a different coin type, to check if it gets replaced by the one from the backup +// .with_coin_type(IOTA_COIN_TYPE) +// .finish() +// .await?; + +// // restore with ignore_if_coin_type_mismatch: Some(true) to not overwrite the coin type +// restore_wallet +// .restore_backup( +// PathBuf::from("test-storage/backup_and_restore_different_coin_type/backup.stronghold"), +// stronghold_password, +// Some(true), +// None, +// ) +// .await?; + +// // Validate restored data + +// // No wallet restored, because the coin type was different +// assert!(restore_wallet.get_wallet_data().await?.is_empty()); + +// // Restored coin type is not used and it's still the same one +// let new_wallet = restore_wallet; +// assert_eq!(new_wallet.data().await.coin_type(), &IOTA_COIN_TYPE); +// // secret manager is the same +// assert_eq!( +// new_wallet.address().await, +// "smr1qrpwecegav7eh0z363ca69laxej64rrt4e3u0rtycyuh0mam3vq3ulygj9p" +// ); + +// // compare restored client options +// let client_options = restore_wallet.client_options().await; +// let node_dto = NodeDto::Node(Node::from(Url::parse(NODE_OTHER).unwrap())); +// assert!(client_options.node_manager_builder.nodes.contains(&node_dto)); + +// tear_down(storage_path) +// } + +// // Backup and restore with Stronghold +// #[tokio::test] +// async fn backup_and_restore_same_coin_type() -> Result<()> { +// iota_stronghold::engine::snapshot::try_set_encrypt_work_factor(0).unwrap(); + +// let storage_path = "test-storage/backup_and_restore_same_coin_type"; +// setup(storage_path)?; + +// let client_options = ClientOptions::new().with_node(NODE_LOCAL)?; + +// let stronghold_password = "some_hopefully_secure_password".to_owned(); + +// // Create directory if not existing, because stronghold panics otherwise +// std::fs::create_dir_all(storage_path).ok(); +// let stronghold = StrongholdSecretManager::builder() +// .password(stronghold_password.clone()) +// .build("test-storage/backup_and_restore_same_coin_type/1.stronghold")?; + +// stronghold.store_mnemonic(Mnemonic::from("inhale gorilla deny three celery song category owner lottery rent author wealth penalty crawl hobby obtain glad warm early rain clutch slab august bleak".to_string())).await.unwrap(); + +// let wallet = Wallet::builder() +// .with_secret_manager(SecretManager::Stronghold(stronghold)) +// .with_client_options(client_options.clone()) +// .with_coin_type(SHIMMER_COIN_TYPE) +// .with_storage_path("test-storage/backup_and_restore_same_coin_type/1") +// .finish() +// .await?; + +// let wallet_before_backup = wallet; + +// wallet +// .backup( +// PathBuf::from("test-storage/backup_and_restore_same_coin_type/backup.stronghold"), +// stronghold_password.clone(), +// ) +// .await?; + +// // restore from backup + +// let stronghold = +// StrongholdSecretManager::builder().build("test-storage/backup_and_restore_same_coin_type/2.stronghold")?; + +// let restore_wallet = Wallet::builder() +// .with_storage_path("test-storage/backup_and_restore_same_coin_type/2") +// .with_secret_manager(SecretManager::Stronghold(stronghold)) +// .with_client_options(ClientOptions::new().with_node(NODE_OTHER)?) +// // Build with same coin type +// .with_coin_type(SHIMMER_COIN_TYPE) +// .finish() +// .await?; + +// // restore with ignore_if_coin_type_mismatch: Some(true) to not overwrite the coin type +// restore_wallet +// .restore_backup( +// PathBuf::from("test-storage/backup_and_restore_same_coin_type/backup.stronghold"), +// stronghold_password, +// Some(true), +// None, +// ) +// .await?; + +// // Validate restored data + +// // The wallet is restored, because the coin type is the same +// let restored_wallet = restore_wallet.get_wallet_data().await?; +// assert!(restored_wallet.is_some()); + +// // addresses are still there +// assert_eq!( +// restored_wallet.address().await, +// wallet_before_backup.address().await +// ); + +// // compare client options, they are not restored +// let client_options = restore_wallet.client_options().await; +// let node_dto = NodeDto::Node(Node::from(Url::parse(NODE_OTHER).unwrap())); +// assert!(client_options.node_manager_builder.nodes.contains(&node_dto)); + +// tear_down(storage_path) +// } + +// // Backup and restore with Stronghold +// #[tokio::test] +// async fn backup_and_restore_different_coin_type_dont_ignore() -> Result<()> { +// iota_stronghold::engine::snapshot::try_set_encrypt_work_factor(0).unwrap(); + +// let storage_path = "test-storage/backup_and_restore_different_coin_type_dont_ignore"; +// setup(storage_path)?; + +// let client_options = ClientOptions::new().with_node(NODE_OTHER)?; + +// let stronghold_password = "some_hopefully_secure_password".to_owned(); + +// // Create directory if not existing, because stronghold panics otherwise +// std::fs::create_dir_all(storage_path).ok(); +// let stronghold = StrongholdSecretManager::builder() +// .password(stronghold_password.clone()) +// .build("test-storage/backup_and_restore_different_coin_type_dont_ignore/1.stronghold")?; + +// stronghold.store_mnemonic(Mnemonic::from("inhale gorilla deny three celery song category owner lottery rent author wealth penalty crawl hobby obtain glad warm early rain clutch slab august bleak".to_string())).await.unwrap(); + +// let wallet = Wallet::builder() +// .with_secret_manager(SecretManager::Stronghold(stronghold)) +// .with_client_options(client_options.clone()) +// .with_coin_type(SHIMMER_COIN_TYPE) +// .with_storage_path("test-storage/backup_and_restore_different_coin_type_dont_ignore/1") +// .finish() +// .await?; + +// wallet +// .backup( +// PathBuf::from("test-storage/backup_and_restore_different_coin_type_dont_ignore/backup.stronghold"), +// stronghold_password.clone(), +// ) +// .await?; + +// // restore from backup + +// let stronghold = StrongholdSecretManager::builder() +// .build("test-storage/backup_and_restore_different_coin_type_dont_ignore/2.stronghold")?; + +// let restore_wallet = Wallet::builder() +// .with_storage_path("test-storage/backup_and_restore_different_coin_type_dont_ignore/2") +// .with_secret_manager(SecretManager::Stronghold(stronghold)) +// .with_client_options(ClientOptions::new().with_node(NODE_LOCAL)?) +// // Build with a different coin type, to check if it gets replaced by the one from the backup +// .with_coin_type(IOTA_COIN_TYPE) +// .finish() +// .await?; + +// // restore with ignore_if_coin_type_mismatch: Some(true) to not overwrite the coin type +// restore_wallet +// .restore_backup( +// PathBuf::from("test-storage/backup_and_restore_different_coin_type_dont_ignore/backup.stronghold"), +// stronghold_password, +// Some(false), +// None, +// ) +// .await?; + +// // Validate restored data + +// // No wallet restored, because the coin type was different +// let restored_wallet = restore_wallet.get_wallet_data().await?; +// assert_eq!( +// wallet.address().await, +// restored_wallet.address().await, +// ); + +// // TODO: Restored coin type is used +// let new_wallet = restore_wallet; +// assert_eq!(new_wallet.data().await.coin_type(), &SHIMMER_COIN_TYPE); +// // secret manager is restored +// assert_eq!( +// new_wallet.address().await, +// "smr1qzvjvjyqxgfx4f0m3xhn2rj24e03dwsmjz082735y3wx88v2gudu2afedhu" +// ); + +// // compare client options, they are not restored +// let client_options = restore_wallet.client_options().await; +// let node_dto = NodeDto::Node(Node::from(Url::parse(NODE_LOCAL).unwrap())); +// assert!(client_options.node_manager_builder.nodes.contains(&node_dto)); + +// tear_down(storage_path) +// } + +// #[tokio::test] +// async fn backup_and_restore_bech32_hrp_mismatch() -> Result<()> { +// iota_stronghold::engine::snapshot::try_set_encrypt_work_factor(0).unwrap(); + +// let storage_path = "test-storage/backup_and_restore_bech32_hrp_mismatch"; +// setup(storage_path)?; + +// let client_options = ClientOptions::new().with_node(NODE_LOCAL)?; + +// let stronghold_password = "some_hopefully_secure_password".to_owned(); + +// // Create directory if not existing, because stronghold panics otherwise +// std::fs::create_dir_all(storage_path).ok(); +// let stronghold = StrongholdSecretManager::builder() +// .password(stronghold_password.clone()) +// .build("test-storage/backup_and_restore_bech32_hrp_mismatch/1.stronghold")?; + +// stronghold.store_mnemonic(Mnemonic::from("inhale gorilla deny three celery song category owner lottery rent author wealth penalty crawl hobby obtain glad warm early rain clutch slab august bleak".to_string())).await.unwrap(); + +// let wallet = Wallet::builder() +// .with_secret_manager(SecretManager::Stronghold(stronghold)) +// .with_client_options(client_options.clone()) +// .with_coin_type(SHIMMER_COIN_TYPE) +// .with_storage_path("test-storage/backup_and_restore_bech32_hrp_mismatch/1") +// .finish() +// .await?; + +// wallet +// .backup( +// PathBuf::from("test-storage/backup_and_restore_bech32_hrp_mismatch/backup.stronghold"), +// stronghold_password.clone(), +// ) +// .await?; + +// // restore from backup + +// let stronghold = +// StrongholdSecretManager::builder().build("test-storage/backup_and_restore_bech32_hrp_mismatch/2.stronghold")? +// ; + +// let restore_wallet = Wallet::builder() +// .with_storage_path("test-storage/backup_and_restore_bech32_hrp_mismatch/2") +// .with_secret_manager(SecretManager::Stronghold(stronghold)) +// .with_client_options(ClientOptions::new().with_node(NODE_OTHER)?) +// // Build with a different coin type, to check if it gets replaced by the one from the backup +// .with_coin_type(IOTA_COIN_TYPE) +// .finish() +// .await?; + +// restore_wallet +// .restore_backup( +// PathBuf::from("test-storage/backup_and_restore_bech32_hrp_mismatch/backup.stronghold"), +// stronghold_password, +// None, +// Some(iota_sdk::types::block::address::Hrp::from_str_unchecked("otherhrp")), +// ) +// .await?; + +// // Validate restored data + +// // compare restored client options +// let client_options = restore_wallet.client_options().await; +// let node_dto = NodeDto::Node(Node::from(Url::parse(NODE_LOCAL).unwrap())); +// assert!(client_options.node_manager_builder.nodes.contains(&node_dto)); + +// // No restored wallet because the bech32 hrp was different +// let restored_wallet = restore_wallet.get_wallet_data().await?; +// assert!(restored_wallet.is_empty()); + +// // Restored coin type is used +// let new_wallet = restore_wallet; +// assert_eq!(new_wallet.details().await.coin_type(), &SHIMMER_COIN_TYPE); + +// // secret manager is the same +// assert_eq!( +// wallet.generate_ed25519_addresses(1, None).await?, +// new_wallet.generate_ed25519_addresses(1, None).await? +// ); +// tear_down(storage_path) +// } + +// // Restore a Stronghold snapshot without secret manager data +// #[tokio::test] +// async fn restore_no_secret_manager_data() -> Result<()> { +// iota_stronghold::engine::snapshot::try_set_encrypt_work_factor(0).unwrap(); + +// let storage_path = "test-storage/restore_no_secret_manager_data"; +// setup(storage_path)?; + +// let stronghold = StrongholdSecretManager::builder().build(storage_path.to_string() + "/wallet.stronghold")?; + +// let restore_wallet = Wallet::builder() +// .with_storage_path(storage_path) +// .with_secret_manager(SecretManager::Stronghold(stronghold)) +// .with_client_options(ClientOptions::new().with_node(NODE_LOCAL)?) +// .with_coin_type(IOTA_COIN_TYPE) +// .finish() +// .await?; + +// let stronghold_password = "some_hopefully_secure_password".to_owned(); + +// restore_wallet +// .restore_backup( +// PathBuf::from("./tests/wallet/fixtures/no_secret_manager_data.stronghold"), +// stronghold_password.clone(), +// None, +// None, +// ) +// .await?; + +// restore_wallet.set_stronghold_password(stronghold_password).await?; + +// // Backup is restored also without any secret manager data inside and the seed is available +// // Backup was created with mnemonic: "inhale gorilla deny three celery song category owner lottery rent author +// // wealth penalty crawl hobby obtain glad warm early rain clutch slab august bleak" +// assert_eq!( +// restore_wallet.generate_ed25519_address(0, 0, None).await?.to_string(), +// "0xc2ece328eb3d9bbc51d471dd17fd3665aa8c6bae63c78d64c13977efbb8b011e" +// ); +// tear_down(storage_path) +// } diff --git a/sdk/tests/wallet/balance.rs b/sdk/tests/wallet/balance.rs index 9c9256e373..6a2c28310f 100644 --- a/sdk/tests/wallet/balance.rs +++ b/sdk/tests/wallet/balance.rs @@ -2,16 +2,19 @@ // SPDX-License-Identifier: Apache-2.0 use iota_sdk::{ - types::block::output::{ - feature::SenderFeature, - unlock_condition::{AddressUnlockCondition, ExpirationUnlockCondition}, - BasicOutputBuilder, UnlockCondition, + types::block::{ + address::Bech32Address, + output::{ + feature::SenderFeature, + unlock_condition::{AddressUnlockCondition, ExpirationUnlockCondition}, + BasicOutputBuilder, UnlockCondition, + }, }, - wallet::{account::types::Balance, Result}, + wallet::{types::Balance, Result}, }; use pretty_assertions::assert_eq; -use crate::wallet::common::{create_accounts_with_funds, make_wallet, setup, tear_down}; +use crate::wallet::common::{make_wallet, request_funds, setup, tear_down}; #[test] fn balance_add_assign() { @@ -90,32 +93,36 @@ fn balance_add_assign() { #[ignore] #[tokio::test] async fn balance_expiration() -> Result<()> { - let storage_path = "test-storage/balance_expiration"; - setup(storage_path)?; + let storage_path_0 = "test-storage/balance_expiration_0"; + let storage_path_1 = "test-storage/balance_expiration_1"; + let storage_path_2 = "test-storage/balance_expiration_2"; + setup(storage_path_0)?; + setup(storage_path_1)?; + setup(storage_path_2)?; - let wallet = make_wallet(storage_path, None, None).await?; + let wallet_0 = make_wallet(storage_path_0, None, None).await?; + let wallet_1 = make_wallet(storage_path_1, None, None).await?; + let wallet_2 = make_wallet(storage_path_2, None, None).await?; - let account_0 = &create_accounts_with_funds(&wallet, 1).await?[0]; - let account_1 = wallet.create_account().finish().await?; - let account_2 = wallet.create_account().finish().await?; + request_funds(&wallet_0).await?; let slots_until_expired = 20; - let token_supply = account_0.client().get_token_supply().await?; + let token_supply = wallet_0.client().get_token_supply().await?; let outputs = [BasicOutputBuilder::new_with_amount(1_000_000) // Send to account 1 with expiration to account 2, both have no amount yet .with_unlock_conditions([ - UnlockCondition::Address(AddressUnlockCondition::new(account_1.first_address_bech32().await)), + UnlockCondition::Address(AddressUnlockCondition::new(wallet_1.address().await)), UnlockCondition::Expiration(ExpirationUnlockCondition::new( - account_2.first_address_bech32().await, - account_0.client().get_slot_index().await? + slots_until_expired, + wallet_2.address().await, + wallet_0.client().get_slot_index().await? + slots_until_expired, )?), ]) - .with_features([SenderFeature::new(account_0.first_address_bech32().await)]) + .with_features([SenderFeature::new(wallet_0.address().await)]) .finish_output(token_supply)?]; - let balance_before_tx = account_0.balance().await?; - let tx = account_0.send_outputs(outputs, None).await?; - let balance_after_tx = account_0.balance().await?; + let balance_before_tx = wallet_0.balance().await?; + let tx = wallet_0.send_outputs(outputs, None).await?; + let balance_after_tx = wallet_0.balance().await?; // Total doesn't change before syncing after tx got confirmed assert_eq!( balance_before_tx.base_coin().total(), @@ -123,18 +130,18 @@ async fn balance_expiration() -> Result<()> { ); assert_eq!(balance_after_tx.base_coin().available(), 0); - account_0 + wallet_0 .reissue_transaction_until_included(&tx.transaction_id, None, None) .await?; - // Account 1 balance before expiration - let balance = account_1.sync(None).await?; + // Wallet 1 balance before expiration + let balance = wallet_1.sync(None).await?; assert_eq!(balance.potentially_locked_outputs().len(), 1); assert_eq!(balance.base_coin().total(), 0); assert_eq!(balance.base_coin().available(), 0); - // Account 2 balance before expiration - let balance = account_2.sync(None).await?; + // Wallet 2 balance before expiration + let balance = wallet_2.sync(None).await?; assert_eq!(balance.potentially_locked_outputs().len(), 1); assert_eq!(balance.base_coin().total(), 0); assert_eq!(balance.base_coin().available(), 0); @@ -143,47 +150,46 @@ async fn balance_expiration() -> Result<()> { // TODO wait for slots, not seconds tokio::time::sleep(std::time::Duration::from_secs(slots_until_expired as u64)).await; - // Account 1 balance after expiration - let balance = account_1.sync(None).await?; + // Wallet 1 balance after expiration + let balance = wallet_1.sync(None).await?; assert_eq!(balance.potentially_locked_outputs().len(), 0); assert_eq!(balance.base_coin().total(), 0); assert_eq!(balance.base_coin().available(), 0); - // Account 2 balance after expiration - let balance = account_2.sync(None).await?; + // Wallet 2 balance after expiration + let balance = wallet_2.sync(None).await?; assert_eq!(balance.potentially_locked_outputs().len(), 0); assert_eq!(balance.base_coin().total(), 1_000_000); assert_eq!(balance.base_coin().available(), 1_000_000); // It's possible to send the expired output let outputs = [BasicOutputBuilder::new_with_amount(1_000_000) - // Send to account 1 with expiration to account 2, both have no amount yet - .with_unlock_conditions([AddressUnlockCondition::new( - account_1.addresses().await[0].clone().into_bech32(), - )]) + // Send to wallet 1 with expiration to wallet 2, both have no amount yet + .with_unlock_conditions([AddressUnlockCondition::new(wallet_1.address().await)]) .finish_output(token_supply)?]; - let _tx = account_2.send_outputs(outputs, None).await?; + let _tx = wallet_2.send_outputs(outputs, None).await?; - tear_down(storage_path) + tear_down(storage_path_0)?; + tear_down(storage_path_1)?; + tear_down(storage_path_2)?; + Ok(()) } #[ignore] #[tokio::test] -async fn addresses_balance() -> Result<()> { - let storage_path = "test-storage/addresses_balance"; - setup(storage_path)?; +async fn balance_transfer() -> Result<()> { + let storage_path_0 = "test-storage/addresses_balance_0"; + let storage_path_1 = "test-storage/addresses_balance_1"; + setup(storage_path_0)?; + setup(storage_path_1)?; - let wallet = make_wallet(storage_path, None, None).await?; + let wallet_0 = make_wallet(storage_path_0, None, None).await?; + let wallet_1 = make_wallet(storage_path_1, None, None).await?; - let account_0 = &create_accounts_with_funds(&wallet, 1).await?[0]; - let account_1 = wallet.create_account().finish().await?; - let addresses_0 = account_0.addresses_with_unspent_outputs().await?; - let acc_1_addr = &account_1.generate_ed25519_addresses(1, None).await?[0]; + request_funds(&wallet_0).await?; - let balance_0 = account_0 - .addresses_balance(addresses_0.iter().map(|a| a.address().clone()).collect()) - .await?; - let balance_0_sync = account_0.balance().await?; + let balance_0 = wallet_0.balance().await?; + let balance_0_sync = wallet_0.sync(None).await?; let to_send = balance_0.base_coin().available(); // Check if 0 has balance and sync() and address_balance() match @@ -191,54 +197,30 @@ async fn addresses_balance() -> Result<()> { assert_eq!(balance_0, balance_0_sync); // Make sure 1 is empty - let balance_1 = account_1.sync(None).await?; + let balance_1 = wallet_1.sync(None).await?; assert_eq!(balance_1.base_coin().available(), 0); // Send to 1 - let tx = account_0.send(to_send, acc_1_addr.address().clone(), None).await?; + let tx = wallet_0.send(to_send, wallet_1.address().await, None).await?; + // Balance should update without sync - let balance_0 = account_0 - .addresses_balance(addresses_0.iter().map(|a| a.address().clone()).collect()) - .await?; - let balance_0_sync = account_0.balance().await?; + let balance_0 = wallet_0.balance().await?; + let balance_0_sync = wallet_0.sync(None).await?; assert_eq!(balance_0.base_coin().available(), 0); assert_eq!(balance_0, balance_0_sync); - account_0 + wallet_0 .reissue_transaction_until_included(&tx.transaction_id, None, None) .await?; - account_1.sync(None).await?; // Balance should have transferred entirely - let balance_1 = account_1.addresses_balance(vec![acc_1_addr.address().clone()]).await?; - let balance_1_sync = account_1.balance().await?; + let balance_1_sync = wallet_1.sync(None).await?; assert!(balance_1.base_coin().available() > 0); assert_eq!(balance_1, balance_1_sync); - // Internal transfer on account 1 - let acc_1_addr_2 = &account_1.generate_ed25519_addresses(1, None).await?[0]; - - let tx = account_1 - .send(to_send / 2, acc_1_addr_2.address().clone(), None) - .await?; - account_1 - .reissue_transaction_until_included(&tx.transaction_id, None, None) - .await?; - let balance_1_sync = account_1.sync(None).await?; - - // Check the new address - let balance_1 = account_1 - .addresses_balance(vec![acc_1_addr_2.address().clone()]) - .await?; - assert_eq!(to_send / 2, balance_1.base_coin().available()); - - // Check old and new together - let balance_1_total = account_1 - .addresses_balance(vec![acc_1_addr.address().clone(), acc_1_addr_2.address().clone()]) - .await?; - assert_eq!(balance_1_total, balance_1_sync); - - tear_down(storage_path) + tear_down(storage_path_0)?; + tear_down(storage_path_1)?; + Ok(()) } #[ignore] @@ -250,36 +232,37 @@ async fn balance_voting_power() -> Result<()> { let wallet = make_wallet(storage_path, None, None).await?; - let account = &create_accounts_with_funds(&wallet, 1).await?[0]; + request_funds(&wallet).await?; let faucet_amount = 100_000_000_000; - let balance = account.balance().await?; + let balance = wallet.balance().await?; assert_eq!(balance.base_coin().total(), faucet_amount); assert_eq!(balance.base_coin().available(), faucet_amount); let voting_power = 1_000_000; // Only use a part as voting power - let tx = account.increase_voting_power(voting_power).await?; - account + let tx = wallet.increase_voting_power(voting_power).await?; + wallet .reissue_transaction_until_included(&tx.transaction_id, None, None) .await?; - let balance = account.sync(None).await?; + let balance = wallet.sync(None).await?; assert_eq!(balance.base_coin().total(), faucet_amount); assert_eq!(balance.base_coin().available(), faucet_amount - voting_power); - let account_voting_power = account.get_voting_power().await?; - assert_eq!(account_voting_power, voting_power); + let wallet_voting_power = wallet.get_voting_power().await?; + assert_eq!(wallet_voting_power, voting_power); // Increase voting power to total amount - let tx = account.increase_voting_power(faucet_amount - voting_power).await?; - account + let tx = wallet.increase_voting_power(faucet_amount - voting_power).await?; + wallet .reissue_transaction_until_included(&tx.transaction_id, None, None) .await?; - let balance = account.sync(None).await?; + let balance = wallet.sync(None).await?; assert_eq!(balance.base_coin().total(), faucet_amount); assert_eq!(balance.base_coin().available(), 0); - let account_voting_power = account.get_voting_power().await?; - assert_eq!(account_voting_power, faucet_amount); + let wallet_voting_power = wallet.get_voting_power().await?; + assert_eq!(wallet_voting_power, faucet_amount); - tear_down(storage_path) + tear_down(storage_path)?; + Ok(()) } diff --git a/sdk/tests/wallet/bech32_hrp_validation.rs b/sdk/tests/wallet/bech32_hrp_validation.rs index 1128ecd699..5271a0bbb3 100644 --- a/sdk/tests/wallet/bech32_hrp_validation.rs +++ b/sdk/tests/wallet/bech32_hrp_validation.rs @@ -4,7 +4,7 @@ use iota_sdk::{ client::Error as ClientError, types::block::address::{Bech32Address, ToBech32Ext}, - wallet::{account::OutputParams, Error, Result, SendParams}, + wallet::{Error, OutputParams, Result, SendParams}, }; use pretty_assertions::assert_eq; @@ -18,20 +18,18 @@ async fn bech32_hrp_send_amount() -> Result<()> { let wallet = make_wallet(storage_path, None, None).await?; - let account = wallet.create_account().finish().await?; - - let error = account + let error = wallet .send_with_params( [SendParams::new( 1_000_000, - Bech32Address::try_new("wronghrp", account.first_address_bech32().await)?, + Bech32Address::try_new("wronghrp", wallet.address().await)?, )?], None, ) .await .unwrap_err(); - let bech32_hrp = account.client().get_bech32_hrp().await?; + let bech32_hrp = wallet.client().get_bech32_hrp().await?; match error { Error::Client(error) => match *error { @@ -54,16 +52,11 @@ async fn bech32_hrp_prepare_output() -> Result<()> { setup(storage_path)?; let wallet = make_wallet(storage_path, None, None).await?; - let account = wallet.create_account().finish().await?; - let error = account + let error = wallet .prepare_output( OutputParams { - recipient_address: account.addresses().await[0] - .clone() - .into_bech32() - .into_inner() - .to_bech32_unchecked("wronghrp"), + recipient_address: wallet.address().await.to_bech32_unchecked("wronghrp"), amount: 1_000_000, assets: None, features: None, @@ -75,7 +68,7 @@ async fn bech32_hrp_prepare_output() -> Result<()> { .await .unwrap_err(); - let bech32_hrp = account.client().get_bech32_hrp().await?; + let bech32_hrp = wallet.client().get_bech32_hrp().await?; match error { Error::Client(error) => match *error { diff --git a/sdk/tests/wallet/burn_outputs.rs b/sdk/tests/wallet/burn_outputs.rs index a7c07c4898..1517d73c10 100644 --- a/sdk/tests/wallet/burn_outputs.rs +++ b/sdk/tests/wallet/burn_outputs.rs @@ -7,12 +7,12 @@ use iota_sdk::{ unlock_condition::{AddressUnlockCondition, ExpirationUnlockCondition}, NativeToken, NftId, NftOutputBuilder, OutputId, UnlockCondition, }, - wallet::{Account, CreateNativeTokenParams, MintNftParams, Result}, + wallet::{CreateNativeTokenParams, MintNftParams, Result, Wallet}, U256, }; use pretty_assertions::assert_eq; -use crate::wallet::common::{create_accounts_with_funds, make_wallet, setup, tear_down}; +use crate::wallet::common::{make_wallet, request_funds, setup, tear_down}; #[ignore] #[tokio::test] @@ -21,19 +21,19 @@ async fn mint_and_burn_nft() -> Result<()> { setup(storage_path)?; let wallet = make_wallet(storage_path, None, None).await?; - let account = &create_accounts_with_funds(&wallet, 1).await?[0]; + request_funds(&wallet).await?; let nft_options = [MintNftParams::new() - .with_address(account.first_address_bech32().await) + .with_address(wallet.address().await) .with_metadata(b"some nft metadata".to_vec()) .with_immutable_metadata(b"some immutable nft metadata".to_vec())]; - let transaction = account.mint_nfts(nft_options, None).await.unwrap(); - account + let transaction = wallet.mint_nfts(nft_options, None).await.unwrap(); + wallet .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; - let balance = account.sync(None).await.unwrap(); + let balance = wallet.sync(None).await.unwrap(); let output_id = OutputId::new(transaction.transaction_id, 0u16).unwrap(); let nft_id = NftId::from(&output_id); @@ -42,11 +42,11 @@ async fn mint_and_burn_nft() -> Result<()> { println!("account balance -> {}", serde_json::to_string(&balance).unwrap()); assert!(search.is_some()); - let transaction = account.burn(nft_id, None).await.unwrap(); - account + let transaction = wallet.burn(nft_id, None).await.unwrap(); + wallet .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; - let balance = account.sync(None).await.unwrap(); + let balance = wallet.sync(None).await.unwrap(); let search = balance.nfts().iter().find(|&balance_nft_id| *balance_nft_id == nft_id); println!("account balance -> {}", serde_json::to_string(&balance).unwrap()); assert!(search.is_none()); @@ -60,40 +60,35 @@ async fn mint_and_burn_expired_nft() -> Result<()> { let storage_path = "test-storage/mint_and_burn_expired_nft"; setup(storage_path)?; - let wallet = make_wallet(storage_path, None, None).await?; - let account_0 = &create_accounts_with_funds(&wallet, 1).await?[0]; - let account_1 = wallet.create_account().finish().await?; + let wallet_0 = make_wallet(storage_path, None, None).await?; + let wallet_1 = make_wallet(storage_path, None, None).await?; + request_funds(&wallet_0).await?; - let token_supply = account_0.client().get_token_supply().await?; + let token_supply = wallet_0.client().get_token_supply().await?; let amount = 1_000_000; let outputs = [NftOutputBuilder::new_with_amount(amount, NftId::null()) .with_unlock_conditions([ - UnlockCondition::Address(AddressUnlockCondition::new( - account_0.addresses().await[0].clone().into_bech32().into_inner(), - )), + UnlockCondition::Address(AddressUnlockCondition::new(wallet_0.address().await)), // immediately expired to account_1 - UnlockCondition::Expiration(ExpirationUnlockCondition::new( - account_1.addresses().await[0].clone().into_bech32().into_inner(), - 1, - )?), + UnlockCondition::Expiration(ExpirationUnlockCondition::new(wallet_1.address().await, 1)?), ]) .finish_output(token_supply)?]; - let transaction = account_0.send_outputs(outputs, None).await?; - account_0 + let transaction = wallet_0.send_outputs(outputs, None).await?; + wallet_0 .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; let output_id = OutputId::new(transaction.transaction_id, 0u16)?; let nft_id = NftId::from(&output_id); - account_1.sync(None).await?; - let transaction = account_1.burn(nft_id, None).await?; - account_1 + wallet_1.sync(None).await?; + let transaction = wallet_1.burn(nft_id, None).await?; + wallet_1 .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; - let balance = account_1.sync(None).await?; + let balance = wallet_1.sync(None).await?; // After burning the amount is available on account_1 assert_eq!(balance.base_coin().available(), amount); @@ -107,16 +102,16 @@ async fn create_and_melt_native_token() -> Result<()> { setup(storage_path)?; let wallet = make_wallet(storage_path, None, None).await?; - let account = &create_accounts_with_funds(&wallet, 1).await?[0]; + request_funds(&wallet).await?; // First create an account output, this needs to be done only once, because an account can have many foundry outputs - let transaction = account.create_account_output(None, None).await?; + let transaction = wallet.create_account_output(None, None).await?; // Wait for transaction to get included - account + wallet .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; - account.sync(None).await?; + wallet.sync(None).await?; let circulating_supply = U256::from(60i32); let params = CreateNativeTokenParams { @@ -126,32 +121,32 @@ async fn create_and_melt_native_token() -> Result<()> { foundry_metadata: None, }; - let create_transaction = account.create_native_token(params, None).await.unwrap(); + let create_transaction = wallet.create_native_token(params, None).await.unwrap(); - account + wallet .reissue_transaction_until_included(&create_transaction.transaction.transaction_id, None, None) .await?; - let balance = account.sync(None).await.unwrap(); + let balance = wallet.sync(None).await.unwrap(); let search = balance .native_tokens() .iter() .find(|token| token.token_id() == &create_transaction.token_id && token.available() == circulating_supply); - println!("account balance -> {}", serde_json::to_string(&balance).unwrap()); + println!("wallet balance -> {}", serde_json::to_string(&balance).unwrap()); assert!(search.is_some()); // Melt some of the circulating supply let melt_amount = U256::from(40i32); - let transaction = account + let transaction = wallet .melt_native_token(create_transaction.token_id, melt_amount, None) .await .unwrap(); - account + wallet .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; - let balance = account.sync(None).await.unwrap(); - println!("account balance -> {}", serde_json::to_string(&balance).unwrap()); + let balance = wallet.sync(None).await.unwrap(); + println!("wallet balance -> {}", serde_json::to_string(&balance).unwrap()); let search = balance.native_tokens().iter().find(|token| { (token.token_id() == &create_transaction.token_id) && (token.available() == circulating_supply - melt_amount) @@ -160,16 +155,16 @@ async fn create_and_melt_native_token() -> Result<()> { // Then melt the rest of the supply let melt_amount = circulating_supply - melt_amount; - let transaction = account + let transaction = wallet .melt_native_token(create_transaction.token_id, melt_amount, None) .await .unwrap(); - account + wallet .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; - let balance = account.sync(None).await.unwrap(); - println!("account balance -> {}", serde_json::to_string(&balance).unwrap()); + let balance = wallet.sync(None).await.unwrap(); + println!("wallet balance -> {}", serde_json::to_string(&balance).unwrap()); let search = balance .native_tokens() @@ -178,47 +173,47 @@ async fn create_and_melt_native_token() -> Result<()> { assert!(search.is_none()); // Call to run tests in sequence - destroy_foundry(account).await?; - destroy_account(account).await?; + destroy_foundry(&wallet).await?; + destroy_account(&wallet).await?; tear_down(storage_path) } -async fn destroy_foundry(account: &Account) -> Result<()> { - let balance = account.sync(None).await?; - println!("account balance -> {}", serde_json::to_string(&balance).unwrap()); +async fn destroy_foundry(wallet: &Wallet) -> Result<()> { + let balance = wallet.sync(None).await?; + println!("wallet balance -> {}", serde_json::to_string(&balance).unwrap()); // Let's burn the first foundry we can find, although we may not find the required account output so maybe not a // good idea let foundry_id = *balance.foundries().first().unwrap(); - let transaction = account.burn(foundry_id, None).await.unwrap(); - account + let transaction = wallet.burn(foundry_id, None).await.unwrap(); + wallet .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; - let balance = account.sync(None).await.unwrap(); + let balance = wallet.sync(None).await.unwrap(); let search = balance .foundries() .iter() .find(|&balance_foundry_id| *balance_foundry_id == foundry_id); - println!("account balance -> {}", serde_json::to_string(&balance).unwrap()); + println!("wallet balance -> {}", serde_json::to_string(&balance).unwrap()); assert!(search.is_none()); Ok(()) } -async fn destroy_account(account: &Account) -> Result<()> { - let balance = account.sync(None).await.unwrap(); +async fn destroy_account(wallet: &Wallet) -> Result<()> { + let balance = wallet.sync(None).await.unwrap(); println!("account balance -> {}", serde_json::to_string(&balance).unwrap()); // Let's destroy the first account we can find let account_id = *balance.accounts().first().unwrap(); println!("account_id -> {account_id}"); - let transaction = account.burn(account_id, None).await.unwrap(); - account + let transaction = wallet.burn(account_id, None).await.unwrap(); + wallet .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; - let balance = account.sync(None).await.unwrap(); + let balance = wallet.sync(None).await.unwrap(); let search = balance .accounts() .iter() @@ -237,17 +232,17 @@ async fn create_and_burn_native_tokens() -> Result<()> { let wallet = make_wallet(storage_path, None, None).await?; - let account = &create_accounts_with_funds(&wallet, 1).await?[0]; + request_funds(&wallet).await?; let native_token_amount = U256::from(100); - let tx = account.create_account_output(None, None).await?; - account + let tx = wallet.create_account_output(None, None).await?; + wallet .reissue_transaction_until_included(&tx.transaction_id, None, None) .await?; - account.sync(None).await?; + wallet.sync(None).await?; - let create_tx = account + let create_tx = wallet .create_native_token( CreateNativeTokenParams { account_id: None, @@ -258,18 +253,18 @@ async fn create_and_burn_native_tokens() -> Result<()> { None, ) .await?; - account + wallet .reissue_transaction_until_included(&create_tx.transaction.transaction_id, None, None) .await?; - account.sync(None).await?; + wallet.sync(None).await?; - let tx = account + let tx = wallet .burn(NativeToken::new(create_tx.token_id, native_token_amount)?, None) .await?; - account + wallet .reissue_transaction_until_included(&tx.transaction_id, None, None) .await?; - let balance = account.sync(None).await?; + let balance = wallet.sync(None).await?; assert!(balance.native_tokens().is_empty()); @@ -283,34 +278,34 @@ async fn mint_and_burn_nft_with_account() -> Result<()> { setup(storage_path)?; let wallet = make_wallet(storage_path, None, None).await?; - let account = &create_accounts_with_funds(&wallet, 1).await?[0]; + request_funds(&wallet).await?; - let tx = account.create_account_output(None, None).await?; - account + let tx = wallet.create_account_output(None, None).await?; + wallet .reissue_transaction_until_included(&tx.transaction_id, None, None) .await?; - account.sync(None).await?; + wallet.sync(None).await?; let nft_options = [MintNftParams::new() .with_metadata(b"some nft metadata".to_vec()) .with_immutable_metadata(b"some immutable nft metadata".to_vec())]; - let nft_tx = account.mint_nfts(nft_options, None).await.unwrap(); - account + let nft_tx = wallet.mint_nfts(nft_options, None).await.unwrap(); + wallet .reissue_transaction_until_included(&nft_tx.transaction_id, None, None) .await?; let output_id = OutputId::new(nft_tx.transaction_id, 0u16).unwrap(); let nft_id = NftId::from(&output_id); - let balance = account.sync(None).await?; + let balance = wallet.sync(None).await?; let account_id = balance.accounts().first().unwrap(); - let burn_tx = account + let burn_tx = wallet .burn(Burn::new().add_nft(nft_id).add_account(*account_id), None) .await?; - account + wallet .reissue_transaction_until_included(&burn_tx.transaction_id, None, None) .await?; - let balance = account.sync(None).await?; + let balance = wallet.sync(None).await?; assert!(balance.accounts().is_empty()); assert!(balance.nfts().is_empty()); diff --git a/sdk/tests/wallet/claim_outputs.rs b/sdk/tests/wallet/claim_outputs.rs index b414ee2d86..271737075f 100644 --- a/sdk/tests/wallet/claim_outputs.rs +++ b/sdk/tests/wallet/claim_outputs.rs @@ -6,32 +6,33 @@ use iota_sdk::{ unlock_condition::{AddressUnlockCondition, ExpirationUnlockCondition}, BasicOutputBuilder, NativeToken, NftId, NftOutputBuilder, UnlockCondition, }, - wallet::{ - account::{OutputsToClaim, TransactionOptions}, - CreateNativeTokenParams, Result, SendNativeTokensParams, SendParams, - }, + wallet::{CreateNativeTokenParams, OutputsToClaim, Result, SendNativeTokensParams, SendParams, TransactionOptions}, U256, }; use pretty_assertions::assert_eq; -use crate::wallet::common::{create_accounts_with_funds, make_wallet, setup, tear_down}; +use crate::wallet::common::{make_wallet, request_funds, setup, tear_down}; #[ignore] #[tokio::test] async fn claim_2_basic_micro_outputs() -> Result<()> { - let storage_path = "test-storage/claim_2_basic_micro_outputs"; - setup(storage_path)?; + let storage_path_0 = "test-storage/claim_2_basic_micro_outputs_0"; + let storage_path_1 = "test-storage/claim_2_basic_micro_outputs_1"; + setup(storage_path_0)?; + setup(storage_path_1)?; - let wallet = make_wallet(storage_path, None, None).await?; + let wallet_0 = make_wallet(storage_path_0, None, None).await?; + let wallet_1 = make_wallet(storage_path_1, None, None).await?; - let accounts = create_accounts_with_funds(&wallet, 2).await?; + request_funds(&wallet_0).await?; + request_funds(&wallet_1).await?; let micro_amount = 1; - let tx = accounts[1] + let tx = wallet_1 .send_with_params( [ - SendParams::new(micro_amount, accounts[0].first_address_bech32().await)?, - SendParams::new(micro_amount, accounts[0].first_address_bech32().await)?, + SendParams::new(micro_amount, wallet_0.address().await)?, + SendParams::new(micro_amount, wallet_0.address().await)?, ], TransactionOptions { allow_micro_amount: true, @@ -40,48 +41,55 @@ async fn claim_2_basic_micro_outputs() -> Result<()> { ) .await?; - accounts[1] + wallet_1 .reissue_transaction_until_included(&tx.transaction_id, None, None) .await?; // Claim with account 0 - let balance = accounts[0].sync(None).await.unwrap(); + let balance = wallet_0.sync(None).await.unwrap(); assert_eq!(balance.potentially_locked_outputs().len(), 2); let base_coin_amount_before_claiming = balance.base_coin().available(); - let tx = accounts[0] - .claim_outputs(accounts[0].claimable_outputs(OutputsToClaim::MicroTransactions).await?) + let tx = wallet_0 + .claim_outputs(wallet_0.claimable_outputs(OutputsToClaim::MicroTransactions).await?) .await?; - accounts[0] + wallet_0 .reissue_transaction_until_included(&tx.transaction_id, None, None) .await?; - let balance = accounts[0].sync(None).await.unwrap(); + let balance = wallet_0.sync(None).await.unwrap(); assert_eq!(balance.potentially_locked_outputs().len(), 0); assert_eq!( balance.base_coin().available(), base_coin_amount_before_claiming + 2 * micro_amount ); - tear_down(storage_path) + tear_down(storage_path_0)?; + tear_down(storage_path_1)?; + + Ok(()) } #[ignore] #[tokio::test] async fn claim_1_of_2_basic_outputs() -> Result<()> { - let storage_path = "test-storage/claim_1_of_2_basic_outputs"; - setup(storage_path)?; + let storage_path_0 = "test-storage/claim_1_of_2_basic_outputs_0"; + let storage_path_1 = "test-storage/claim_1_of_2_basic_outputs_1"; + setup(storage_path_0)?; + setup(storage_path_1)?; - let wallet = make_wallet(storage_path, None, None).await?; + let wallet_0 = make_wallet(storage_path_0, None, None).await?; + let wallet_1 = make_wallet(storage_path_0, None, None).await?; - let accounts = create_accounts_with_funds(&wallet, 2).await?; + request_funds(&wallet_0).await?; + request_funds(&wallet_1).await?; let amount = 10; - let tx = accounts[1] + let tx = wallet_1 .send_with_params( [ - SendParams::new(amount, accounts[0].first_address_bech32().await)?, - SendParams::new(0, accounts[0].first_address_bech32().await)?, + SendParams::new(amount, wallet_0.address().await)?, + SendParams::new(0, wallet_0.address().await)?, ], TransactionOptions { allow_micro_amount: true, @@ -90,52 +98,57 @@ async fn claim_1_of_2_basic_outputs() -> Result<()> { ) .await?; - accounts[1] + wallet_1 .reissue_transaction_until_included(&tx.transaction_id, None, None) .await?; // Claim with account 0 - let balance = accounts[0].sync(None).await.unwrap(); + let balance = wallet_0.sync(None).await.unwrap(); assert_eq!(balance.potentially_locked_outputs().len(), 2); let base_coin_amount_before_claiming = balance.base_coin().available(); - let tx = accounts[0] - .claim_outputs(accounts[0].claimable_outputs(OutputsToClaim::Amount).await?) + let tx = wallet_0 + .claim_outputs(wallet_0.claimable_outputs(OutputsToClaim::Amount).await?) .await?; - accounts[0] + wallet_0 .reissue_transaction_until_included(&tx.transaction_id, None, None) .await?; - let balance = accounts[0].sync(None).await.unwrap(); + let balance = wallet_0.sync(None).await.unwrap(); assert_eq!(balance.potentially_locked_outputs().len(), 1); assert_eq!( balance.base_coin().available(), base_coin_amount_before_claiming + amount ); - tear_down(storage_path) + tear_down(storage_path_0)?; + tear_down(storage_path_1)?; + + Ok(()) } #[ignore] #[tokio::test] async fn claim_2_basic_outputs_no_outputs_in_claim_account() -> Result<()> { - let storage_path = "test-storage/claim_2_basic_outputs_no_outputs_in_claim_account"; - setup(storage_path)?; + let storage_path_0 = "test-storage/claim_2_basic_outputs_no_outputs_in_claim_account_0"; + let storage_path_1 = "test-storage/claim_2_basic_outputs_no_outputs_in_claim_account_1"; + setup(storage_path_0)?; + setup(storage_path_1)?; - let wallet = make_wallet(storage_path, None, None).await?; + let wallet_0 = make_wallet(storage_path_0, None, None).await?; + let wallet_1 = make_wallet(storage_path_1, None, None).await?; - let account_0 = &create_accounts_with_funds(&wallet, 1).await?[0]; - let account_1 = wallet.create_account().finish().await?; + request_funds(&wallet_0).await?; - let token_supply = account_0.client().get_token_supply().await?; - let rent_structure = account_0.client().get_rent_structure().await?; + let token_supply = wallet_0.client().get_token_supply().await?; + let rent_structure = wallet_0.client().get_rent_structure().await?; // TODO more fitting value - let expiration_slot = account_0.client().get_slot_index().await? + 86400; + let expiration_slot = wallet_0.client().get_slot_index().await? + 86400; let output = BasicOutputBuilder::new_with_minimum_storage_deposit(rent_structure) - .add_unlock_condition(AddressUnlockCondition::new(account_1.first_address_bech32().await)) + .add_unlock_condition(AddressUnlockCondition::new(wallet_1.address().await)) .add_unlock_condition(ExpirationUnlockCondition::new( - account_0.first_address_bech32().await, + wallet_0.address().await, expiration_slot, )?) .finish_output(token_supply)?; @@ -143,53 +156,57 @@ async fn claim_2_basic_outputs_no_outputs_in_claim_account() -> Result<()> { let outputs = vec![output; 2]; - let tx = account_0.send_outputs(outputs, None).await?; + let tx = wallet_0.send_outputs(outputs, None).await?; - account_0 + wallet_0 .reissue_transaction_until_included(&tx.transaction_id, None, None) .await?; - // Claim with account 1 - let balance = account_1.sync(None).await.unwrap(); + // Claim with wallet 1 + let balance = wallet_1.sync(None).await.unwrap(); assert_eq!(balance.potentially_locked_outputs().len(), 2); let base_coin_amount_before_claiming = balance.base_coin().available(); - let tx = account_1 - .claim_outputs(account_1.claimable_outputs(OutputsToClaim::All).await?) + let tx = wallet_1 + .claim_outputs(wallet_1.claimable_outputs(OutputsToClaim::All).await?) .await?; - account_1 + wallet_1 .reissue_transaction_until_included(&tx.transaction_id, None, None) .await?; - let balance = account_1.sync(None).await.unwrap(); + let balance = wallet_1.sync(None).await.unwrap(); assert_eq!(balance.potentially_locked_outputs().len(), 0); assert_eq!( balance.base_coin().available(), base_coin_amount_before_claiming + 2 * amount ); - tear_down(storage_path) + tear_down(storage_path_0) } #[ignore] #[tokio::test] async fn claim_2_native_tokens() -> Result<()> { - let storage_path = "test-storage/claim_2_native_tokens"; - setup(storage_path)?; + let storage_path_0 = "test-storage/claim_2_native_tokens_0"; + let storage_path_1 = "test-storage/claim_2_native_tokens_1"; + setup(storage_path_0)?; + setup(storage_path_1)?; - let wallet = make_wallet(storage_path, None, None).await?; + let wallet_0 = make_wallet(storage_path_0, None, None).await?; + let wallet_1 = make_wallet(storage_path_1, None, None).await?; - let accounts = create_accounts_with_funds(&wallet, 2).await?; + request_funds(&wallet_0).await?; + request_funds(&wallet_1).await?; let native_token_amount = U256::from(100); - let tx = accounts[1].create_account_output(None, None).await?; - accounts[1] + let tx = wallet_1.create_account_output(None, None).await?; + wallet_1 .reissue_transaction_until_included(&tx.transaction_id, None, None) .await?; - accounts[1].sync(None).await?; + wallet_1.sync(None).await?; - let create_tx_0 = accounts[1] + let create_tx_0 = wallet_1 .create_native_token( CreateNativeTokenParams { account_id: None, @@ -200,12 +217,12 @@ async fn claim_2_native_tokens() -> Result<()> { None, ) .await?; - accounts[1] + wallet_1 .reissue_transaction_until_included(&create_tx_0.transaction.transaction_id, None, None) .await?; - accounts[1].sync(None).await?; + wallet_1.sync(None).await?; - let create_tx_1 = accounts[1] + let create_tx_1 = wallet_1 .create_native_token( CreateNativeTokenParams { account_id: None, @@ -216,42 +233,36 @@ async fn claim_2_native_tokens() -> Result<()> { None, ) .await?; - accounts[1] + wallet_1 .reissue_transaction_until_included(&create_tx_1.transaction.transaction_id, None, None) .await?; - accounts[1].sync(None).await?; + wallet_1.sync(None).await?; - let tx = accounts[1] + let tx = wallet_1 .send_native_tokens( [ - SendNativeTokensParams::new( - accounts[0].first_address_bech32().await, - [(create_tx_0.token_id, native_token_amount)], - )?, - SendNativeTokensParams::new( - accounts[0].first_address_bech32().await, - [(create_tx_1.token_id, native_token_amount)], - )?, + SendNativeTokensParams::new(wallet_0.address().await, [(create_tx_0.token_id, native_token_amount)])?, + SendNativeTokensParams::new(wallet_0.address().await, [(create_tx_1.token_id, native_token_amount)])?, ], None, ) .await?; - accounts[1] + wallet_1 .reissue_transaction_until_included(&tx.transaction_id, None, None) .await?; // Claim with account 0 - let balance = accounts[0].sync(None).await.unwrap(); + let balance = wallet_0.sync(None).await.unwrap(); assert_eq!(balance.potentially_locked_outputs().len(), 2); - let tx = accounts[0] - .claim_outputs(accounts[0].claimable_outputs(OutputsToClaim::NativeTokens).await?) + let tx = wallet_0 + .claim_outputs(wallet_0.claimable_outputs(OutputsToClaim::NativeTokens).await?) .await?; - accounts[0] + wallet_0 .reissue_transaction_until_included(&tx.transaction_id, None, None) .await?; - let balance = accounts[0].sync(None).await.unwrap(); + let balance = wallet_0.sync(None).await.unwrap(); assert_eq!(balance.potentially_locked_outputs().len(), 0); assert_eq!(balance.native_tokens().len(), 2); let native_token_0 = balance @@ -267,29 +278,34 @@ async fn claim_2_native_tokens() -> Result<()> { .unwrap(); assert_eq!(native_token_1.total(), native_token_amount); - tear_down(storage_path) + tear_down(storage_path_0)?; + tear_down(storage_path_1)?; + + Ok(()) } #[ignore] #[tokio::test] async fn claim_2_native_tokens_no_outputs_in_claim_account() -> Result<()> { - let storage_path = "test-storage/claim_2_native_tokens_no_outputs_in_claim_account"; - setup(storage_path)?; + let storage_path_0 = "test-storage/claim_2_native_tokens_no_outputs_in_claim_account_0"; + let storage_path_1 = "test-storage/claim_2_native_tokens_no_outputs_in_claim_account_1"; + setup(storage_path_0)?; + setup(storage_path_1)?; - let wallet = make_wallet(storage_path, None, None).await?; + let wallet_0 = make_wallet(storage_path_0, None, None).await?; + let wallet_1 = make_wallet(storage_path_1, None, None).await?; - let account_0 = &create_accounts_with_funds(&wallet, 1).await?[0]; - let account_1 = wallet.create_account().finish().await?; + request_funds(&wallet_0).await?; let native_token_amount = U256::from(100); - let tx = account_0.create_account_output(None, None).await?; - account_0 + let tx = wallet_0.create_account_output(None, None).await?; + wallet_0 .reissue_transaction_until_included(&tx.transaction_id, None, None) .await?; - account_0.sync(None).await?; + wallet_0.sync(None).await?; - let create_tx_0 = account_0 + let create_tx_0 = wallet_0 .create_native_token( CreateNativeTokenParams { account_id: None, @@ -300,12 +316,12 @@ async fn claim_2_native_tokens_no_outputs_in_claim_account() -> Result<()> { None, ) .await?; - account_0 + wallet_0 .reissue_transaction_until_included(&create_tx_0.transaction.transaction_id, None, None) .await?; - account_0.sync(None).await?; + wallet_0.sync(None).await?; - let create_tx_1 = account_0 + let create_tx_1 = wallet_0 .create_native_token( CreateNativeTokenParams { account_id: None, @@ -316,30 +332,30 @@ async fn claim_2_native_tokens_no_outputs_in_claim_account() -> Result<()> { None, ) .await?; - account_0 + wallet_0 .reissue_transaction_until_included(&create_tx_1.transaction.transaction_id, None, None) .await?; - account_0.sync(None).await?; + wallet_0.sync(None).await?; - let rent_structure = account_0.client().get_rent_structure().await?; - let token_supply = account_0.client().get_token_supply().await?; + let rent_structure = wallet_0.client().get_rent_structure().await?; + let token_supply = wallet_0.client().get_token_supply().await?; - let tx = account_0 + let tx = wallet_0 .send_outputs( [ BasicOutputBuilder::new_with_minimum_storage_deposit(rent_structure) - .add_unlock_condition(AddressUnlockCondition::new(account_1.first_address_bech32().await)) + .add_unlock_condition(AddressUnlockCondition::new(wallet_1.address().await)) .add_unlock_condition(ExpirationUnlockCondition::new( - account_0.first_address_bech32().await, - account_0.client().get_slot_index().await? + 5000, + wallet_0.address().await, + wallet_0.client().get_slot_index().await? + 5000, )?) .add_native_token(NativeToken::new(create_tx_0.token_id, native_token_amount)?) .finish_output(token_supply)?, BasicOutputBuilder::new_with_minimum_storage_deposit(rent_structure) - .add_unlock_condition(AddressUnlockCondition::new(account_1.first_address_bech32().await)) + .add_unlock_condition(AddressUnlockCondition::new(wallet_1.address().await)) .add_unlock_condition(ExpirationUnlockCondition::new( - account_0.first_address_bech32().await, - account_0.client().get_slot_index().await? + 5000, + wallet_0.address().await, + wallet_0.client().get_slot_index().await? + 5000, )?) .add_native_token(NativeToken::new(create_tx_1.token_id, native_token_amount)?) .finish_output(token_supply)?, @@ -347,22 +363,22 @@ async fn claim_2_native_tokens_no_outputs_in_claim_account() -> Result<()> { None, ) .await?; - account_0 + wallet_0 .reissue_transaction_until_included(&tx.transaction_id, None, None) .await?; // Claim with account 1 - let balance = account_1.sync(None).await.unwrap(); + let balance = wallet_1.sync(None).await.unwrap(); assert_eq!(balance.potentially_locked_outputs().len(), 2); - let tx = account_1 - .claim_outputs(account_1.claimable_outputs(OutputsToClaim::NativeTokens).await?) + let tx = wallet_1 + .claim_outputs(wallet_1.claimable_outputs(OutputsToClaim::NativeTokens).await?) .await?; - account_1 + wallet_1 .reissue_transaction_until_included(&tx.transaction_id, None, None) .await?; - let balance = account_1.sync(None).await.unwrap(); + let balance = wallet_1.sync(None).await.unwrap(); assert_eq!(balance.potentially_locked_outputs().len(), 0); assert_eq!(balance.native_tokens().len(), 2); let native_token_0 = balance @@ -378,137 +394,154 @@ async fn claim_2_native_tokens_no_outputs_in_claim_account() -> Result<()> { .unwrap(); assert_eq!(native_token_1.total(), native_token_amount); - tear_down(storage_path) + tear_down(storage_path_0)?; + tear_down(storage_path_1)?; + + Ok(()) } #[ignore] #[tokio::test] async fn claim_2_nft_outputs() -> Result<()> { - let storage_path = "test-storage/claim_2_nft_outputs"; - setup(storage_path)?; + let storage_path_0 = "test-storage/claim_2_nft_outputs_0"; + let storage_path_1 = "test-storage/claim_2_nft_outputs_1"; + setup(storage_path_0)?; + setup(storage_path_1)?; - let wallet = make_wallet(storage_path, None, None).await?; + let wallet_0 = make_wallet(storage_path_0, None, None).await?; + let wallet_1 = make_wallet(storage_path_1, None, None).await?; - let accounts = create_accounts_with_funds(&wallet, 2).await?; + request_funds(&wallet_0).await?; + request_funds(&wallet_1).await?; - let token_supply = accounts[1].client().get_token_supply().await?; + let token_supply = wallet_1.client().get_token_supply().await?; let outputs = [ // address of the owner of the NFT NftOutputBuilder::new_with_amount(1_000_000, NftId::null()) .with_unlock_conditions([ - UnlockCondition::Address(AddressUnlockCondition::new(accounts[0].first_address_bech32().await)), + UnlockCondition::Address(AddressUnlockCondition::new(wallet_0.address().await)), UnlockCondition::Expiration(ExpirationUnlockCondition::new( - accounts[1].first_address_bech32().await, - accounts[1].client().get_slot_index().await? + 5000, + wallet_1.address().await, + wallet_1.client().get_slot_index().await? + 5000, )?), ]) .finish_output(token_supply)?, NftOutputBuilder::new_with_amount(1_000_000, NftId::null()) .with_unlock_conditions([ - UnlockCondition::Address(AddressUnlockCondition::new(accounts[0].first_address_bech32().await)), + UnlockCondition::Address(AddressUnlockCondition::new(wallet_0.address().await)), UnlockCondition::Expiration(ExpirationUnlockCondition::new( - accounts[1].first_address_bech32().await, - accounts[1].client().get_slot_index().await? + 5000, + wallet_1.address().await, + wallet_1.client().get_slot_index().await? + 5000, )?), ]) .finish_output(token_supply)?, ]; - let tx = accounts[1].send_outputs(outputs, None).await?; - accounts[1] + let tx = wallet_1.send_outputs(outputs, None).await?; + wallet_1 .reissue_transaction_until_included(&tx.transaction_id, None, None) .await?; // Claim with account 0 - let balance = accounts[0].sync(None).await.unwrap(); + let balance = wallet_0.sync(None).await.unwrap(); assert_eq!(balance.potentially_locked_outputs().len(), 2); - let tx = accounts[0] - .claim_outputs(accounts[0].claimable_outputs(OutputsToClaim::Nfts).await?) + let tx = wallet_0 + .claim_outputs(wallet_0.claimable_outputs(OutputsToClaim::Nfts).await?) .await?; - accounts[0] + wallet_0 .reissue_transaction_until_included(&tx.transaction_id, None, None) .await?; - let balance = accounts[0].sync(None).await.unwrap(); + let balance = wallet_0.sync(None).await.unwrap(); assert_eq!(balance.potentially_locked_outputs().len(), 0); assert_eq!(balance.nfts().len(), 2); - tear_down(storage_path) + tear_down(storage_path_0)?; + tear_down(storage_path_1)?; + + Ok(()) } #[ignore] #[tokio::test] async fn claim_2_nft_outputs_no_outputs_in_claim_account() -> Result<()> { - let storage_path = "test-storage/claim_2_nft_outputs_no_outputs_in_claim_account"; - setup(storage_path)?; + let storage_path_0 = "test-storage/claim_2_nft_outputs_no_outputs_in_claim_wallet_0"; + let storage_path_1 = "test-storage/claim_2_nft_outputs_no_outputs_in_claim_wallet_1"; + setup(storage_path_0)?; + setup(storage_path_1)?; - let wallet = make_wallet(storage_path, None, None).await?; + let wallet_0 = make_wallet(storage_path_0, None, None).await?; + let wallet_1 = make_wallet(storage_path_1, None, None).await?; - let account_0 = &create_accounts_with_funds(&wallet, 1).await?[0]; - let account_1 = wallet.create_account().finish().await?; + request_funds(&wallet_0).await?; - let token_supply = account_0.client().get_token_supply().await?; + let token_supply = wallet_0.client().get_token_supply().await?; let outputs = [ // address of the owner of the NFT NftOutputBuilder::new_with_amount(1_000_000, NftId::null()) .with_unlock_conditions([ - UnlockCondition::Address(AddressUnlockCondition::new(account_1.first_address_bech32().await)), + UnlockCondition::Address(AddressUnlockCondition::new(wallet_1.address().await)), UnlockCondition::Expiration(ExpirationUnlockCondition::new( - account_0.first_address_bech32().await, - account_0.client().get_slot_index().await? + 5000, + wallet_0.address().await, + wallet_0.client().get_slot_index().await? + 5000, )?), ]) .finish_output(token_supply)?, NftOutputBuilder::new_with_amount(1_000_000, NftId::null()) .with_unlock_conditions([ - UnlockCondition::Address(AddressUnlockCondition::new(account_1.first_address_bech32().await)), + UnlockCondition::Address(AddressUnlockCondition::new(wallet_1.address().await)), UnlockCondition::Expiration(ExpirationUnlockCondition::new( - account_0.first_address_bech32().await, - account_0.client().get_slot_index().await? + 5000, + wallet_0.address().await, + wallet_0.client().get_slot_index().await? + 5000, )?), ]) .finish_output(token_supply)?, ]; - let tx = account_0.send_outputs(outputs, None).await?; - account_0 + let tx = wallet_0.send_outputs(outputs, None).await?; + wallet_0 .reissue_transaction_until_included(&tx.transaction_id, None, None) .await?; - // Claim with account 1 - let balance = account_1.sync(None).await.unwrap(); + // Claim with wallet 1 + let balance = wallet_1.sync(None).await.unwrap(); assert_eq!(balance.potentially_locked_outputs().len(), 2); - let tx = account_1 - .claim_outputs(account_1.claimable_outputs(OutputsToClaim::Nfts).await?) + let tx = wallet_1 + .claim_outputs(wallet_1.claimable_outputs(OutputsToClaim::Nfts).await?) .await?; - account_1 + wallet_1 .reissue_transaction_until_included(&tx.transaction_id, None, None) .await?; - let balance = account_1.sync(None).await.unwrap(); + let balance = wallet_1.sync(None).await.unwrap(); assert_eq!(balance.potentially_locked_outputs().len(), 0); assert_eq!(balance.nfts().len(), 2); - tear_down(storage_path) + tear_down(storage_path_0)?; + tear_down(storage_path_1)?; + + Ok(()) } #[ignore] #[tokio::test] async fn claim_basic_micro_output_error() -> Result<()> { - let storage_path = "test-storage/claim_basic_micro_output_error"; - setup(storage_path)?; + let storage_path_0 = "test-storage/claim_basic_micro_output_error_0"; + let storage_path_1 = "test-storage/claim_basic_micro_output_error_1"; + setup(storage_path_0)?; + setup(storage_path_1)?; - let wallet = make_wallet(storage_path, None, None).await?; + let wallet_0 = make_wallet(storage_path_0, None, None).await?; + let wallet_1 = make_wallet(storage_path_1, None, None).await?; - let account_0 = &create_accounts_with_funds(&wallet, 1).await?[0]; - let account_1 = wallet.create_account().finish().await?; + request_funds(&wallet_0).await?; let micro_amount = 1; - let tx = account_0 + let tx = wallet_0 .send_with_params( - [SendParams::new(micro_amount, account_1.first_address_bech32().await)?], + [SendParams::new(micro_amount, wallet_1.address().await)?], TransactionOptions { allow_micro_amount: true, ..Default::default() @@ -516,18 +549,21 @@ async fn claim_basic_micro_output_error() -> Result<()> { ) .await?; - account_0 + wallet_0 .reissue_transaction_until_included(&tx.transaction_id, None, None) .await?; // Try claim with account 1 will fail since it has no funds to cover the storage deposit - let balance = account_1.sync(None).await.unwrap(); + let balance = wallet_1.sync(None).await.unwrap(); assert_eq!(balance.potentially_locked_outputs().len(), 1); - let result = account_1 - .claim_outputs(account_1.claimable_outputs(OutputsToClaim::MicroTransactions).await?) + let result = wallet_1 + .claim_outputs(wallet_1.claimable_outputs(OutputsToClaim::MicroTransactions).await?) .await; assert!(matches!(result, Err(iota_sdk::wallet::Error::InsufficientFunds { .. }))); - tear_down(storage_path) + tear_down(storage_path_0)?; + tear_down(storage_path_1)?; + + Ok(()) } diff --git a/sdk/tests/wallet/common/mod.rs b/sdk/tests/wallet/common/mod.rs index 732a0abb41..bca29f4e3d 100644 --- a/sdk/tests/wallet/common/mod.rs +++ b/sdk/tests/wallet/common/mod.rs @@ -3,7 +3,7 @@ mod constants; -use crypto::keys::bip39::Mnemonic; +use crypto::keys::{bip39::Mnemonic, bip44::Bip44}; use iota_sdk::{ client::{ constants::SHIMMER_COIN_TYPE, @@ -11,7 +11,7 @@ use iota_sdk::{ secret::{mnemonic::MnemonicSecretManager, SecretManager}, Client, }, - wallet::{Account, ClientOptions, Result, Wallet}, + wallet::{ClientOptions, Result, Wallet}, }; pub use self::constants::*; @@ -27,7 +27,7 @@ pub use self::constants::*; /// /// Returns: /// -/// An Wallet +/// A Wallet #[allow(dead_code, unused_variables)] pub(crate) async fn make_wallet(storage_path: &str, mnemonic: Option, node: Option<&str>) -> Result { let client_options = ClientOptions::new().with_node(node.unwrap_or(NODE_LOCAL))?; @@ -38,7 +38,8 @@ pub(crate) async fn make_wallet(storage_path: &str, mnemonic: Option, let mut wallet_builder = Wallet::builder() .with_secret_manager(SecretManager::Mnemonic(secret_manager)) .with_client_options(client_options) - .with_coin_type(SHIMMER_COIN_TYPE); + .with_bip_path(Bip44::new(SHIMMER_COIN_TYPE)); + #[cfg(feature = "storage")] { wallet_builder = wallet_builder.with_storage_path(storage_path); @@ -58,7 +59,7 @@ pub(crate) async fn make_ledger_nano_wallet(storage_path: &str, node: Option<&st let mut wallet_builder = Wallet::builder() .with_secret_manager(SecretManager::LedgerNano(secret_manager)) .with_client_options(client_options) - .with_coin_type(SHIMMER_COIN_TYPE); + .with_bip_path(Bip44::new(SHIMMER_COIN_TYPE)); #[cfg(feature = "storage")] { wallet_builder = wallet_builder.with_storage_path(storage_path); @@ -67,28 +68,20 @@ pub(crate) async fn make_ledger_nano_wallet(storage_path: &str, node: Option<&st wallet_builder.finish().await } -/// Create `amount` new accounts, request funds from the faucet and sync the accounts afterwards until the faucet output -/// is available. Returns the new accounts. +/// Request funds from the faucet and sync the wallet. #[allow(dead_code)] -pub(crate) async fn create_accounts_with_funds(wallet: &Wallet, amount: usize) -> Result> { - let mut new_accounts = Vec::new(); - 'accounts: for _ in 0..amount { - let account = wallet.create_account().finish().await?; - request_funds_from_faucet(FAUCET_URL, &account.first_address_bech32().await).await?; +pub(crate) async fn request_funds(wallet: &Wallet) -> Result<()> { + request_funds_from_faucet(FAUCET_URL, &wallet.address().await).await?; - // Continue only after funds are received - for _ in 0..30 { - tokio::time::sleep(std::time::Duration::from_secs(2)).await; - let balance = account.sync(None).await?; - if balance.base_coin().available() > 0 { - new_accounts.push(account); - continue 'accounts; - } + // Continue only after funds are received + for _ in 0..30 { + tokio::time::sleep(std::time::Duration::from_secs(2)).await; + let balance = wallet.sync(None).await?; + if balance.base_coin().available() > 0 { + return Ok(()); } - panic!("Faucet no longer wants to hand over coins"); } - - Ok(new_accounts) + panic!("Faucet no longer wants to hand over coins"); } #[allow(dead_code)] diff --git a/sdk/tests/wallet/consolidation.rs b/sdk/tests/wallet/consolidation.rs index c3fc3e8a7b..69f292f8d2 100644 --- a/sdk/tests/wallet/consolidation.rs +++ b/sdk/tests/wallet/consolidation.rs @@ -1,51 +1,53 @@ // Copyright 2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use iota_sdk::wallet::{account::ConsolidationParams, Result, SendParams}; +use iota_sdk::wallet::{ConsolidationParams, Result, SendParams}; use pretty_assertions::assert_eq; -use crate::wallet::common::{create_accounts_with_funds, make_wallet, setup, tear_down}; +use crate::wallet::common::{make_wallet, request_funds, setup, tear_down}; #[ignore] #[tokio::test] async fn consolidation() -> Result<()> { - let storage_path = "test-storage/consolidation"; - setup(storage_path)?; + let storage_path_0 = "test-storage/consolidation_0"; + let storage_path_1 = "test-storage/consolidation_1"; + setup(storage_path_0)?; + setup(storage_path_1)?; - let wallet = make_wallet(storage_path, None, None).await?; + let wallet_0 = make_wallet(storage_path_0, None, None).await?; + let wallet_1 = make_wallet(storage_path_1, None, None).await?; - let account_0 = &create_accounts_with_funds(&wallet, 1).await?[0]; - let account_1 = wallet.create_account().finish().await?; + request_funds(&wallet_0).await?; - // Send 10 outputs to account_1 + // Send 10 outputs to wallet_1 let amount = 1_000_000; - let tx = account_0 - .send_with_params( - vec![SendParams::new(amount, account_1.first_address_bech32().await)?; 10], - None, - ) + let tx = wallet_0 + .send_with_params(vec![SendParams::new(amount, wallet_1.address().await)?; 10], None) .await?; - account_0 + wallet_0 .reissue_transaction_until_included(&tx.transaction_id, None, None) .await?; - let balance = account_1.sync(None).await.unwrap(); + let balance = wallet_1.sync(None).await.unwrap(); assert_eq!(balance.base_coin().available(), 10 * amount); - assert_eq!(account_1.unspent_outputs(None).await?.len(), 10); + assert_eq!(wallet_1.unspent_outputs(None).await?.len(), 10); - let tx = account_1 + let tx = wallet_1 .consolidate_outputs(ConsolidationParams::new().with_force(true)) .await?; - account_1 + wallet_1 .reissue_transaction_until_included(&tx.transaction_id, None, None) .await?; - let balance = account_1.sync(None).await.unwrap(); + let balance = wallet_1.sync(None).await.unwrap(); // Balance still the same assert_eq!(balance.base_coin().available(), 10 * amount); // Only one unspent output - assert_eq!(account_1.unspent_outputs(None).await?.len(), 1); + assert_eq!(wallet_1.unspent_outputs(None).await?.len(), 1); - tear_down(storage_path) + tear_down(storage_path_0)?; + tear_down(storage_path_1)?; + + Ok(()) } diff --git a/sdk/tests/wallet/core.rs b/sdk/tests/wallet/core.rs index 3afe917b18..8723afe641 100644 --- a/sdk/tests/wallet/core.rs +++ b/sdk/tests/wallet/core.rs @@ -13,6 +13,7 @@ use iota_sdk::{ constants::IOTA_COIN_TYPE, secret::{mnemonic::MnemonicSecretManager, SecretManager}, }, + crypto::keys::bip44::Bip44, types::block::address::Bech32Address, wallet::{ClientOptions, Result, Wallet}, }; @@ -56,70 +57,65 @@ async fn update_client_options() -> Result<()> { tear_down(storage_path) } -#[cfg(feature = "storage")] -#[tokio::test] -async fn different_seed() -> Result<()> { - let storage_path = "test-storage/different_seed"; - setup(storage_path)?; +// #[cfg(feature = "storage")] +// #[tokio::test] +// async fn different_seed() -> Result<()> { +// let storage_path = "test-storage/different_seed"; +// setup(storage_path)?; - let wallet = make_wallet(storage_path, None, None).await?; - let _account = wallet.create_account().with_alias("Alice").finish().await?; +// let wallet = make_wallet(storage_path, None, None).await?; - drop(_account); - drop(wallet); +// drop(wallet); - // Recreate Wallet with a different mnemonic - let wallet = make_wallet(storage_path, None, None).await?; - - // Generating a new account needs to return an error, because the seed from the secret_manager is different - assert!(wallet.create_account().with_alias("Bob").finish().await.is_err()); +// // Recreate Wallet with a different mnemonic +// // Generating a new wallet needs to return an error, because the seed from the secret_manager is different +// assert!(make_wallet(storage_path, None, None).await.is_err()); - tear_down(storage_path) -} +// tear_down(storage_path) +// } #[cfg(feature = "storage")] #[tokio::test] -async fn changed_coin_type() -> Result<()> { +async fn changed_bip_path() -> Result<()> { + use iota_sdk::crypto::keys::bip44::Bip44; + let storage_path = "test-storage/changed_coin_type"; setup(storage_path)?; let mnemonic = Mnemonic::from(DEFAULT_MNEMONIC.to_owned()); let wallet = make_wallet(storage_path, Some(mnemonic.clone()), None).await?; - let _account = wallet.create_account().with_alias("Alice").finish().await?; - drop(_account); drop(wallet); let err = Wallet::builder() .with_secret_manager(SecretManager::Mnemonic(MnemonicSecretManager::try_from_mnemonic( mnemonic.clone(), )?)) - .with_coin_type(IOTA_COIN_TYPE) + .with_bip_path(Bip44::new(IOTA_COIN_TYPE)) .with_storage_path(storage_path) .finish() .await; // Building the wallet with another coin type needs to return an error, because a different coin type was used in // the existing account - assert!(matches!( - err, - Err(Error::InvalidCoinType { - new_coin_type: IOTA_COIN_TYPE, - existing_coin_type: SHIMMER_COIN_TYPE - }) - )); + let mismatch_err: Result = Err(Error::BipPathMismatch { + new_bip_path: Some(Bip44::new(IOTA_COIN_TYPE)), + old_bip_path: Some(Bip44::new(SHIMMER_COIN_TYPE)), + }); + assert!(matches!(err, mismatch_err)); // Building the wallet with the same coin type still works - let wallet = Wallet::builder() - .with_secret_manager(SecretManager::Mnemonic(MnemonicSecretManager::try_from_mnemonic( - mnemonic, - )?)) - .with_storage_path(storage_path) - .finish() - .await?; - // Also still possible to create a new account - assert!(wallet.create_account().with_alias("Bob").finish().await.is_ok()); + assert!( + Wallet::builder() + .with_secret_manager(SecretManager::Mnemonic(MnemonicSecretManager::try_from_mnemonic( + mnemonic, + )?)) + .with_storage_path(storage_path) + .finish() + .await + .is_ok() + ); tear_down(storage_path) } @@ -130,11 +126,10 @@ async fn shimmer_coin_type() -> Result<()> { setup(storage_path)?; let wallet = make_wallet(storage_path, Some(Mnemonic::from(DEFAULT_MNEMONIC.to_owned())), None).await?; - let account = wallet.create_account().finish().await?; // Creating a new account with providing a coin type will use the Shimmer coin type with shimmer testnet bech32 hrp assert_eq!( - Bech32Address::try_new("smr", account.first_address_bech32().await)?.to_string(), + Bech32Address::try_new("smr", wallet.address().await)?.to_string(), // Address generated with bip32 path: [44, 4219, 0, 0, 0] "smr1qq724zgvdujt3jdcd3xzsuqq7wl9pwq3dvsa5zvx49rj9tme8cat65xq7jz" ); @@ -154,7 +149,7 @@ async fn iota_coin_type() -> Result<()> { let mut wallet_builder = Wallet::builder() .with_secret_manager(SecretManager::Mnemonic(secret_manager)) .with_client_options(client_options) - .with_coin_type(IOTA_COIN_TYPE); + .with_bip_path(Bip44::new(IOTA_COIN_TYPE)); #[cfg(feature = "storage")] { @@ -162,11 +157,9 @@ async fn iota_coin_type() -> Result<()> { } let wallet = wallet_builder.finish().await?; - let account = wallet.create_account().finish().await?; - // Creating a new account with providing a coin type will use the iota coin type with shimmer testnet bech32 hrp assert_eq!( - Bech32Address::try_new("smr", account.first_address_bech32().await)?.to_string(), + Bech32Address::try_new("smr", wallet.address().await)?.to_string(), // Address generated with bip32 path: [44, 4218, 0, 0, 0] "smr1qrpwecegav7eh0z363ca69laxej64rrt4e3u0rtycyuh0mam3vq3ulygj9p" ); diff --git a/sdk/tests/wallet/error.rs b/sdk/tests/wallet/error.rs index a80fd657b9..037ab03582 100644 --- a/sdk/tests/wallet/error.rs +++ b/sdk/tests/wallet/error.rs @@ -6,12 +6,21 @@ use pretty_assertions::assert_eq; #[test] fn stringified_error() { - let error = Error::AccountNotFound("0".into()); + // testing a unit-type-like error + let error = Error::MissingBipPath; assert_eq!( &serde_json::to_string(&error).unwrap(), - "{\"type\":\"accountNotFound\",\"error\":\"account 0 not found\"}" + "{\"type\":\"missingBipPath\",\"error\":\"missing BIP path\"}" ); + // testing a tuple-like error + let error = Error::InvalidMnemonic("nilly willy".to_string()); + assert_eq!( + serde_json::to_string(&error).unwrap(), + "{\"type\":\"invalidMnemonic\",\"error\":\"invalid mnemonic: nilly willy\"}" + ); + + // testing a struct-like error let error = Error::NoOutputsToConsolidate { available_outputs: 0, consolidation_threshold: 0, @@ -20,10 +29,4 @@ fn stringified_error() { &serde_json::to_string(&error).unwrap(), "{\"type\":\"noOutputsToConsolidate\",\"error\":\"nothing to consolidate: available outputs: 0, consolidation threshold: 0\"}" ); - - let error = Error::FailedToGetRemainder; - assert_eq!( - &serde_json::to_string(&error).unwrap(), - "{\"type\":\"failedToGetRemainder\",\"error\":\"failed to get remainder address\"}" - ); } diff --git a/sdk/tests/wallet/events.rs b/sdk/tests/wallet/events.rs index b0e59eb4ce..481d3ba105 100644 --- a/sdk/tests/wallet/events.rs +++ b/sdk/tests/wallet/events.rs @@ -15,11 +15,11 @@ use iota_sdk::{ }, }, wallet::{ - account::types::{InclusionState, OutputData, OutputDataDto}, events::types::{ AddressData, NewOutputEvent, SpentOutputEvent, TransactionInclusionEvent, TransactionProgressEvent, WalletEvent, }, + types::{InclusionState, OutputData, OutputDataDto}, }, }; use pretty_assertions::assert_eq; diff --git a/sdk/tests/wallet/migrate_stronghold_snapshot_v2_to_v3.rs b/sdk/tests/wallet/migrate_stronghold_snapshot_v2_to_v3.rs index 5862ce3aef..1bd1747ecb 100644 --- a/sdk/tests/wallet/migrate_stronghold_snapshot_v2_to_v3.rs +++ b/sdk/tests/wallet/migrate_stronghold_snapshot_v2_to_v3.rs @@ -12,6 +12,7 @@ use iota_sdk::{ stronghold::{Error as StrongholdError, StrongholdAdapter}, Error as ClientError, }, + crypto::keys::bip44::Bip44, wallet::{ClientOptions, Error as WalletError, Wallet}, }; use pretty_assertions::assert_eq; @@ -88,7 +89,7 @@ async fn stronghold_snapshot_v2_v3_migration() { .with_secret_manager(stronghold_secret_manager) .with_client_options(ClientOptions::new().with_node(NODE_LOCAL).unwrap()) // Build with a different coin type, to check if it gets replaced by the one from the backup - .with_coin_type(IOTA_COIN_TYPE) + .with_bip_path(Bip44::new(IOTA_COIN_TYPE)) .finish() .await .unwrap(); diff --git a/sdk/tests/wallet/mod.rs b/sdk/tests/wallet/mod.rs index 60058449ce..0cb359191e 100644 --- a/sdk/tests/wallet/mod.rs +++ b/sdk/tests/wallet/mod.rs @@ -1,8 +1,6 @@ // Copyright 2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -mod account_recovery; -mod accounts; mod address_generation; #[cfg(all(feature = "stronghold", feature = "storage"))] mod backup_restore; diff --git a/sdk/tests/wallet/native_tokens.rs b/sdk/tests/wallet/native_tokens.rs index b0bc9bd239..a392e8fdd4 100644 --- a/sdk/tests/wallet/native_tokens.rs +++ b/sdk/tests/wallet/native_tokens.rs @@ -2,12 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 use iota_sdk::{ - wallet::{account::SyncOptions, CreateNativeTokenParams, Result}, + wallet::{CreateNativeTokenParams, Result, SyncOptions}, U256, }; use pretty_assertions::assert_eq; -use crate::wallet::common::{create_accounts_with_funds, make_wallet, setup, tear_down}; +use crate::wallet::common::{make_wallet, request_funds, setup, tear_down}; #[ignore] #[tokio::test] @@ -17,15 +17,15 @@ async fn create_and_mint_native_token() -> Result<()> { let wallet = make_wallet(storage_path, None, None).await?; - let account = &create_accounts_with_funds(&wallet, 1).await?[0]; + request_funds(&wallet).await?; - let tx = account.create_account_output(None, None).await?; - account + let tx = wallet.create_account_output(None, None).await?; + wallet .reissue_transaction_until_included(&tx.transaction_id, None, None) .await?; - account.sync(None).await?; + wallet.sync(None).await?; - let create_tx = account + let create_tx = wallet .create_native_token( CreateNativeTokenParams { account_id: None, @@ -36,10 +36,10 @@ async fn create_and_mint_native_token() -> Result<()> { None, ) .await?; - account + wallet .reissue_transaction_until_included(&create_tx.transaction.transaction_id, None, None) .await?; - let balance = account.sync(None).await?; + let balance = wallet.sync(None).await?; assert_eq!(balance.native_tokens().len(), 1); assert_eq!( balance @@ -51,11 +51,11 @@ async fn create_and_mint_native_token() -> Result<()> { U256::from(50) ); - let tx = account.mint_native_token(create_tx.token_id, 50, None).await?; - account + let tx = wallet.mint_native_token(create_tx.token_id, 50, None).await?; + wallet .reissue_transaction_until_included(&tx.transaction_id, None, None) .await?; - let balance = account.sync(None).await?; + let balance = wallet.sync(None).await?; assert_eq!(balance.native_tokens().len(), 1); assert_eq!( balance @@ -78,17 +78,17 @@ async fn native_token_foundry_metadata() -> Result<()> { let wallet = make_wallet(storage_path, None, None).await?; - let account = &create_accounts_with_funds(&wallet, 1).await?[0]; + request_funds(&wallet).await?; - let tx = account.create_account_output(None, None).await?; - account + let tx = wallet.create_account_output(None, None).await?; + wallet .reissue_transaction_until_included(&tx.transaction_id, None, None) .await?; - account.sync(None).await?; + wallet.sync(None).await?; let foundry_metadata = [1, 3, 3, 7]; - let create_tx = account + let create_tx = wallet .create_native_token( CreateNativeTokenParams { account_id: None, @@ -99,11 +99,11 @@ async fn native_token_foundry_metadata() -> Result<()> { None, ) .await?; - account + wallet .reissue_transaction_until_included(&create_tx.transaction.transaction_id, None, None) .await?; // Sync native_token_foundries to get the metadata - let balance = account + let balance = wallet .sync(Some(SyncOptions { sync_native_token_foundries: true, ..Default::default() diff --git a/sdk/tests/wallet/output_preparation.rs b/sdk/tests/wallet/output_preparation.rs index 1f58006536..5b0957c0ba 100644 --- a/sdk/tests/wallet/output_preparation.rs +++ b/sdk/tests/wallet/output_preparation.rs @@ -9,14 +9,11 @@ use iota_sdk::{ output::{MinimumStorageDepositBasicOutput, NativeToken, NftId, Output, Rent, TokenId}, slot::SlotIndex, }, - wallet::{ - account::{Assets, Features, OutputParams, ReturnStrategy, StorageDeposit, Unlocks}, - MintNftParams, Result, - }, + wallet::{Assets, Features, MintNftParams, OutputParams, Result, ReturnStrategy, StorageDeposit, Unlocks}, }; use pretty_assertions::assert_eq; -use crate::wallet::common::{create_accounts_with_funds, make_wallet, setup, tear_down}; +use crate::wallet::common::{make_wallet, request_funds, setup, tear_down}; #[ignore] #[tokio::test] @@ -25,14 +22,14 @@ async fn output_preparation() -> Result<()> { setup(storage_path)?; let wallet = make_wallet(storage_path, None, None).await?; - let account = &create_accounts_with_funds(&wallet, 1).await?[0]; + request_funds(&wallet).await?; let recipient_address_bech32 = String::from("rms1qpszqzadsym6wpppd6z037dvlejmjuke7s24hm95s9fg9vpua7vluaw60xu"); // Roundtrip to get the correct bech32 HRP let recipient_address = - Address::try_from_bech32(&recipient_address_bech32)?.to_bech32(account.client().get_bech32_hrp().await?); + Address::try_from_bech32(&recipient_address_bech32)?.to_bech32(wallet.client().get_bech32_hrp().await?); - let output = account + let output = wallet .prepare_output( OutputParams { recipient_address: recipient_address.clone(), @@ -51,7 +48,7 @@ async fn output_preparation() -> Result<()> { let sdr = output.unlock_conditions().unwrap().storage_deposit_return().unwrap(); assert_eq!(sdr.amount(), 46300); - let output = account + let output = wallet .prepare_output( OutputParams { recipient_address: recipient_address.clone(), @@ -72,7 +69,7 @@ async fn output_preparation() -> Result<()> { TokenId::from_str("0x08847bd287c912fadedb6bf38900bda9f2d377b75b2a0bece8738699f56ebca4130100000000")?, 10, )?; - let output = account + let output = wallet .prepare_output( OutputParams { recipient_address: recipient_address.clone(), @@ -93,7 +90,7 @@ async fn output_preparation() -> Result<()> { assert_eq!(output.unlock_conditions().unwrap().len(), 1); assert_eq!(output.native_tokens().unwrap().first(), Some(&native_token)); - let output = account + let output = wallet .prepare_output( OutputParams { recipient_address: recipient_address.clone(), @@ -118,7 +115,7 @@ async fn output_preparation() -> Result<()> { assert_eq!(output.features().unwrap().len(), 2); // only send 1 with metadata feature - let output = account + let output = wallet .prepare_output( OutputParams { recipient_address: recipient_address.clone(), @@ -146,7 +143,7 @@ async fn output_preparation() -> Result<()> { // metadata and tag features assert_eq!(output.features().unwrap().len(), 2); - let output = account + let output = wallet .prepare_output( OutputParams { recipient_address: recipient_address.clone(), @@ -171,7 +168,7 @@ async fn output_preparation() -> Result<()> { // metadata and tag features assert_eq!(output.features().unwrap().len(), 2); - let output = account + let output = wallet .prepare_output( OutputParams { recipient_address: recipient_address.clone(), @@ -199,8 +196,8 @@ async fn output_preparation() -> Result<()> { // metadata and tag features assert_eq!(output.features().unwrap().len(), 2); - // Error if this NftId is not in the account - let error = account + // Error if this NftId is not in the wallet + let error = wallet .prepare_output( OutputParams { recipient_address: recipient_address.clone(), @@ -224,7 +221,7 @@ async fn output_preparation() -> Result<()> { _ => panic!("should return NftNotFoundInUnspentOutputs error"), } - if let Ok(output) = account + if let Ok(output) = wallet .prepare_output( OutputParams { recipient_address: recipient_address.clone(), @@ -252,13 +249,11 @@ async fn output_preparation() -> Result<()> { let issuer_and_sender_address_bech32 = Bech32Address::try_from_str("rms1qq724zgvdujt3jdcd3xzsuqq7wl9pwq3dvsa5zvx49rj9tme8cat6qptyfm")?; // Roundtrip to get the correct bech32 HRP - let issuer_and_sender_address = issuer_and_sender_address_bech32 - .into_inner() - .to_bech32(account.client().get_bech32_hrp().await?); + let issuer_and_sender_address = issuer_and_sender_address_bech32.to_bech32(wallet.client().get_bech32_hrp().await?); let expected_address = issuer_and_sender_address.inner(); // sender address present when building basic output - let output = account + let output = wallet .prepare_output( OutputParams { recipient_address: recipient_address.clone(), @@ -288,7 +283,7 @@ async fn output_preparation() -> Result<()> { assert_eq!(features.sender().unwrap().address(), expected_address); // error when adding issuer when building basic output - let error = account + let error = wallet .prepare_output( OutputParams { recipient_address: recipient_address.clone(), @@ -313,7 +308,7 @@ async fn output_preparation() -> Result<()> { } // issuer and sender address present when building nft output - let output = account + let output = wallet .prepare_output( OutputParams { recipient_address: recipient_address.clone(), @@ -357,7 +352,7 @@ async fn output_preparation() -> Result<()> { assert!(conditions.is_expired(2)); // nft with expiration - let output = account + let output = wallet .prepare_output( OutputParams { recipient_address: recipient_address.clone(), @@ -388,7 +383,7 @@ async fn output_preparation() -> Result<()> { // address, sdr, expiration assert_eq!(output.unlock_conditions().unwrap().len(), 3); - let output = account + let output = wallet .prepare_output( OutputParams { recipient_address: recipient_address.clone(), @@ -431,17 +426,17 @@ async fn output_preparation_sdr() -> Result<()> { setup(storage_path)?; let wallet = make_wallet(storage_path, None, None).await?; - let account = &create_accounts_with_funds(&wallet, 1).await?[0]; + request_funds(&wallet).await?; - let rent_structure = account.client().get_rent_structure().await?; - let token_supply = account.client().get_token_supply().await?; + let rent_structure = wallet.client().get_rent_structure().await?; + let token_supply = wallet.client().get_token_supply().await?; let recipient_address_bech32 = String::from("rms1qpszqzadsym6wpppd6z037dvlejmjuke7s24hm95s9fg9vpua7vluaw60xu"); // Roundtrip to get the correct bech32 HRP let recipient_address = - Address::try_from_bech32(&recipient_address_bech32)?.to_bech32(account.client().get_bech32_hrp().await?); + Address::try_from_bech32(&recipient_address_bech32)?.to_bech32(wallet.client().get_bech32_hrp().await?); - let output = account + let output = wallet .prepare_output( OutputParams { recipient_address: recipient_address.clone(), @@ -462,7 +457,7 @@ async fn output_preparation_sdr() -> Result<()> { let sdr = output.unlock_conditions().unwrap().storage_deposit_return().unwrap(); assert_eq!(sdr.amount(), 42600); - let output = account + let output = wallet .prepare_output( OutputParams { recipient_address: recipient_address.clone(), @@ -484,7 +479,7 @@ async fn output_preparation_sdr() -> Result<()> { assert_eq!(sdr.amount(), 42600); // ReturnStrategy::Return provided - let output = account + let output = wallet .prepare_output( OutputParams { recipient_address: recipient_address.clone(), @@ -509,7 +504,7 @@ async fn output_preparation_sdr() -> Result<()> { assert_eq!(sdr.amount(), 42600); // ReturnStrategy::Gift provided - let output = account + let output = wallet .prepare_output( OutputParams { recipient_address: recipient_address.clone(), @@ -542,29 +537,28 @@ async fn prepare_nft_output_features_update() -> Result<()> { setup(storage_path)?; let wallet = make_wallet(storage_path, None, None).await?; - let accounts = &create_accounts_with_funds(&wallet, 1).await?; - let addresses = accounts[0].addresses().await; - let address = addresses[0].address(); + request_funds(&wallet).await?; + let wallet_address = wallet.address().await; let nft_options = [MintNftParams::new() - .with_address(address.clone()) - .with_sender(address.clone()) + .with_address(wallet_address.clone()) + .with_sender(wallet_address.clone()) .with_metadata(b"some nft metadata".to_vec()) .with_tag(b"some nft tag".to_vec()) - .with_issuer(address.clone()) + .with_issuer(wallet_address.clone()) .with_immutable_metadata(b"some immutable nft metadata".to_vec())]; - let transaction = accounts[0].mint_nfts(nft_options, None).await.unwrap(); - accounts[0] + let transaction = wallet.mint_nfts(nft_options, None).await.unwrap(); + wallet .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; - let nft_id = *accounts[0].sync(None).await?.nfts().first().unwrap(); + let nft_id = *wallet.sync(None).await?.nfts().first().unwrap(); - let nft = accounts[0] + let nft = wallet .prepare_output( OutputParams { - recipient_address: address.clone(), + recipient_address: wallet_address, amount: 1_000_000, assets: Some(Assets { native_tokens: None, @@ -586,10 +580,7 @@ async fn prepare_nft_output_features_update() -> Result<()> { .clone(); assert_eq!(nft.amount(), 1_000_000); - assert_eq!( - nft.address(), - accounts[0].addresses().await[0].clone().into_bech32().as_ref() - ); + assert_eq!(nft.address(), wallet.address().await.inner()); assert!(nft.features().sender().is_none()); assert!(nft.features().tag().is_none()); assert_eq!(nft.features().metadata().unwrap().data(), &[42]); @@ -599,7 +590,7 @@ async fn prepare_nft_output_features_update() -> Result<()> { ); assert_eq!( nft.immutable_features().issuer().unwrap().address(), - accounts[0].addresses().await[0].clone().into_bech32().as_ref() + wallet.address().await.inner() ); tear_down(storage_path) @@ -608,27 +599,28 @@ async fn prepare_nft_output_features_update() -> Result<()> { #[ignore] #[tokio::test] async fn prepare_output_remainder_dust() -> Result<()> { - let storage_path = "test-storage/prepare_output_remainder_dust"; - setup(storage_path)?; + let storage_path_0 = "test-storage/prepare_output_remainder_dust_0"; + let storage_path_1 = "test-storage/prepare_output_remainder_dust_1"; + setup(storage_path_0)?; + setup(storage_path_1)?; - let wallet = make_wallet(storage_path, None, None).await?; - let accounts = &create_accounts_with_funds(&wallet, 2).await?; - let account = &accounts[0]; - let addresses = &accounts[1].addresses().await; - let address = addresses[0].address(); + let wallet_0 = make_wallet(storage_path_0, None, None).await?; + let wallet_1 = make_wallet(storage_path_1, None, None).await?; + request_funds(&wallet_0).await?; + request_funds(&wallet_1).await?; - let rent_structure = account.client().get_rent_structure().await?; - let token_supply = account.client().get_token_supply().await?; + let rent_structure = wallet_0.client().get_rent_structure().await?; + let token_supply = wallet_0.client().get_token_supply().await?; - let balance = account.sync(None).await?; + let balance = wallet_0.sync(None).await?; let minimum_required_storage_deposit = MinimumStorageDepositBasicOutput::new(rent_structure, token_supply).finish()?; // Send away most balance so we can test with leaving dust - let output = account + let output = wallet_0 .prepare_output( OutputParams { - recipient_address: address.clone(), + recipient_address: wallet_1.address().await, amount: balance.base_coin().available() - 63900, assets: None, features: None, @@ -638,17 +630,17 @@ async fn prepare_output_remainder_dust() -> Result<()> { None, ) .await?; - let transaction = account.send_outputs(vec![output], None).await?; - account + let transaction = wallet_0.send_outputs(vec![output], None).await?; + wallet_0 .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; - let balance = account.sync(None).await?; + let balance = wallet_0.sync(None).await?; // 63900 left - let output = account + let output = wallet_0 .prepare_output( OutputParams { - recipient_address: address.clone(), + recipient_address: wallet_1.address().await, amount: minimum_required_storage_deposit - 1, // Leave less than min. deposit assets: None, features: None, @@ -669,10 +661,10 @@ async fn prepare_output_remainder_dust() -> Result<()> { // storage deposit gifted, only address unlock condition assert_eq!(output.unlock_conditions().unwrap().len(), 1); - let result = account + let result = wallet_0 .prepare_output( OutputParams { - recipient_address: address.clone(), + recipient_address: wallet_1.address().await, amount: minimum_required_storage_deposit - 1, // Leave less than min. deposit assets: None, features: None, @@ -689,10 +681,10 @@ async fn prepare_output_remainder_dust() -> Result<()> { matches!(result, Err(iota_sdk::wallet::Error::InsufficientFunds{available, required}) if available == balance.base_coin().available() && required == 85199) ); - let output = account + let output = wallet_0 .prepare_output( OutputParams { - recipient_address: address.clone(), + recipient_address: wallet_1.address().await, amount: 100, // leave more behind than min. deposit assets: None, features: None, @@ -713,10 +705,10 @@ async fn prepare_output_remainder_dust() -> Result<()> { // storage deposit gifted, only address unlock condition assert_eq!(output.unlock_conditions().unwrap().len(), 1); - let output = account + let output = wallet_0 .prepare_output( OutputParams { - recipient_address: address.clone(), + recipient_address: wallet_1.address().await, amount: 100, // leave more behind than min. deposit assets: None, features: None, @@ -740,45 +732,47 @@ async fn prepare_output_remainder_dust() -> Result<()> { let sdr = output.unlock_conditions().unwrap().storage_deposit_return().unwrap(); assert_eq!(sdr.amount(), 63900 - 100); - tear_down(storage_path) + tear_down(storage_path_0)?; + tear_down(storage_path_1)?; + + Ok(()) } #[ignore] #[tokio::test] async fn prepare_output_only_single_nft() -> Result<()> { - let storage_path = "test-storage/prepare_output_only_single_nft"; - setup(storage_path)?; + let storage_path_0 = "test-storage/prepare_output_only_single_nft_0"; + let storage_path_1 = "test-storage/prepare_output_only_single_nft_1"; + setup(storage_path_0)?; + setup(storage_path_1)?; - let wallet = make_wallet(storage_path, None, None).await?; - let account_0 = &create_accounts_with_funds(&wallet, 1).await?[0]; - // Create second account without funds, so it only gets the NFT - let account_1 = wallet.create_account().finish().await?; - let addresses = &account_0.addresses().await; - let account_0_address = addresses[0].address(); - let addresses = &account_1.addresses().await; - let account_1_address = addresses[0].address(); - - // Send NFT to second account - let tx = account_0 - .mint_nfts( - [MintNftParams::new().try_with_address(account_1_address.clone())?], - None, - ) + let wallet_0 = make_wallet(storage_path_0, None, None).await?; + request_funds(&wallet_0).await?; + + // Create second wallet without funds, so it only gets the NFT + let wallet_1 = make_wallet(storage_path_1, None, None).await?; + + let wallet_0_address = wallet_0.address().await; + let wallet_1_address = wallet_1.address().await; + + // Send NFT to second wallet + let tx = wallet_0 + .mint_nfts([MintNftParams::new().try_with_address(wallet_1_address)?], None) .await?; - account_0 + wallet_0 .reissue_transaction_until_included(&tx.transaction_id, None, None) .await?; - let balance = account_1.sync(None).await?; + let balance = wallet_1.sync(None).await?; assert_eq!(balance.nfts().len(), 1); - let nft_data = &account_1.unspent_outputs(None).await?[0]; + let nft_data = &wallet_1.unspent_outputs(None).await?[0]; let nft_id = *balance.nfts().first().unwrap(); - // Send NFT back to first account - let output = account_1 + // Send NFT back to first wallet + let output = wallet_1 .prepare_output( OutputParams { - recipient_address: account_0_address.clone(), + recipient_address: wallet_0_address, amount: nft_data.output.amount(), assets: Some(Assets { native_tokens: None, @@ -791,21 +785,24 @@ async fn prepare_output_only_single_nft() -> Result<()> { None, ) .await?; - let tx = account_1.send_outputs([output], None).await?; - account_1 + let tx = wallet_1.send_outputs([output], None).await?; + wallet_1 .reissue_transaction_until_included(&tx.transaction_id, None, None) .await?; - // account_0 now has the NFT - let balance_0 = account_0.sync(None).await?; + // wallet_0 now has the NFT + let balance_0 = wallet_0.sync(None).await?; assert_eq!(*balance_0.nfts().first().unwrap(), nft_id); - // account_1 has no NFT and also no base coin amount - let balance_1 = account_1.sync(None).await?; + // wallet_1 has no NFT and also no base coin amount + let balance_1 = wallet_1.sync(None).await?; assert!(balance_1.nfts().is_empty()); assert_eq!(balance_1.base_coin().total(), 0); - tear_down(storage_path) + tear_down(storage_path_0)?; + tear_down(storage_path_1)?; + + Ok(()) } #[ignore] @@ -815,9 +812,8 @@ async fn prepare_existing_nft_output_gift() -> Result<()> { setup(storage_path)?; let wallet = make_wallet(storage_path, None, None).await?; - let accounts = &create_accounts_with_funds(&wallet, 1).await?; - let addresses = accounts[0].addresses().await; - let address = addresses[0].address(); + request_funds(&wallet).await?; + let address = wallet.address().await; let nft_options = [MintNftParams::new() .with_address(address.clone()) @@ -827,17 +823,17 @@ async fn prepare_existing_nft_output_gift() -> Result<()> { .with_issuer(address.clone()) .with_immutable_metadata(b"some immutable nft metadata".to_vec())]; - let transaction = accounts[0].mint_nfts(nft_options, None).await.unwrap(); - accounts[0] + let transaction = wallet.mint_nfts(nft_options, None).await.unwrap(); + wallet .reissue_transaction_until_included(&transaction.transaction_id, None, None) .await?; - let nft_id = *accounts[0].sync(None).await?.nfts().first().unwrap(); + let nft_id = *wallet.sync(None).await?.nfts().first().unwrap(); - let nft = accounts[0] + let nft = wallet .prepare_output( OutputParams { - recipient_address: address.clone(), + recipient_address: address, amount: 0, assets: Some(Assets { native_tokens: None, @@ -861,10 +857,7 @@ async fn prepare_existing_nft_output_gift() -> Result<()> { assert_eq!(nft.amount(), minimum_storage_deposit); assert_eq!(nft.amount(), 52300); - assert_eq!( - nft.address(), - accounts[0].addresses().await[0].clone().into_bech32().as_ref() - ); + assert_eq!(nft.address(), wallet.address().await.inner()); assert!(nft.features().is_empty()); assert_eq!( nft.immutable_features().metadata().unwrap().data(), @@ -872,7 +865,7 @@ async fn prepare_existing_nft_output_gift() -> Result<()> { ); assert_eq!( nft.immutable_features().issuer().unwrap().address(), - accounts[0].addresses().await[0].clone().into_bech32().as_ref() + wallet.address().await.inner() ); tear_down(storage_path) diff --git a/sdk/tests/wallet/syncing.rs b/sdk/tests/wallet/syncing.rs index 35d0c08fb4..fedf45842f 100644 --- a/sdk/tests/wallet/syncing.rs +++ b/sdk/tests/wallet/syncing.rs @@ -1,232 +1,227 @@ // Copyright 2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use iota_sdk::{ - types::block::output::{ - unlock_condition::{AddressUnlockCondition, ExpirationUnlockCondition, StorageDepositReturnUnlockCondition}, - AccountId, AccountOutputBuilder, BasicOutputBuilder, NftId, NftOutputBuilder, UnlockCondition, - }, - wallet::{account::SyncOptions, Result}, -}; -use pretty_assertions::assert_eq; - -use crate::wallet::common::{create_accounts_with_funds, make_wallet, setup, tear_down}; - -#[tokio::test] -#[cfg(feature = "rocksdb")] -async fn updated_default_sync_options() -> Result<()> { - let storage_path = "test-storage/updated_default_sync_options"; - setup(storage_path)?; - - let default_sync = SyncOptions::default(); - - let wallet = make_wallet(storage_path, None, None).await?; - let account = wallet.create_account().finish().await?; - - assert_eq!(default_sync, account.default_sync_options().await); - - let custom_options = SyncOptions { - address_start_index: 10, - ..Default::default() - }; - account.set_default_sync_options(custom_options.clone()).await?; - assert_eq!(custom_options, account.default_sync_options().await); - - drop(account); - drop(wallet); - - let wallet = make_wallet(storage_path, None, None).await?; - let account = wallet.get_account(0).await?; - - assert_eq!(custom_options, account.default_sync_options().await); - - tear_down(storage_path) -} - -#[ignore] -#[tokio::test] -async fn sync_only_most_basic_outputs() -> Result<()> { - let storage_path = "test-storage/sync_only_most_basic_outputs"; - setup(storage_path)?; - - let wallet = make_wallet(storage_path, None, None).await?; - - let account_0 = &create_accounts_with_funds(&wallet, 1).await?[0]; - let account_1 = wallet.create_account().finish().await?; - - let account_1_address = account_1.first_address_bech32().await; - - let token_supply = account_0.client().get_token_supply().await?; - // Only one basic output without further unlock conditions - let outputs = [ - BasicOutputBuilder::new_with_amount(1_000_000) - .with_unlock_conditions([AddressUnlockCondition::new(account_1_address.clone())]) - .finish_output(token_supply)?, - BasicOutputBuilder::new_with_amount(1_000_000) - .with_unlock_conditions([ - UnlockCondition::Address(AddressUnlockCondition::new(account_1_address.clone())), - UnlockCondition::Expiration(ExpirationUnlockCondition::new( - account_1_address.clone(), - // Already expired - account_0.client().get_slot_index().await? - 5000, - )?), - ]) - .finish_output(token_supply)?, - BasicOutputBuilder::new_with_amount(1_000_000) - .with_unlock_conditions([ - UnlockCondition::Address(AddressUnlockCondition::new(account_1_address.clone())), - UnlockCondition::Expiration(ExpirationUnlockCondition::new( - account_1_address.clone(), - // Not expired - account_0.client().get_slot_index().await? + 5000, - )?), - ]) - .finish_output(token_supply)?, - BasicOutputBuilder::new_with_amount(1_000_000) - .with_unlock_conditions([ - UnlockCondition::Address(AddressUnlockCondition::new(account_1_address.clone())), - UnlockCondition::StorageDepositReturn(StorageDepositReturnUnlockCondition::new( - account_1_address.clone(), - 1_000_000, - token_supply, - )?), - ]) - .finish_output(token_supply)?, - NftOutputBuilder::new_with_amount(1_000_000, NftId::null()) - .with_unlock_conditions([AddressUnlockCondition::new(account_1_address.clone())]) - .finish_output(token_supply)?, - NftOutputBuilder::new_with_amount(1_000_000, NftId::null()) - .with_unlock_conditions([ - UnlockCondition::Address(AddressUnlockCondition::new(account_1_address.clone())), - UnlockCondition::Expiration(ExpirationUnlockCondition::new( - account_1_address.clone(), - account_0.client().get_slot_index().await? + 5000, - )?), - ]) - .finish_output(token_supply)?, - AccountOutputBuilder::new_with_amount(1_000_000, AccountId::null()) - .with_unlock_conditions([UnlockCondition::Address(AddressUnlockCondition::new( - account_1_address.clone(), - ))]) - .finish_output(token_supply)?, - ]; - - let tx = account_0.send_outputs(outputs, None).await?; - account_0 - .reissue_transaction_until_included(&tx.transaction_id, None, None) - .await?; - - // Sync with sync_only_most_basic_outputs: true, only the first output should be synced - let balance = account_1 - .sync(Some(SyncOptions { - sync_only_most_basic_outputs: true, - ..Default::default() - })) - .await?; - assert_eq!(balance.potentially_locked_outputs().len(), 0); - assert_eq!(balance.nfts().len(), 0); - assert_eq!(balance.accounts().len(), 0); - let unspent_outputs = account_1.unspent_outputs(None).await?; - assert_eq!(unspent_outputs.len(), 1); - unspent_outputs.into_iter().for_each(|output_data| { - assert!(output_data.output.is_basic()); - assert_eq!(output_data.output.unlock_conditions().unwrap().len(), 1); - assert_eq!( - output_data - .output - .unlock_conditions() - .unwrap() - .address() - .unwrap() - .address(), - account_1_address.as_ref() - ); - }); - - tear_down(storage_path) -} - -#[ignore] -#[tokio::test] -async fn sync_incoming_transactions() -> Result<()> { - let storage_path = "test-storage/sync_incoming_transactions"; - setup(storage_path)?; - - let wallet = make_wallet(storage_path, None, None).await?; - - let account_0 = &create_accounts_with_funds(&wallet, 1).await?[0]; - let account_1 = wallet.create_account().finish().await?; - - let account_1_address = account_1.first_address_bech32().await; - - let token_supply = account_0.client().get_token_supply().await?; - - let outputs = [ - BasicOutputBuilder::new_with_amount(750_000) - .with_unlock_conditions([AddressUnlockCondition::new(account_1_address.clone())]) - .finish_output(token_supply)?, - BasicOutputBuilder::new_with_amount(250_000) - .with_unlock_conditions([AddressUnlockCondition::new(account_1_address)]) - .finish_output(token_supply)?, - ]; - - let tx = account_0.send_outputs(outputs, None).await?; - account_0 - .reissue_transaction_until_included(&tx.transaction_id, None, None) - .await?; - - account_1 - .sync(Some(SyncOptions { - sync_incoming_transactions: true, - ..Default::default() - })) - .await?; - let incoming_transactions = account_1.incoming_transactions().await; - assert_eq!(incoming_transactions.len(), 1); - let incoming_tx = account_1.get_incoming_transaction(&tx.transaction_id).await.unwrap(); - assert_eq!(incoming_tx.inputs.len(), 1); - let transaction = incoming_tx.payload.transaction(); - - // 2 created outputs plus remainder - assert_eq!(transaction.outputs().len(), 3); - - tear_down(storage_path) -} - -#[ignore] -#[tokio::test] -#[cfg(feature = "storage")] -async fn background_syncing() -> Result<()> { - let storage_path = "test-storage/background_syncing"; - setup(storage_path)?; - - let wallet = make_wallet(storage_path, None, None).await?; - - wallet.start_background_syncing(None, None).await?; - - let account = wallet.create_account().finish().await?; - - iota_sdk::client::request_funds_from_faucet( - crate::wallet::common::FAUCET_URL, - &account.first_address_bech32().await, - ) - .await?; - - for _ in 0..30 { - tokio::time::sleep(std::time::Duration::from_secs(2)).await; - let balance = account.balance().await?; - if balance.base_coin().available() > 0 { - break; - } - } - - // Balance should be != 0 without calling account.sync() - let balance = account.balance().await?; - if balance.base_coin().available() == 0 { - panic!("Faucet no longer wants to hand over coins or background syncing failed"); - } - - wallet.stop_background_syncing().await?; - - tear_down(storage_path) -} +// use iota_sdk::{ +// types::block::output::{ +// unlock_condition::{AddressUnlockCondition, ExpirationUnlockCondition, StorageDepositReturnUnlockCondition}, +// AccountId, AccountOutputBuilder, BasicOutputBuilder, NftId, NftOutputBuilder, UnlockCondition, +// }, +// wallet::{Result, SyncOptions}, +// }; +// use pretty_assertions::assert_eq; + +// use crate::wallet::common::{make_wallet, request_funds, setup, tear_down}; + +// #[tokio::test] +// #[cfg(feature = "rocksdb")] +// async fn updated_default_sync_options() -> Result<()> { +// let storage_path = "test-storage/updated_default_sync_options"; +// setup(storage_path)?; + +// let default_sync = SyncOptions::default(); + +// let wallet = make_wallet(storage_path, None, None).await?; + +// assert_eq!(default_sync, wallet.default_sync_options().await); + +// let custom_options = SyncOptions { +// address_start_index: 10, +// ..Default::default() +// }; +// wallet.set_default_sync_options(custom_options.clone()).await?; +// assert_eq!(custom_options, wallet.default_sync_options().await); + +// drop(wallet); + +// let wallet = make_wallet(storage_path, None, None).await?; + +// assert_eq!(custom_options, wallet.default_sync_options().await); + +// tear_down(storage_path) +// } + +// #[ignore] +// #[tokio::test] +// async fn sync_only_most_basic_outputs() -> Result<()> { +// let storage_path_0 = "test-storage/sync_only_most_basic_outputs_0"; +// setup(storage_path_0)?; +// let storage_path_1 = "test-storage/sync_only_most_basic_outputs_1"; +// setup(storage_path_1)?; + +// let wallet_0 = create_wallet_with_funds(storage_path_0, None, None, 1).await?; +// let wallet_1 = make_wallet(storage_path_1, None, None).await?; + +// let wallet_1_address = wallet_1.address().await; + +// let token_supply = wallet_0.client().get_token_supply().await?; +// // Only one basic output without further unlock conditions +// let outputs = [ +// BasicOutputBuilder::new_with_amount(1_000_000) +// .with_unlock_conditions([AddressUnlockCondition::new(wallet_1_address.clone())]) +// .finish_output(token_supply)?, +// BasicOutputBuilder::new_with_amount(1_000_000) +// .with_unlock_conditions([ +// UnlockCondition::Address(AddressUnlockCondition::new(wallet_1_address.clone())), +// UnlockCondition::Expiration(ExpirationUnlockCondition::new( +// wallet_1_address.clone(), +// // Already expired +// wallet_0.client().get_slot_index().await? - 5000, +// )?), +// ]) +// .finish_output(token_supply)?, +// BasicOutputBuilder::new_with_amount(1_000_000) +// .with_unlock_conditions([ +// UnlockCondition::Address(AddressUnlockCondition::new(wallet_1_address.clone())), +// UnlockCondition::Expiration(ExpirationUnlockCondition::new( +// wallet_1_address.clone(), +// // Not expired +// wallet_0.client().get_slot_index().await? + 5000, +// )?), +// ]) +// .finish_output(token_supply)?, +// BasicOutputBuilder::new_with_amount(1_000_000) +// .with_unlock_conditions([ +// UnlockCondition::Address(AddressUnlockCondition::new(wallet_1_address.clone())), +// UnlockCondition::StorageDepositReturn(StorageDepositReturnUnlockCondition::new( +// wallet_1_address.clone(), +// 1_000_000, +// token_supply, +// )?), +// ]) +// .finish_output(token_supply)?, +// NftOutputBuilder::new_with_amount(1_000_000, NftId::null()) +// .with_unlock_conditions([AddressUnlockCondition::new(wallet_1_address.clone())]) +// .finish_output(token_supply)?, +// NftOutputBuilder::new_with_amount(1_000_000, NftId::null()) +// .with_unlock_conditions([ +// UnlockCondition::Address(AddressUnlockCondition::new(wallet_1_address.clone())), +// UnlockCondition::Expiration(ExpirationUnlockCondition::new( +// wallet_1_address.clone(), +// wallet_0.client().get_slot_index().await? + 5000, +// )?), +// ]) +// .finish_output(token_supply)?, +// AccountOutputBuilder::new_with_amount(1_000_000, AccountId::null()) +// .with_unlock_conditions([UnlockCondition::Address(AddressUnlockCondition::new( +// wallet_1_address.clone(), +// ))]) +// .finish_output(token_supply)?, +// ]; + +// let tx = wallet_0.send_outputs(outputs, None).await?; +// wallet_0 +// .reissue_transaction_until_included(&tx.transaction_id, None, None) +// .await?; + +// // Sync with sync_only_most_basic_outputs: true, only the first output should be synced +// let balance = wallet_1 +// .sync(Some(SyncOptions { +// sync_only_most_basic_outputs: true, +// ..Default::default() +// })) +// .await?; +// assert_eq!(balance.potentially_locked_outputs().len(), 0); +// assert_eq!(balance.nfts().len(), 0); +// assert_eq!(balance.accounts().len(), 0); +// let unspent_outputs = wallet_1.unspent_outputs(None).await?; +// assert_eq!(unspent_outputs.len(), 1); +// unspent_outputs.into_iter().for_each(|output_data| { +// assert!(output_data.output.is_basic()); +// assert_eq!(output_data.output.unlock_conditions().unwrap().len(), 1); +// assert_eq!( +// output_data +// .output +// .unlock_conditions() +// .unwrap() +// .address() +// .unwrap() +// .address(), +// wallet_1_address.as_ref() +// ); +// }); + +// tear_down(storage_path) +// } + +// #[ignore] +// #[tokio::test] +// async fn sync_incoming_transactions() -> Result<()> { +// let storage_path_0 = "test-storage/sync_incoming_transactions_0"; +// setup(storage_path_0)?; +// let storage_path_1 = "test-storage/sync_incoming_transactions_1"; +// setup(storage_path_1)?; + +// let wallet_0 = create_wallet_with_funds(storage_path_0, None, None, 1).await?; +// let wallet_1 = make_wallet(storage_path_1, None, None).await?; + +// let wallet_1_address = wallet_1.address().await; + +// let token_supply = wallet_0.client().get_token_supply().await?; + +// let outputs = [ +// BasicOutputBuilder::new_with_amount(750_000) +// .with_unlock_conditions([AddressUnlockCondition::new(wallet_1_address.clone())]) +// .finish_output(token_supply)?, +// BasicOutputBuilder::new_with_amount(250_000) +// .with_unlock_conditions([AddressUnlockCondition::new(wallet_1_address)]) +// .finish_output(token_supply)?, +// ]; + +// let tx = wallet_0.send_outputs(outputs, None).await?; +// wallet_0 +// .reissue_transaction_until_included(&tx.transaction_id, None, None) +// .await?; + +// wallet_1 +// .sync(Some(SyncOptions { +// sync_incoming_transactions: true, +// ..Default::default() +// })) +// .await?; +// let incoming_transactions = wallet_1.incoming_transactions().await; +// assert_eq!(incoming_transactions.len(), 1); +// let incoming_tx = wallet_1.get_incoming_transaction(&tx.transaction_id).await.unwrap(); +// assert_eq!(incoming_tx.inputs.len(), 1); +// let transaction = incoming_tx.payload.transaction(); + +// // 2 created outputs plus remainder +// assert_eq!(transaction.outputs().len(), 3); + +// tear_down(storage_path) +// } + +// #[ignore] +// #[tokio::test] +// #[cfg(feature = "storage")] +// async fn background_syncing() -> Result<()> { +// let storage_path = "test-storage/background_syncing"; +// setup(storage_path)?; + +// let wallet = make_wallet(storage_path, None, None).await?; + +// wallet.start_background_syncing(None, None).await?; + +// iota_sdk::client::request_funds_from_faucet( +// crate::wallet::common::FAUCET_URL, +// &wallet.address().await, +// ) +// .await?; + +// for _ in 0..30 { +// tokio::time::sleep(std::time::Duration::from_secs(2)).await; +// let balance = wallet.balance().await?; +// if balance.base_coin().available() > 0 { +// break; +// } +// } + +// // Balance should be != 0 without calling wallet.sync() +// let balance = wallet.balance().await?; +// if balance.base_coin().available() == 0 { +// panic!("Faucet no longer wants to hand over coins or background syncing failed"); +// } + +// wallet.stop_background_syncing().await?; + +// tear_down(storage_path) +// } diff --git a/sdk/tests/wallet/transactions.rs b/sdk/tests/wallet/transactions.rs index 3b7ff019a3..42e1419288 100644 --- a/sdk/tests/wallet/transactions.rs +++ b/sdk/tests/wallet/transactions.rs @@ -1,299 +1,312 @@ // Copyright 2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use iota_sdk::wallet::{account::TransactionOptions, MintNftParams, Result, SendNftParams, SendParams}; +use iota_sdk::wallet::{MintNftParams, Result, SendNftParams, SendParams, TransactionOptions}; use pretty_assertions::assert_eq; -use crate::wallet::common::{create_accounts_with_funds, make_wallet, setup, tear_down}; - -#[ignore] -#[tokio::test] -async fn send_amount() -> Result<()> { - let storage_path = "test-storage/send_amount"; - setup(storage_path)?; - - let wallet = make_wallet(storage_path, None, None).await?; - - let account_0 = &create_accounts_with_funds(&wallet, 1).await?[0]; - let account_1 = wallet.create_account().finish().await?; - - let amount = 1_000_000; - let tx = account_0 - .send_with_params([SendParams::new(amount, account_1.first_address_bech32().await)?], None) - .await?; - - account_0 - .reissue_transaction_until_included(&tx.transaction_id, None, None) - .await?; - - let balance = account_1.sync(None).await.unwrap(); - assert_eq!(balance.base_coin().available(), amount); - - tear_down(storage_path) -} - -#[ignore] -#[tokio::test] -async fn send_amount_127_outputs() -> Result<()> { - let storage_path = "test-storage/send_amount_127_outputs"; - setup(storage_path)?; - - let wallet = make_wallet(storage_path, None, None).await?; - - let account_0 = &create_accounts_with_funds(&wallet, 1).await?[0]; - let account_1 = wallet.create_account().finish().await?; - - let amount = 1_000_000; - let tx = account_0 - .send_with_params( - vec![ - SendParams::new( - amount, - account_1.first_address_bech32().await, - )?; - // Only 127, because we need one remainder - 127 - ], - None, - ) - .await?; - - account_0 - .reissue_transaction_until_included(&tx.transaction_id, None, None) - .await?; - - let balance = account_1.sync(None).await.unwrap(); - assert_eq!(balance.base_coin().available(), 127 * amount); - - tear_down(storage_path) -} - -#[ignore] -#[tokio::test] -async fn send_amount_custom_input() -> Result<()> { - let storage_path = "test-storage/send_amount_custom_input"; - setup(storage_path)?; - - let wallet = make_wallet(storage_path, None, None).await?; - - let account_0 = &create_accounts_with_funds(&wallet, 1).await?[0]; - let account_1 = wallet.create_account().finish().await?; - - // Send 10 outputs to account_1 - let amount = 1_000_000; - let tx = account_0 - .send_with_params( - vec![SendParams::new(amount, account_1.first_address_bech32().await)?; 10], - None, - ) - .await?; - - account_0 - .reissue_transaction_until_included(&tx.transaction_id, None, None) - .await?; - - let balance = account_1.sync(None).await.unwrap(); - assert_eq!(balance.base_coin().available(), 10 * amount); - - // Send back with custom provided input - let custom_input = &account_1.unspent_outputs(None).await?[5]; - let tx = account_1 - .send_with_params( - [SendParams::new(amount, account_0.first_address_bech32().await)?], - Some(TransactionOptions { - custom_inputs: Some(vec![custom_input.output_id]), - ..Default::default() - }), - ) - .await?; - - assert_eq!(tx.inputs.len(), 1); - assert_eq!(tx.inputs.first().unwrap().metadata.output_id(), &custom_input.output_id); - - tear_down(storage_path) -} - -#[ignore] -#[tokio::test] -async fn send_nft() -> Result<()> { - let storage_path = "test-storage/send_nft"; - setup(storage_path)?; - - let wallet = make_wallet(storage_path, None, None).await?; - let accounts = &create_accounts_with_funds(&wallet, 2).await?; - - let nft_options = [MintNftParams::new() - .with_address(accounts[0].first_address_bech32().await) - .with_metadata(b"some nft metadata".to_vec()) - .with_immutable_metadata(b"some immutable nft metadata".to_vec())]; - - let transaction = accounts[0].mint_nfts(nft_options, None).await.unwrap(); - accounts[0] - .reissue_transaction_until_included(&transaction.transaction_id, None, None) - .await?; - let nft_id = *accounts[0].sync(None).await?.nfts().first().unwrap(); - - // Send to account 1 - let transaction = accounts[0] - .send_nft( - [SendNftParams::new(accounts[1].first_address_bech32().await, nft_id)?], - None, - ) - .await - .unwrap(); - accounts[0] - .reissue_transaction_until_included(&transaction.transaction_id, None, None) - .await?; - - let balance = accounts[1].sync(None).await?; - assert_eq!(balance.nfts().len(), 1); - assert_eq!(*balance.nfts().first().unwrap(), nft_id); - - tear_down(storage_path) -} - -#[ignore] -#[tokio::test] -async fn send_with_note() -> Result<()> { - let storage_path = "test-storage/send_with_note"; - setup(storage_path)?; - - let wallet = make_wallet(storage_path, None, None).await?; - - let account_0 = &create_accounts_with_funds(&wallet, 1).await?[0]; - let account_1 = wallet.create_account().finish().await?; - - let amount = 1_000_000; - let tx = account_0 - .send_with_params( - [SendParams::new(amount, account_1.first_address_bech32().await)?], - Some(TransactionOptions { - note: Some(String::from("send_with_note")), - ..Default::default() - }), - ) - .await?; - - assert_eq!(tx.note, Some(String::from("send_with_note"))); - - tear_down(storage_path) -} - -#[ignore] -#[tokio::test] -async fn conflicting_transaction() -> Result<()> { - let storage_path_0 = "test-storage/conflicting_transaction_0"; - let storage_path_1 = "test-storage/conflicting_transaction_1"; - setup(storage_path_0)?; - setup(storage_path_1)?; - - let mnemonic = iota_sdk::client::utils::generate_mnemonic()?; - // Create two wallets with the same mnemonic - let wallet_0 = make_wallet(storage_path_0, Some(mnemonic.clone()), None).await?; - let wallet_0_account = &create_accounts_with_funds(&wallet_0, 1).await?[0]; - let wallet_1 = make_wallet(storage_path_1, Some(mnemonic), None).await?; - let wallet_1_account = wallet_1.create_account().finish().await?; - - // Balance should be equal - assert_eq!(wallet_0_account.sync(None).await?, wallet_1_account.sync(None).await?); - - // Send transaction with each account and without syncing again - let tx = wallet_0_account - .send_with_params( - [SendParams::new( - 1_000_000, - wallet_0_account.first_address_bech32().await, - )?], - None, - ) - .await?; - wallet_0_account - .reissue_transaction_until_included(&tx.transaction_id, None, None) - .await?; - // Second transaction will be conflicting - let tx = wallet_1_account - .send_with_params( - [SendParams::new( - // Something in the transaction must be different than in the first one, otherwise it will be the same - // one - 2_000_000, - wallet_0_account.first_address_bech32().await, - )?], - None, - ) - .await?; - // Should return an error since the tx is conflicting - match wallet_1_account - .reissue_transaction_until_included(&tx.transaction_id, None, None) - .await - .unwrap_err() - { - iota_sdk::wallet::Error::Client(client_error) => { - let iota_sdk::client::Error::TangleInclusion(_) = *client_error else { - panic!("Expected TangleInclusion error"); - }; - } - _ => panic!("Expected TangleInclusion error"), - } - - // After syncing the balance is still equal - assert_eq!(wallet_0_account.sync(None).await?, wallet_1_account.sync(None).await?); - - let conflicting_tx = wallet_1_account.get_transaction(&tx.transaction_id).await.unwrap(); - assert_eq!( - conflicting_tx.inclusion_state, - iota_sdk::wallet::account::types::InclusionState::Conflicting - ); - // The conflicting tx is also removed from the pending txs - assert!(wallet_1_account.pending_transactions().await.is_empty()); - - tear_down(storage_path_0).ok(); - tear_down(storage_path_1) -} - -#[tokio::test] -#[cfg(all(feature = "ledger_nano", feature = "events"))] -#[ignore = "requires ledger nano instance"] -async fn prepare_transaction_ledger() -> Result<()> { - use iota_sdk::wallet::events::{types::TransactionProgressEvent, WalletEvent, WalletEventType}; - - let storage_path = "test-storage/wallet_address_generation_ledger"; - setup(storage_path)?; - - let wallet = crate::wallet::common::make_ledger_nano_wallet(storage_path, None).await?; - - let account_0 = &create_accounts_with_funds(&wallet, 1).await?[0]; - let account_1 = wallet.create_account().finish().await?; - - let amount = 1_000_000; - - let (sender, mut receiver) = tokio::sync::mpsc::channel(1); - - wallet - .listen([WalletEventType::TransactionProgress], move |event| { - if let WalletEvent::TransactionProgress(progress) = &event.event { - if let TransactionProgressEvent::PreparedTransaction(data) = progress { - sender - .try_send(data.as_ref().clone()) - .expect("too many PreparedTransaction events"); - } - } else { - panic!("expected TransactionProgress event") - } - }) - .await; - - let tx = account_0 - .send_with_params([SendParams::new(amount, account_1.first_address_bech32().await)?], None) - .await?; - - let data = receiver.recv().await.expect("never recieved event"); - // TODO put it back - // assert_eq!(data.transaction, tx.payload.transaction().into()); - for (sign, input) in data.inputs_data.iter().zip(tx.inputs) { - assert_eq!(sign.output, input.output); - assert_eq!(sign.output_metadata, input.metadata); - } - - tear_down(storage_path) -} +use crate::wallet::common::{make_wallet, request_funds, setup, tear_down}; + +// #[ignore] +// #[tokio::test] +// async fn send_amount() -> Result<()> { +// let storage_path_0 = "test-storage/send_amount_0"; +// setup(storage_path_0)?; +// let storage_path_1 = "test-storage/send_amount_1"; +// setup(storage_path_1)?; + +// let wallet_0 = make_wallet(storage_path_0, None, None).await?; +// request_funds(&wallet_0, 1).await?; + +// let wallet_1 = make_wallet(storage_path_1, None, None).await?; + +// let amount = 1_000_000; +// let tx = wallet_0 +// .send_with_params([SendParams::new(amount, wallet_1.address().await)?], None) +// .await?; + +// wallet_0 +// .reissue_transaction_until_included(&tx.transaction_id, None, None) +// .await?; + +// let balance = wallet_1.sync(None).await.unwrap(); +// assert_eq!(balance.base_coin().available(), amount); + +// tear_down(storage_path) +// } + +// #[ignore] +// #[tokio::test] +// async fn send_amount_127_outputs() -> Result<()> { +// let storage_path_0 = "test-storage/send_amount_127_outputs_0"; +// setup(storage_path_0)?; +// let storage_path_1 = "test-storage/send_amount_127_outputs_1"; +// setup(storage_path_1)?; + +// let wallet_0 = make_wallet(storage_path_0, None, None).await?; +// request_funds(&wallet_0, 1).await?; + +// let wallet_1 = make_wallet(storage_path_1, None, None).await?; + +// let amount = 1_000_000; +// let tx = wallet_0 +// .send_with_params( +// vec![ +// SendParams::new( +// amount, +// wallet_1.address().await, +// )?; +// // Only 127, because we need one remainder +// 127 +// ], +// None, +// ) +// .await?; + +// wallet_0 +// .reissue_transaction_until_included(&tx.transaction_id, None, None) +// .await?; + +// let balance = wallet_1.sync(None).await.unwrap(); +// assert_eq!(balance.base_coin().available(), 127 * amount); + +// tear_down(storage_path) +// } + +// #[ignore] +// #[tokio::test] +// async fn send_amount_custom_input() -> Result<()> { +// let storage_path_0 = "test-storage/send_amount_custom_input_0"; +// setup(storage_path_0)?; +// let storage_path_1 = "test-storage/send_amount_custom_input_1"; +// setup(storage_path_1)?; + +// let wallet_0 = make_wallet(storage_path_0, None, None).await?; +// request_funds(&wallet_0, 1).await?; + +// let wallet_1 = make_wallet(storage_path_1, None, None).await?; + +// // Send 10 outputs to wallet_1 +// let amount = 1_000_000; +// let tx = wallet_0 +// .send_with_params( +// vec![SendParams::new(amount, wallet_1.first_address_bech32().await)?; 10], +// None, +// ) +// .await?; + +// wallet_0 +// .reissue_transaction_until_included(&tx.transaction_id, None, None) +// .await?; + +// let balance = wallet_1.sync(None).await.unwrap(); +// assert_eq!(balance.base_coin().available(), 10 * amount); + +// // Send back with custom provided input +// let custom_input = &wallet_1.unspent_outputs(None).await?[5]; +// let tx = wallet_1 +// .send_with_params( +// [SendParams::new(amount, wallet_0.first_address_bech32().await)?], +// Some(TransactionOptions { +// custom_inputs: Some(vec![custom_input.output_id]), +// ..Default::default() +// }), +// ) +// .await?; + +// assert_eq!(tx.inputs.len(), 1); +// assert_eq!(tx.inputs.first().unwrap().metadata.output_id(), &custom_input.output_id); + +// tear_down(storage_path) +// } + +// #[ignore] +// #[tokio::test] +// async fn send_nft() -> Result<()> { +// let storage_path_0 = "test-storage/send_nft_0"; +// setup(storage_path_0)?; +// let storage_path_1 = "test-storage/send_nft_1"; +// setup(storage_path_1)?; + +// let wallet_0 = make_wallet(storage_path_0, None, None).await?; +// request_funds(&wallet_0, 2).await?; + +// let wallet_1 = make_wallet(storage_path_1, None, None).await?; + +// let nft_options = [MintNftParams::new() +// .with_address(wallet_0.address().await) +// .with_metadata(b"some nft metadata".to_vec()) +// .with_immutable_metadata(b"some immutable nft metadata".to_vec())]; + +// let transaction = wallet_0.mint_nfts(nft_options, None).await.unwrap(); +// wallet_0 +// .reissue_transaction_until_included(&transaction.transaction_id, None, None) +// .await?; +// let nft_id = *wallet_0.sync(None).await?.nfts().first().unwrap(); + +// // Send to wallet 1 +// let transaction = wallet_0 +// .send_nft( +// [SendNftParams::new(wallet_1.address().await, nft_id)?], +// None, +// ) +// .await +// .unwrap(); +// wallet_0 +// .reissue_transaction_until_included(&transaction.transaction_id, None, None) +// .await?; + +// let balance = wallet_1.sync(None).await?; +// assert_eq!(balance.nfts().len(), 1); +// assert_eq!(*balance.nfts().first().unwrap(), nft_id); + +// tear_down(storage_path) +// } + +// #[ignore] +// #[tokio::test] +// async fn send_with_note() -> Result<()> { +// let storage_path_0 = "test-storage/send_with_note_0"; +// setup(storage_path_0)?; +// let storage_path_1 = "test-storage/send_with_note_1"; +// setup(storage_path_1)?; + +// let wallet_0 = make_wallet(storage_path_0, None, None).await?; +// request_funds(&wallet_0, 1).await?; + +// let wallet_1 = make_wallet(storage_path_1, None, None).await?; + +// let amount = 1_000_000; +// let tx = wallet_0 +// .send_with_params( +// [SendParams::new(amount, wallet_1.address().await)?], +// Some(TransactionOptions { +// note: Some(String::from("send_with_note")), +// ..Default::default() +// }), +// ) +// .await?; + +// assert_eq!(tx.note, Some(String::from("send_with_note"))); + +// tear_down(storage_path) +// } + +// #[ignore] +// #[tokio::test] +// async fn conflicting_transaction() -> Result<()> { +// let storage_path_0 = "test-storage/conflicting_transaction_0"; +// let storage_path_1 = "test-storage/conflicting_transaction_1"; +// setup(storage_path_0)?; +// setup(storage_path_1)?; + +// let mnemonic = iota_sdk::client::utils::generate_mnemonic()?; +// // Create two wallets with the same mnemonic +// let wallet_0 = make_wallet(storage_path_0, Some(mnemonic.clone()), None).await?; +// request_funds(&wallet_0, 1).await?; +// let wallet_1 = make_wallet(storage_path_1, Some(mnemonic), None).await?; + +// // Balance should be equal +// assert_eq!(wallet_0.sync(None).await?, wallet_1.sync(None).await?); + +// // Send transaction without syncing again +// let tx = wallet_0 +// .send_with_params( +// [SendParams::new( +// 1_000_000, +// wallet_0.address().await, +// )?], +// None, +// ) +// .await?; +// wallet_0 +// .reissue_transaction_until_included(&tx.transaction_id, None, None) +// .await?; +// // Second transaction will be conflicting +// let tx = wallet_1 +// .send_with_params( +// [SendParams::new( +// // Something in the transaction must be different than in the first one, otherwise it will be the +// same // one +// 2_000_000, +// wallet_0.address().await, +// )?], +// None, +// ) +// .await?; +// // Should return an error since the tx is conflicting +// match wallet_1 +// .reissue_transaction_until_included(&tx.transaction_id, None, None) +// .await +// .unwrap_err() +// { +// iota_sdk::wallet::Error::Client(client_error) => { +// let iota_sdk::client::Error::TangleInclusion(_) = *client_error else { +// panic!("Expected TangleInclusion error"); +// }; +// } +// _ => panic!("Expected TangleInclusion error"), +// } + +// // After syncing the balance is still equal +// assert_eq!(wallet_0.sync(None).await?, wallet_1.sync(None).await?); + +// let conflicting_tx = wallet_1.get_transaction(&tx.transaction_id).await.unwrap(); +// assert_eq!( +// conflicting_tx.inclusion_state, +// iota_sdk::wallet::types::InclusionState::Conflicting +// ); +// // The conflicting tx is also removed from the pending txs +// assert!(wallet_1.pending_transactions().await.is_empty()); + +// tear_down(storage_path_0).ok(); +// tear_down(storage_path_1) +// } + +// #[tokio::test] +// #[cfg(all(feature = "ledger_nano", feature = "events"))] +// #[ignore = "requires ledger nano instance"] +// async fn prepare_transaction_ledger() -> Result<()> { +// use iota_sdk::wallet::events::{types::TransactionProgressEvent, WalletEvent, WalletEventType}; + +// let storage_path_0 = "test-storage/wallet_address_generation_ledger_0"; +// setup(storage_path_0)?; +// let storage_path_1 = "test-storage/wallet_address_generation_ledger_1"; +// setup(storage_path_1)?; + +// let wallet_0 = crate::wallet::common::make_ledger_nano_wallet(storage_path_0, None).await?; +// request_funds(&wallet_0, 1).await?; + +// let wallet_1 = make_wallet(storage_path_1, None, None).await?; + +// let amount = 1_000_000; + +// let (sender, mut receiver) = tokio::sync::mpsc::channel(1); + +// wallet +// .listen([WalletEventType::TransactionProgress], move |event| { +// if let WalletEvent::TransactionProgress(progress) = &event.event { +// if let TransactionProgressEvent::PreparedTransaction(data) = progress { +// sender +// .try_send(data.as_ref().clone()) +// .expect("too many PreparedTransaction events"); +// } +// } else { +// panic!("expected TransactionProgress event") +// } +// }) +// .await; + +// let tx = wallet_0 +// .send_with_params([SendParams::new(amount, wallet_1.address().await)?], None) +// .await?; + +// let data = receiver.recv().await.expect("never recieved event"); +// // TODO put it back +// // assert_eq!(data.transaction, tx.payload.transaction().into()); +// for (sign, input) in data.inputs_data.iter().zip(tx.inputs) { +// assert_eq!(sign.output, input.output); +// assert_eq!(sign.output_metadata, input.metadata); +// } + +// tear_down(storage_path) +// }