diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3fcc219efe..4303766a60 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -253,4 +253,4 @@ jobs: uses: ./.github/actions/cargo-cache - name: Test - run: WASM_BINDGEN_TEST_TIMEOUT=360 GECKODRIVER=/bin/geckodriver wasm-pack test --firefox --headless mm2src/mm2_main + run: WASM_BINDGEN_TEST_TIMEOUT=480 GECKODRIVER=/bin/geckodriver wasm-pack test --firefox --headless mm2src/mm2_main diff --git a/Cargo.lock b/Cargo.lock index 4604c8bc2c..558465a224 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8642,9 +8642,13 @@ dependencies = [ "byteorder", "common", "derive_more", + "ethcore-transaction", + "ethereum-types", + "ethkey", "futures 0.3.28", "hw_common", "js-sys", + "lazy_static", "mm2_err_handle", "prost", "rand 0.7.3", diff --git a/Cargo.toml b/Cargo.toml index 5868b5b314..b87feeb988 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,6 @@ members = [ "mm2src/mm2_p2p", "mm2src/mm2_rpc", "mm2src/mm2_state_machine", - "mm2src/mm2_test_helpers", "mm2src/rpc_task", "mm2src/trezor", ] @@ -46,6 +45,7 @@ exclude = [ "mm2src/floodsub", "mm2src/gossipsub", "mm2src/mm2_libp2p", + "mm2src/mm2_test_helpers", ] # https://doc.rust-lang.org/beta/cargo/reference/features.html#feature-resolver-version-2 diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index daeb1f5106..963031cd08 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -17,6 +17,7 @@ enable-solana = [ ] default = [] run-docker-tests = [] +for-tests = [] [lib] name = "coins" diff --git a/mm2src/coins/coin_balance.rs b/mm2src/coins/coin_balance.rs index b5f4a511e1..26f838a5fb 100644 --- a/mm2src/coins/coin_balance.rs +++ b/mm2src/coins/coin_balance.rs @@ -1,12 +1,11 @@ -use crate::hd_pubkey::HDXPubExtractor; -use crate::hd_wallet::{HDAccountOps, HDAddressId, HDWalletCoinOps, HDWalletOps, NewAccountCreatingError, - NewAddressDerivingError}; -use crate::{BalanceError, BalanceResult, CoinBalance, CoinWithDerivationMethod, DerivationMethod, HDAddress, - MarketCoinOps}; +use crate::hd_wallet::{HDAccountOps, HDAddressId, HDAddressOps, HDCoinAddress, HDCoinHDAccount, + HDPathAccountToAddressId, HDWalletCoinOps, HDWalletOps, HDXPubExtractor, + NewAccountCreationError, NewAddressDerivingError}; +use crate::{BalanceError, BalanceResult, CoinBalance, CoinBalanceMap, CoinWithDerivationMethod, DerivationMethod, + IguanaBalanceOps, MarketCoinOps}; use async_trait::async_trait; use common::log::{debug, info}; use crypto::{Bip44Chain, RpcDerivationPath}; -use futures::compat::Future01CompatExt; use mm2_err_handle::prelude::*; use mm2_number::BigDecimal; #[cfg(test)] use mocktopus::macros::*; @@ -15,10 +14,13 @@ use std::ops::Range; use std::{fmt, iter}; pub type AddressIdRange = Range; +pub(crate) type HDBalanceAddress = <::HDAddressScanner as HDAddressBalanceScanner>::Address; +pub(crate) type HDWalletBalanceObject = ::BalanceObject; +#[derive(Display)] pub enum EnableCoinBalanceError { NewAddressDerivingError(NewAddressDerivingError), - NewAccountCreatingError(NewAccountCreatingError), + NewAccountCreationError(NewAccountCreationError), BalanceError(BalanceError), } @@ -26,67 +28,113 @@ impl From for EnableCoinBalanceError { fn from(e: NewAddressDerivingError) -> Self { EnableCoinBalanceError::NewAddressDerivingError(e) } } -impl From for EnableCoinBalanceError { - fn from(e: NewAccountCreatingError) -> Self { EnableCoinBalanceError::NewAccountCreatingError(e) } +impl From for EnableCoinBalanceError { + fn from(e: NewAccountCreationError) -> Self { EnableCoinBalanceError::NewAccountCreationError(e) } } impl From for EnableCoinBalanceError { fn from(e: BalanceError) -> Self { EnableCoinBalanceError::BalanceError(e) } } +/// `BalanceObjectOps` should be implemented for a type that represents balance/s of a wallet. +/// For instance, if the wallet is for a platform coin and its tokens, the implementing type should be able to return the balances of the coin and its associated tokens. +pub trait BalanceObjectOps { + /// Creates a new balance object. + fn new() -> Self + where + Self: Sized; + + /// Adds another balance object to the current balance object. + fn add(&mut self, other: Self) + where + Self: Sized; + + /// Returns the total balance for the specified ticker. + /// If the balance object doesn't contain the specified ticker, it should return `None`. + fn get_total_for_ticker(&self, ticker: &str) -> Option; +} + +/// Encapsulates the balance of a specific wallet. +/// It provides two variants: `Iguana` and `HD`, each representing a different type of wallet. +/// This enum is used to abstract the differences between these two types of wallets, allowing for more generic operations on the balances. #[derive(Clone, Debug, PartialEq, Serialize)] #[serde(tag = "wallet_type")] -pub enum CoinBalanceReport { - Iguana(IguanaWalletBalance), - HD(HDWalletBalance), +pub enum CoinBalanceReport +where + BalanceObject: BalanceObjectOps, +{ + Iguana(IguanaWalletBalance), + HD(HDWalletBalance), } -impl CoinBalanceReport { - /// Returns a map where the key is address, and the value is the address's total balance [`CoinBalance::total`]. - pub fn to_addresses_total_balances(&self) -> HashMap { +impl CoinBalanceReport +where + BalanceObject: BalanceObjectOps, +{ + /// Returns a map where the key is address, and the value is the address's total balance for the specified ticker. + pub fn to_addresses_total_balances(&self, ticker: &str) -> HashMap> { match self { CoinBalanceReport::Iguana(IguanaWalletBalance { ref address, ref balance, - }) => iter::once((address.clone(), balance.get_total())).collect(), + }) => iter::once((address.clone(), balance.get_total_for_ticker(ticker))).collect(), CoinBalanceReport::HD(HDWalletBalance { ref accounts }) => accounts .iter() .flat_map(|account_balance| { - account_balance - .addresses - .iter() - .map(|addr_balance| (addr_balance.address.clone(), addr_balance.balance.get_total())) + account_balance.addresses.iter().map(|addr_balance| { + ( + addr_balance.address.clone(), + addr_balance.balance.get_total_for_ticker(ticker), + ) + }) }) .collect(), } } } +/// `IguanaWalletBalance` represents the balance of an Iguana wallet. +/// The BalanceObject generic parameter can be any type that represents the balance/s of a single address. #[derive(Clone, Debug, PartialEq, Serialize)] -pub struct IguanaWalletBalance { +pub struct IguanaWalletBalance { pub address: String, - pub balance: CoinBalance, + pub balance: BalanceObject, } +/// Represents the balance of an HD wallet. +/// `BalanceObject` is a generic parameter which can be any type represent balance/s. #[derive(Clone, Debug, PartialEq, Serialize)] -pub struct HDWalletBalance { - pub accounts: Vec, +pub struct HDWalletBalance { + pub accounts: Vec>, } +/// Represents the balance of a single account in an HD wallet. +/// `BalanceObject` is a generic parameter which can be any type represent balance/s. #[derive(Clone, Debug, PartialEq, Serialize)] -pub struct HDAccountBalance { +pub struct HDAccountBalance { pub account_index: u32, pub derivation_path: RpcDerivationPath, - pub total_balance: CoinBalance, - pub addresses: Vec, + pub total_balance: BalanceObject, + pub addresses: Vec>, +} + +/// Encapsulates the balance of an account in an HD wallet. +/// It provides two variants: `Single` and `Map`, each representing a different type of balance. +/// `Single` is used when the balance is for one coin, while `Map` is used when the balance is for multiple coins. +#[derive(Clone, Debug, PartialEq, Serialize)] +#[serde(untagged)] +pub enum HDAccountBalanceEnum { + Single(HDAccountBalance), + Map(HDAccountBalance), } +/// Represents the balance of a single address in an HD wallet. #[derive(Clone, Debug, PartialEq, Serialize)] -pub struct HDAddressBalance { +pub struct HDAddressBalance { pub address: String, pub derivation_path: RpcDerivationPath, pub chain: Bip44Chain, - pub balance: CoinBalance, + pub balance: BalanceObject, } #[derive(Clone, Copy, Debug, Deserialize, Serialize)] @@ -112,25 +160,30 @@ pub struct EnabledCoinBalanceParams { pub min_addresses_number: Option, } +/// `CoinBalanceReportOps` provides methods for getting the balance of a coin for different types of wallets. #[async_trait] pub trait CoinBalanceReportOps { - async fn coin_balance_report(&self) -> BalanceResult; + /// Represents the balance of a coin or a coin and its associated tokens for a certain wallet.. + type BalanceObject: BalanceObjectOps; + /// Returns the balance of a coin or a coin and its associated tokens for a certain wallet. + async fn coin_balance_report(&self) -> BalanceResult>; } #[async_trait] impl CoinBalanceReportOps for Coin where - Coin: CoinWithDerivationMethod::HDWallet> + Coin: CoinWithDerivationMethod + HDWalletBalanceOps - + MarketCoinOps + + IguanaBalanceOps> + Sync, - ::Address: fmt::Display + Sync, + HDCoinAddress: fmt::Display + Sync, { - async fn coin_balance_report(&self) -> BalanceResult { + type BalanceObject = HDWalletBalanceObject; + + async fn coin_balance_report(&self) -> BalanceResult> { match self.derivation_method() { DerivationMethod::SingleAddress(my_address) => self - .my_balance() - .compat() + .iguana_balances() .await .map(|balance| { CoinBalanceReport::Iguana(IguanaWalletBalance { @@ -149,56 +202,66 @@ where #[async_trait] pub trait EnableCoinBalanceOps { + type BalanceObject: BalanceObjectOps; + async fn enable_coin_balance( &self, - xpub_extractor: &XPubExtractor, + xpub_extractor: Option, params: EnabledCoinBalanceParams, - ) -> MmResult + path_to_address: &HDPathAccountToAddressId, + ) -> MmResult, EnableCoinBalanceError> where - XPubExtractor: HDXPubExtractor; + XPubExtractor: HDXPubExtractor + Send; } #[async_trait] impl EnableCoinBalanceOps for Coin where - Coin: CoinWithDerivationMethod::HDWallet> + Coin: CoinWithDerivationMethod + HDWalletBalanceOps - + MarketCoinOps + + IguanaBalanceOps> + Sync, - ::Address: fmt::Display + Sync, + HDCoinAddress: fmt::Display + Sync, { + type BalanceObject = HDWalletBalanceObject; + async fn enable_coin_balance( &self, - xpub_extractor: &XPubExtractor, + xpub_extractor: Option, params: EnabledCoinBalanceParams, - ) -> MmResult + path_to_address: &HDPathAccountToAddressId, + ) -> MmResult, EnableCoinBalanceError> where - XPubExtractor: HDXPubExtractor, + XPubExtractor: HDXPubExtractor + Send, { match self.derivation_method() { DerivationMethod::SingleAddress(my_address) => self - .my_balance() - .compat() + .iguana_balances() .await .map(|balance| { CoinBalanceReport::Iguana(IguanaWalletBalance { - address: my_address.to_string(), + address: self.address_formatter()(my_address), balance, }) }) .mm_err(EnableCoinBalanceError::from), DerivationMethod::HDWallet(hd_wallet) => self - .enable_hd_wallet(hd_wallet, xpub_extractor, params) + .enable_hd_wallet(hd_wallet, xpub_extractor, params, path_to_address) .await .map(CoinBalanceReport::HD), } } } +/// `HDWalletBalanceOps` provides different methods related to the balance of an HD wallet. #[async_trait] pub trait HDWalletBalanceOps: HDWalletCoinOps { - type HDAddressScanner: HDAddressBalanceScanner
; + /// The type of the scanner that will be used to scan for balances in an HD wallet. + type HDAddressScanner: HDAddressBalanceScanner
> + Sync; + /// Represents a balance in an HD wallet. + type BalanceObject: BalanceObjectOps + Clone + Send; + /// Returns the scanner of balances for the HD wallet. async fn produce_hd_address_scanner(&self) -> BalanceResult; /// Requests balances of already known addresses, and if it's prescribed by [`EnableCoinParams::scan_policy`], @@ -207,33 +270,41 @@ pub trait HDWalletBalanceOps: HDWalletCoinOps { async fn enable_hd_wallet( &self, hd_wallet: &Self::HDWallet, - xpub_extractor: &XPubExtractor, + xpub_extractor: Option, params: EnabledCoinBalanceParams, - ) -> MmResult + path_to_address: &HDPathAccountToAddressId, + ) -> MmResult, EnableCoinBalanceError> where - XPubExtractor: HDXPubExtractor; + XPubExtractor: HDXPubExtractor + Send; /// Scans for the new addresses of the specified `hd_account` using the given `address_scanner`. /// Returns balances of the new addresses. async fn scan_for_new_addresses( &self, hd_wallet: &Self::HDWallet, - hd_account: &mut Self::HDAccount, + hd_account: &mut HDCoinHDAccount, address_scanner: &Self::HDAddressScanner, gap_limit: u32, - ) -> BalanceResult>; + ) -> BalanceResult>>; /// Requests balances of every activated HD account. - async fn all_accounts_balances(&self, hd_wallet: &Self::HDWallet) -> BalanceResult> { + async fn all_accounts_balances( + &self, + hd_wallet: &Self::HDWallet, + ) -> BalanceResult>> { let accounts = hd_wallet.get_accounts().await; let mut result = Vec::with_capacity(accounts.len()); for (_account_id, hd_account) in accounts { let addresses = self.all_known_addresses_balances(&hd_account).await?; - let total_balance = addresses.iter().fold(CoinBalance::default(), |total, addr_balance| { - total + addr_balance.balance.clone() - }); + let total_balance = addresses + .iter() + .fold(Self::BalanceObject::new(), |mut total, addr_balance| { + total.add(addr_balance.balance.clone()); + total + }); + let account_balance = HDAccountBalance { account_index: hd_account.account_id(), derivation_path: RpcDerivationPath(hd_account.account_derivation_path()), @@ -248,17 +319,19 @@ pub trait HDWalletBalanceOps: HDWalletCoinOps { } /// Requests balances of every known addresses of the given `hd_account`. - async fn all_known_addresses_balances(&self, hd_account: &Self::HDAccount) -> BalanceResult>; + async fn all_known_addresses_balances( + &self, + hd_account: &HDCoinHDAccount, + ) -> BalanceResult>>; /// Requests balances of known addresses of the given `address_ids` addresses at the specified `chain`. async fn known_addresses_balances_with_ids( &self, - hd_account: &Self::HDAccount, + hd_account: &HDCoinHDAccount, chain: Bip44Chain, address_ids: Ids, - ) -> BalanceResult> + ) -> BalanceResult>> where - Self::Address: fmt::Display + Clone, Ids: Iterator + Send, { let address_ids = address_ids.map(|address_id| HDAddressId { chain, address_id }); @@ -268,13 +341,7 @@ pub trait HDWalletBalanceOps: HDWalletCoinOps { .derive_addresses(hd_account, address_ids) .await? .into_iter() - .map( - |HDAddress { - address, - derivation_path, - .. - }| (address, derivation_path), - ) + .map(|hd_address| (hd_address.address(), hd_address.derivation_path().clone())) .unzip(); let balances = self @@ -286,7 +353,7 @@ pub trait HDWalletBalanceOps: HDWalletCoinOps { // So we can zip the derivation paths with the pairs `(Address, CoinBalance)`. .zip(der_paths) .map(|((address, balance), derivation_path)| HDAddressBalance { - address: address.to_string(), + address: self.address_formatter()(&address), derivation_path: RpcDerivationPath(derivation_path), chain, balance, @@ -298,22 +365,22 @@ pub trait HDWalletBalanceOps: HDWalletCoinOps { /// Requests balance of the given `address`. /// This function is expected to be more efficient than ['HDWalletBalanceOps::is_address_used'] in most cases /// since many of RPC clients allow us to request the address balance without the history. - async fn known_address_balance(&self, address: &Self::Address) -> BalanceResult; + async fn known_address_balance(&self, address: &HDBalanceAddress) -> BalanceResult; /// Requests balances of the given `addresses`. /// The pairs `(Address, CoinBalance)` are guaranteed to be in the same order in which they were requested. async fn known_addresses_balances( &self, - addresses: Vec, - ) -> BalanceResult>; + addresses: Vec>, + ) -> BalanceResult, Self::BalanceObject)>>; /// Checks if the address has been used by the user by checking if the transaction history of the given `address` is not empty. /// Please note the function can return zero balance even if the address has been used before. async fn is_address_used( &self, - address: &Self::Address, + address: &HDBalanceAddress, address_scanner: &Self::HDAddressScanner, - ) -> BalanceResult> { + ) -> BalanceResult> { if !address_scanner.is_address_used(address).await? { return Ok(AddressBalanceStatus::NotUsed); } @@ -322,18 +389,20 @@ pub trait HDWalletBalanceOps: HDWalletCoinOps { Ok(AddressBalanceStatus::Used(balance)) } + // Todo: should probably be moved to a separate trait. Addresses should be HashSet too /// Prepares addresses for real time balance streaming if coin balance event is enabled. - async fn prepare_addresses_for_balance_stream_if_enabled( - &self, - addresses: HashSet, - ) -> MmResult<(), String>; + async fn prepare_addresses_for_balance_stream_if_enabled(&self, addresses: HashSet) + -> MmResult<(), String>; } +/// `HDAddressBalanceScanner` trait provides different methods related to scanning for balances in an HD wallet. #[async_trait] #[cfg_attr(test, mockable)] -pub trait HDAddressBalanceScanner: Sync { - type Address; +pub trait HDAddressBalanceScanner { + /// The type of address that the scanner will be scanning for. + type Address: Send + Sync; + /// Checks if the given `address` has been used before. async fn is_address_used(&self, address: &Self::Address) -> BalanceResult; } @@ -344,19 +413,22 @@ pub enum AddressBalanceStatus { pub mod common_impl { use super::*; - use crate::hd_wallet::{HDAccountOps, HDWalletOps}; + use crate::hd_wallet::{create_new_account, ExtractExtendedPubkey, HDAccountOps, HDAccountStorageOps, HDAddressOps, + HDWalletOps}; + use crypto::Secp256k1ExtendedPublicKey; pub(crate) async fn enable_hd_account( coin: &Coin, hd_wallet: &Coin::HDWallet, - hd_account: &mut Coin::HDAccount, + hd_account: &mut HDCoinHDAccount, + chain: Bip44Chain, address_scanner: &Coin::HDAddressScanner, scan_new_addresses: bool, min_addresses_number: Option, - ) -> MmResult + ) -> MmResult>, EnableCoinBalanceError> where Coin: HDWalletBalanceOps + MarketCoinOps + Sync, - Coin::Address: fmt::Display, + HDCoinAddress: fmt::Display, { let gap_limit = hd_wallet.gap_limit(); let mut addresses = coin.all_known_addresses_balances(hd_account).await?; @@ -368,12 +440,16 @@ pub mod common_impl { } if let Some(min_addresses_number) = min_addresses_number { - gen_new_addresses(coin, hd_wallet, hd_account, &mut addresses, min_addresses_number).await? + gen_new_addresses(coin, hd_wallet, hd_account, chain, &mut addresses, min_addresses_number).await? } - let total_balance = addresses.iter().fold(CoinBalance::default(), |total, addr_balance| { - total + addr_balance.balance.clone() - }); + let total_balance = addresses + .iter() + .fold(HDWalletBalanceObject::::new(), |mut total, addr_balance| { + total.add(addr_balance.balance.clone()); + total + }); + let account_balance = HDAccountBalance { account_index: hd_account.account_id(), derivation_path: RpcDerivationPath(hd_account.account_derivation_path()), @@ -387,13 +463,18 @@ pub mod common_impl { pub(crate) async fn enable_hd_wallet( coin: &Coin, hd_wallet: &Coin::HDWallet, - xpub_extractor: &XPubExtractor, + xpub_extractor: Option, params: EnabledCoinBalanceParams, - ) -> MmResult + path_to_address: &HDPathAccountToAddressId, + ) -> MmResult>, EnableCoinBalanceError> where - Coin: HDWalletBalanceOps + MarketCoinOps + Sync, - Coin::Address: fmt::Display, - XPubExtractor: HDXPubExtractor, + Coin: ExtractExtendedPubkey + + HDWalletBalanceOps + + MarketCoinOps + + Sync, + HDCoinAddress: fmt::Display, + XPubExtractor: HDXPubExtractor + Send, + HDCoinHDAccount: HDAccountStorageOps, { let mut accounts = hd_wallet.get_accounts_mut().await; let address_scanner = coin.produce_hd_address_scanner().await?; @@ -402,7 +483,7 @@ pub mod common_impl { accounts: Vec::with_capacity(accounts.len() + 1), }; - if accounts.is_empty() { + if accounts.get(&path_to_address.account_id).is_none() { // Is seems that we couldn't find any HD account from the HD wallet storage. drop(accounts); info!( @@ -411,7 +492,8 @@ pub mod common_impl { ); // Create new HD account. - let mut new_account = coin.create_new_account(hd_wallet, xpub_extractor).await?; + let mut new_account = + create_new_account(coin, hd_wallet, xpub_extractor, Some(path_to_address.account_id)).await?; let scan_new_addresses = matches!( params.scan_policy, EnableCoinScanPolicy::ScanIfNewWallet | EnableCoinScanPolicy::Scan @@ -421,11 +503,13 @@ pub mod common_impl { coin, hd_wallet, &mut new_account, + path_to_address.chain, &address_scanner, scan_new_addresses, - params.min_addresses_number, + params.min_addresses_number.max(Some(path_to_address.address_id + 1)), ) .await?; + // Todo: The enabled address should be indicated in the response. result.accounts.push(account_balance); return Ok(result); } @@ -436,14 +520,23 @@ pub mod common_impl { coin.ticker() ); let scan_new_addresses = matches!(params.scan_policy, EnableCoinScanPolicy::Scan); - for (_account_id, hd_account) in accounts.iter_mut() { + for (account_id, hd_account) in accounts.iter_mut() { + let min_addresses_number = if *account_id == path_to_address.account_id { + // The account for the enabled address is already indexed. + // But in case the address index is larger than the number of derived addresses, + // we need to derive new addresses to make sure that the enabled address is indexed. + params.min_addresses_number.max(Some(path_to_address.address_id + 1)) + } else { + params.min_addresses_number + }; let account_balance = enable_hd_account( coin, hd_wallet, hd_account, + path_to_address.chain, &address_scanner, scan_new_addresses, - params.min_addresses_number, + min_addresses_number, ) .await?; result.accounts.push(account_balance); @@ -456,15 +549,15 @@ pub mod common_impl { async fn gen_new_addresses( coin: &Coin, hd_wallet: &Coin::HDWallet, - hd_account: &mut Coin::HDAccount, - result_addresses: &mut Vec, + hd_account: &mut HDCoinHDAccount, + chain: Bip44Chain, + result_addresses: &mut Vec>>, min_addresses_number: u32, ) -> MmResult<(), EnableCoinBalanceError> where Coin: HDWalletBalanceOps + MarketCoinOps + Sync, - Coin::Address: fmt::Display, { - let max_addresses_number = hd_wallet.address_limit(); + let max_addresses_number = hd_account.address_limit(); if min_addresses_number >= max_addresses_number { return MmError::err(EnableCoinBalanceError::NewAddressDerivingError( NewAddressDerivingError::AddressLimitReached { max_addresses_number }, @@ -479,7 +572,6 @@ pub mod common_impl { } let to_generate = min_addresses_number - actual_addresses_number; - let chain = hd_wallet.default_receiver_chain(); let ticker = coin.ticker(); let account_id = hd_account.account_id(); info!("Generate '{to_generate}' addresses: ticker={ticker} account_id={account_id}, chain={chain:?}"); @@ -487,19 +579,15 @@ pub mod common_impl { let mut new_addresses = Vec::with_capacity(to_generate); let mut addresses_to_request = Vec::with_capacity(to_generate); for _ in 0..to_generate { - let HDAddress { - address, - derivation_path, - .. - } = coin.generate_new_address(hd_wallet, hd_account, chain).await?; + let hd_address = coin.generate_new_address(hd_wallet, hd_account, chain).await?; new_addresses.push(HDAddressBalance { - address: address.to_string(), - derivation_path: RpcDerivationPath(derivation_path), + address: coin.address_formatter()(&hd_address.address()), + derivation_path: RpcDerivationPath(hd_address.derivation_path().clone()), chain, - balance: CoinBalance::default(), + balance: HDWalletBalanceObject::::new(), }); - addresses_to_request.push(address); + addresses_to_request.push(hd_address.address().clone()); } let to_extend = coin diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 41291b84fd..62514973e7 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -21,13 +21,34 @@ // Copyright © 2023 Pampex LTD and TillyHK LTD. All rights reserved. // use super::eth::Action::{Call, Create}; +use super::watcher_common::{validate_watcher_reward, REWARD_GAS_AMOUNT}; +use super::*; +use crate::coin_balance::{EnableCoinBalanceError, EnabledCoinBalanceParams, HDAccountBalance, HDAddressBalance, + HDBalanceAddress, HDWalletBalance, HDWalletBalanceOps}; use crate::eth::eth_rpc::ETH_RPC_REQUEST_TIMEOUT; use crate::eth::web3_transport::websocket_transport::{WebsocketTransport, WebsocketTransportNode}; +use crate::hd_wallet::{HDAccountOps, HDCoinAddress, HDCoinHDAccount, HDCoinHDAddress, HDCoinWithdrawOps, + HDConfirmAddress, HDPathAccountToAddressId, HDWalletCoinOps, HDXPubExtractor}; use crate::lp_price::get_base_price_in_rel; +use crate::nft::nft_errors::ParseContractTypeError; use crate::nft::nft_structs::{ContractType, ConvertChain, NftInfo, TransactionNftDetails, WithdrawErc1155, WithdrawErc721}; -use crate::{DexFee, MakerNftSwapOpsV2, ParseCoinAssocTypes, ParseNftAssocTypes, RefundMakerPaymentArgs, RpcCommonOps, - SendNftMakerPaymentArgs, SpendNftMakerPaymentArgs, ToBytes, ValidateNftMakerPaymentArgs, +use crate::nft::WithdrawNftResult; +use crate::rpc_command::account_balance::{AccountBalanceParams, AccountBalanceRpcOps, HDAccountBalanceResponse}; +use crate::rpc_command::get_new_address::{GetNewAddressParams, GetNewAddressResponse, GetNewAddressRpcError, + GetNewAddressRpcOps}; +use crate::rpc_command::hd_account_balance_rpc_error::HDAccountBalanceRpcError; +use crate::rpc_command::init_account_balance::{InitAccountBalanceParams, InitAccountBalanceRpcOps}; +use crate::rpc_command::init_create_account::{CreateAccountRpcError, CreateAccountState, CreateNewAccountParams, + InitCreateAccountRpcOps}; +use crate::rpc_command::init_scan_for_new_addresses::{InitScanAddressesRpcOps, ScanAddressesParams, + ScanAddressesResponse}; +use crate::rpc_command::init_withdraw::{InitWithdrawCoin, WithdrawTaskHandleShared}; +use crate::rpc_command::{account_balance, get_new_address, init_account_balance, init_create_account, + init_scan_for_new_addresses}; +use crate::{coin_balance, scan_for_new_addresses_impl, BalanceResult, CoinWithDerivationMethod, DerivationMethod, + DexFee, MakerNftSwapOpsV2, ParseCoinAssocTypes, ParseNftAssocTypes, PrivKeyPolicy, RefundMakerPaymentArgs, + RpcCommonOps, SendNftMakerPaymentArgs, SpendNftMakerPaymentArgs, ToBytes, ValidateNftMakerPaymentArgs, ValidateWatcherSpendInput, WatcherSpendType}; use async_trait::async_trait; use bitcrypto::{dhash160, keccak256, ripemd160, sha256}; @@ -38,10 +59,8 @@ use common::executor::{abortable_queue::AbortableQueue, AbortSettings, Abortable use common::log::{debug, error, info, warn}; use common::number_type_casting::SafeTypeCastingNumbers; use common::{get_utc_timestamp, now_sec, small_rng, DEX_FEE_ADDR_RAW_PUBKEY}; -#[cfg(target_arch = "wasm32")] -use common::{now_ms, wait_until_ms}; use crypto::privkey::key_pair_from_secret; -use crypto::{CryptoCtx, CryptoCtxError, GlobalHDAccountArc, KeyPairPolicy, StandardHDCoinAddress}; +use crypto::{Bip44Chain, CryptoCtx, CryptoCtxError, GlobalHDAccountArc, KeyPairPolicy}; use derive_more::Display; use enum_derives::EnumFromStringify; use ethabi::{Contract, Function, Token}; @@ -51,13 +70,11 @@ use ethereum_types::{Address, H160, H256, U256}; use ethkey::{public_to_address, sign, verify_address, KeyPair, Public, Signature}; use futures::compat::Future01CompatExt; use futures::future::{join_all, select_ok, try_join_all, Either, FutureExt, TryFutureExt}; -use futures::lock::MutexGuard as AsyncMutexGuard; use futures01::Future; use http::{StatusCode, Uri}; use instant::Instant; use keys::Public as HtlcPubKey; use mm2_core::mm_ctx::{MmArc, MmWeak}; -use mm2_err_handle::prelude::*; use mm2_event_stream::behaviour::{EventBehaviour, EventInitStatus}; use mm2_net::transport::{slurp_url, GuiAuthValidation, GuiAuthValidationGenerator, SlurpError}; use mm2_number::bigdecimal_custom::CheckedDivision; @@ -65,6 +82,7 @@ use mm2_number::{BigDecimal, BigUint, MmNumber}; use mm2_rpc::data::legacy::GasStationPricePolicy; #[cfg(test)] use mocktopus::macros::*; use rand::seq::SliceRandom; +use rlp::{DecoderError, Encodable, RlpStream}; use rpc::v1::types::Bytes as BytesJson; use secp256k1::PublicKey; use serde_json::{self as json, Value as Json}; @@ -73,7 +91,6 @@ use sha3::{Digest, Keccak256}; use std::collections::HashMap; use std::convert::{TryFrom, TryInto}; use std::ops::Deref; -#[cfg(not(target_arch = "wasm32"))] use std::path::PathBuf; use std::str::from_utf8; use std::str::FromStr; use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering}; @@ -82,53 +99,38 @@ use std::time::Duration; use web3::types::{Action as TraceAction, BlockId, BlockNumber, Bytes, CallRequest, FilterBuilder, Log, Trace, TraceFilterBuilder, Transaction as Web3Transaction, TransactionId, U64}; use web3::{self, Web3}; -use web3_transport::{http_transport::HttpTransportNode, Web3Transport}; cfg_wasm32! { + use common::{now_ms, wait_until_ms}; use crypto::MetamaskArc; use ethereum_types::{H264, H520}; use mm2_metamask::MetamaskError; use web3::types::TransactionRequest; } -use super::watcher_common::{validate_watcher_reward, REWARD_GAS_AMOUNT}; -use super::{coin_conf, lp_coinfind_or_err, AsyncMutex, BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, - CoinBalance, CoinFutSpawner, CoinProtocol, CoinTransportMetrics, CoinsContext, ConfirmPaymentInput, - EthValidateFeeArgs, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, IguanaPrivKey, MakerSwapTakerCoin, - MarketCoinOps, MmCoin, MmCoinEnum, MyAddressError, MyWalletAddress, NegotiateSwapContractAddrErr, - NumConversError, NumConversResult, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, - PrivKeyBuildPolicy, PrivKeyPolicyNotAllowed, RawTransactionError, RawTransactionFut, - RawTransactionRequest, RawTransactionRes, RawTransactionResult, RefundError, RefundPaymentArgs, - RefundResult, RewardTarget, RpcClientType, RpcTransportEventHandler, RpcTransportEventHandlerShared, - SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignEthTransactionParams, - SignRawTransactionEnum, SignRawTransactionRequest, SignatureError, SignatureResult, SpendPaymentArgs, - SwapOps, TakerSwapMakerCoin, TradeFee, TradePreimageError, TradePreimageFut, TradePreimageResult, - TradePreimageValue, Transaction, TransactionDetails, TransactionEnum, TransactionErr, TransactionFut, - TransactionType, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, - ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, - ValidatePaymentInput, VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, - WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, - WatcherValidateTakerFeeInput, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest, WithdrawResult, - EARLY_CONFIRMATION_ERR_LOG, INVALID_CONTRACT_ADDRESS_ERR_LOG, INVALID_PAYMENT_STATE_ERR_LOG, - INVALID_RECEIVER_ERR_LOG, INVALID_SENDER_ERR_LOG, INVALID_SWAP_ID_ERR_LOG}; -pub use rlp; -use rlp::{DecoderError, Encodable, RlpStream}; +cfg_native! { + use std::path::PathBuf; +} mod eth_balance_events; mod eth_rpc; #[cfg(test)] mod eth_tests; #[cfg(target_arch = "wasm32")] mod eth_wasm_tests; +#[cfg(any(test, target_arch = "wasm32"))] mod for_tests; pub(crate) mod nft_swap_v2; mod web3_transport; +use web3_transport::{http_transport::HttpTransportNode, Web3Transport}; + +pub mod eth_hd_wallet; +use eth_hd_wallet::EthHDWallet; #[path = "eth/v2_activation.rs"] pub mod v2_activation; use v2_activation::{build_address_and_priv_key_policy, EthActivationV2Error}; +mod eth_withdraw; +use eth_withdraw::{EthWithdraw, InitEthWithdraw, StandardEthWithdraw}; + mod nonce; -use crate::coin_errors::ValidatePaymentResult; -use crate::nft::nft_errors::{GetNftInfoError, ParseContractTypeError}; -use crate::nft::WithdrawNftResult; -use crate::{PrivKeyPolicy, TransactionResult, WithdrawFrom}; use nonce::ParityNonce; /// https://github.com/artemii235/etomic-swap/blob/master/contracts/EtomicSwap.sol @@ -194,7 +196,7 @@ const GAS_PRICE_APPROXIMATION_PERCENT_ON_ORDER_ISSUE: u64 = 5; /// - it may increase by 3% during the swap. const GAS_PRICE_APPROXIMATION_PERCENT_ON_TRADE_PREIMAGE: u64 = 7; -pub(crate) const ETH_GAS: u64 = 150_000; +pub const ETH_GAS: u64 = 150_000; /// Lifetime of generated signed message for gui-auth requests const GUI_AUTH_SIGNED_MESSAGE_LIFETIME_SEC: i64 = 90; @@ -207,9 +209,10 @@ lazy_static! { pub static ref NFT_SWAP_CONTRACT: Contract = Contract::load(NFT_SWAP_CONTRACT_ABI.as_bytes()).unwrap(); } +pub type GasStationResult = Result>; +pub type EthDerivationMethod = DerivationMethod; pub type Web3RpcFut = Box> + Send>; pub type Web3RpcResult = Result>; -pub type GasStationResult = Result>; type EthPrivKeyPolicy = PrivKeyPolicy; type GasDetails = (U256, U256); @@ -301,6 +304,10 @@ impl From for Web3RpcError { } } +impl From for Web3RpcError { + fn from(e: UnexpectedDerivationMethod) -> Self { Web3RpcError::Internal(e.to_string()) } +} + #[cfg(target_arch = "wasm32")] impl From for Web3RpcError { fn from(e: MetamaskError) -> Self { @@ -403,7 +410,7 @@ struct SavedErc20Events { latest_block: U64, } -#[derive(Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum EthCoinType { /// Ethereum itself or it's forks: ETC/others Eth, @@ -424,6 +431,7 @@ pub enum EthPrivKeyBuildPolicy { GlobalHDAccount(GlobalHDAccountArc), #[cfg(target_arch = "wasm32")] Metamask(MetamaskArc), + Trezor, } impl EthPrivKeyBuildPolicy { @@ -442,16 +450,12 @@ impl EthPrivKeyBuildPolicy { } } -impl TryFrom for EthPrivKeyBuildPolicy { - type Error = PrivKeyPolicyNotAllowed; - - /// Converts `PrivKeyBuildPolicy` to `EthPrivKeyBuildPolicy` - /// taking into account that ETH doesn't support `Trezor` yet. - fn try_from(policy: PrivKeyBuildPolicy) -> Result { +impl From for EthPrivKeyBuildPolicy { + fn from(policy: PrivKeyBuildPolicy) -> Self { match policy { - PrivKeyBuildPolicy::IguanaPrivKey(iguana) => Ok(EthPrivKeyBuildPolicy::IguanaPrivKey(iguana)), - PrivKeyBuildPolicy::GlobalHDAccount(global_hd) => Ok(EthPrivKeyBuildPolicy::GlobalHDAccount(global_hd)), - PrivKeyBuildPolicy::Trezor => Err(PrivKeyPolicyNotAllowed::HardwareWalletNotSupported), + PrivKeyBuildPolicy::IguanaPrivKey(iguana) => EthPrivKeyBuildPolicy::IguanaPrivKey(iguana), + PrivKeyBuildPolicy::GlobalHDAccount(global_hd) => EthPrivKeyBuildPolicy::GlobalHDAccount(global_hd), + PrivKeyBuildPolicy::Trezor => EthPrivKeyBuildPolicy::Trezor, } } } @@ -461,7 +465,11 @@ pub struct EthCoinImpl { ticker: String, pub coin_type: EthCoinType, priv_key_policy: EthPrivKeyPolicy, - pub my_address: Address, + /// Either an Iguana address or a 'EthHDWallet' instance. + /// Arc is used to use the same hd wallet from platform coin if we need to. + /// This allows the reuse of the same derived accounts/addresses of the + /// platform coin for tokens and vice versa. + derivation_method: Arc, sign_message_prefix: Option, swap_contract_address: Address, fallback_swap_contract: Option
, @@ -476,10 +484,17 @@ pub struct EthCoinImpl { /// Coin needs access to the context in order to reuse the logging and shutdown facilities. /// Using a weak reference by default in order to avoid circular references and leaks. pub ctx: MmWeak, - chain_id: Option, + chain_id: u64, + /// The name of the coin with which Trezor wallet associates this asset. + trezor_coin: Option, /// the block range used for eth_getLogs logs_block_range: u64, - nonce_lock: Arc>, + /// A mapping of Ethereum addresses to their respective nonce locks. + /// This is used to ensure that only one transaction is sent at a time per address. + /// Each address is associated with an `AsyncMutex` which is locked when a transaction is being created and sent, + /// and unlocked once the transaction is confirmed. This prevents nonce conflicts when multiple transactions + /// are initiated concurrently from the same address. + address_nonce_locks: Arc>>>>, erc20_tokens_infos: Arc>>, /// Stores information about NFTs owned by the user. Each entry in the HashMap is uniquely identified by a composite key /// consisting of the token address and token ID, separated by a comma. This field is essential for tracking the NFT assets @@ -496,9 +511,13 @@ pub struct Web3Instance { is_parity: bool, } +/// Information about a token that follows the ERC20 protocol on an EVM-based network. #[derive(Clone, Debug)] pub struct Erc20TokenInfo { + /// The contract address of the token on the EVM-based network. pub token_address: Address, + /// The number of decimal places the token uses. + /// This represents the smallest unit that the token can be divided into. pub decimals: u8, } @@ -530,16 +549,16 @@ async fn make_gas_station_request(url: &str) -> GasStationResult { impl EthCoinImpl { #[cfg(not(target_arch = "wasm32"))] - fn eth_traces_path(&self, ctx: &MmArc) -> PathBuf { + fn eth_traces_path(&self, ctx: &MmArc, my_address: Address) -> PathBuf { ctx.dbdir() .join("TRANSACTIONS") - .join(format!("{}_{:#02x}_trace.json", self.ticker, self.my_address)) + .join(format!("{}_{:#02x}_trace.json", self.ticker, my_address)) } /// Load saved ETH traces from local DB #[cfg(not(target_arch = "wasm32"))] - fn load_saved_traces(&self, ctx: &MmArc) -> Option { - let content = gstuff::slurp(&self.eth_traces_path(ctx)); + fn load_saved_traces(&self, ctx: &MmArc, my_address: Address) -> Option { + let content = gstuff::slurp(&self.eth_traces_path(ctx, my_address)); if content.is_empty() { None } else { @@ -552,54 +571,54 @@ impl EthCoinImpl { /// Load saved ETH traces from local DB #[cfg(target_arch = "wasm32")] - fn load_saved_traces(&self, _ctx: &MmArc) -> Option { + fn load_saved_traces(&self, _ctx: &MmArc, _my_address: Address) -> Option { common::panic_w("'load_saved_traces' is not implemented in WASM"); unreachable!() } /// Store ETH traces to local DB #[cfg(not(target_arch = "wasm32"))] - fn store_eth_traces(&self, ctx: &MmArc, traces: &SavedTraces) { + fn store_eth_traces(&self, ctx: &MmArc, my_address: Address, traces: &SavedTraces) { let content = json::to_vec(traces).unwrap(); - let tmp_file = format!("{}.tmp", self.eth_traces_path(ctx).display()); + let tmp_file = format!("{}.tmp", self.eth_traces_path(ctx, my_address).display()); std::fs::write(&tmp_file, content).unwrap(); - std::fs::rename(tmp_file, self.eth_traces_path(ctx)).unwrap(); + std::fs::rename(tmp_file, self.eth_traces_path(ctx, my_address)).unwrap(); } /// Store ETH traces to local DB #[cfg(target_arch = "wasm32")] - fn store_eth_traces(&self, _ctx: &MmArc, _traces: &SavedTraces) { + fn store_eth_traces(&self, _ctx: &MmArc, _my_address: Address, _traces: &SavedTraces) { common::panic_w("'store_eth_traces' is not implemented in WASM"); unreachable!() } #[cfg(not(target_arch = "wasm32"))] - fn erc20_events_path(&self, ctx: &MmArc) -> PathBuf { + fn erc20_events_path(&self, ctx: &MmArc, my_address: Address) -> PathBuf { ctx.dbdir() .join("TRANSACTIONS") - .join(format!("{}_{:#02x}_events.json", self.ticker, self.my_address)) + .join(format!("{}_{:#02x}_events.json", self.ticker, my_address)) } /// Store ERC20 events to local DB #[cfg(not(target_arch = "wasm32"))] - fn store_erc20_events(&self, ctx: &MmArc, events: &SavedErc20Events) { + fn store_erc20_events(&self, ctx: &MmArc, my_address: Address, events: &SavedErc20Events) { let content = json::to_vec(events).unwrap(); - let tmp_file = format!("{}.tmp", self.erc20_events_path(ctx).display()); + let tmp_file = format!("{}.tmp", self.erc20_events_path(ctx, my_address).display()); std::fs::write(&tmp_file, content).unwrap(); - std::fs::rename(tmp_file, self.erc20_events_path(ctx)).unwrap(); + std::fs::rename(tmp_file, self.erc20_events_path(ctx, my_address)).unwrap(); } /// Store ERC20 events to local DB #[cfg(target_arch = "wasm32")] - fn store_erc20_events(&self, _ctx: &MmArc, _events: &SavedErc20Events) { + fn store_erc20_events(&self, _ctx: &MmArc, _my_address: Address, _events: &SavedErc20Events) { common::panic_w("'store_erc20_events' is not implemented in WASM"); unreachable!() } /// Load saved ERC20 events from local DB #[cfg(not(target_arch = "wasm32"))] - fn load_saved_erc20_events(&self, ctx: &MmArc) -> Option { - let content = gstuff::slurp(&self.erc20_events_path(ctx)); + fn load_saved_erc20_events(&self, ctx: &MmArc, my_address: Address) -> Option { + let content = gstuff::slurp(&self.erc20_events_path(ctx, my_address)); if content.is_empty() { None } else { @@ -612,7 +631,7 @@ impl EthCoinImpl { /// Load saved ERC20 events from local DB #[cfg(target_arch = "wasm32")] - fn load_saved_erc20_events(&self, _ctx: &MmArc) -> Option { + fn load_saved_erc20_events(&self, _ctx: &MmArc, _my_address: Address) -> Option { common::panic_w("'load_saved_erc20_events' is not implemented in WASM"); unreachable!() } @@ -671,173 +690,19 @@ async fn get_tx_hex_by_hash_impl(coin: EthCoin, tx_hash: H256) -> RawTransaction } async fn withdraw_impl(coin: EthCoin, req: WithdrawRequest) -> WithdrawResult { - let to_addr = coin - .address_from_str(&req.to) - .map_to_mm(WithdrawError::InvalidAddress)?; - let (my_balance, my_address, key_pair) = match req.from { - Some(WithdrawFrom::HDWalletAddress(ref path_to_address)) => { - let raw_priv_key = coin - .priv_key_policy - .hd_wallet_derived_priv_key_or_err(path_to_address)?; - let key_pair = KeyPair::from_secret_slice(raw_priv_key.as_slice()) - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - let address = key_pair.address(); - let balance = coin.address_balance(address).compat().await?; - (balance, address, key_pair) - }, - Some(WithdrawFrom::AddressId(_)) | Some(WithdrawFrom::DerivationPath { .. }) => { - return MmError::err(WithdrawError::UnexpectedFromAddress( - "Withdraw from 'AddressId' or 'DerivationPath' is not supported yet for EVM!".to_string(), - )) - }, - None => ( - coin.my_balance().compat().await?, - coin.my_address, - coin.priv_key_policy.activated_key_or_err()?.clone(), - ), - }; - let my_balance_dec = u256_to_big_decimal(my_balance, coin.decimals)?; - - let (mut wei_amount, dec_amount) = if req.max { - (my_balance, my_balance_dec.clone()) - } else { - let wei_amount = wei_from_big_decimal(&req.amount, coin.decimals)?; - (wei_amount, req.amount.clone()) - }; - if wei_amount > my_balance { - return MmError::err(WithdrawError::NotSufficientBalance { - coin: coin.ticker.clone(), - available: my_balance_dec.clone(), - required: dec_amount, - }); - }; - let (mut eth_value, data, call_addr, fee_coin) = match &coin.coin_type { - EthCoinType::Eth => (wei_amount, vec![], to_addr, coin.ticker()), - EthCoinType::Erc20 { platform, token_addr } => { - let function = ERC20_CONTRACT.function("transfer")?; - let data = function.encode_input(&[Token::Address(to_addr), Token::Uint(wei_amount)])?; - (0.into(), data, *token_addr, platform.as_str()) - }, - EthCoinType::Nft { .. } => return MmError::err(WithdrawError::NftProtocolNotSupported), - }; - let eth_value_dec = u256_to_big_decimal(eth_value, coin.decimals)?; - - let (gas, gas_price) = - get_eth_gas_details(&coin, req.fee, eth_value, data.clone().into(), call_addr, req.max).await?; - let total_fee = gas * gas_price; - let total_fee_dec = u256_to_big_decimal(total_fee, coin.decimals)?; - - if req.max && coin.coin_type == EthCoinType::Eth { - if eth_value < total_fee || wei_amount < total_fee { - return MmError::err(WithdrawError::AmountTooLow { - amount: eth_value_dec, - threshold: total_fee_dec, - }); - } - eth_value -= total_fee; - wei_amount -= total_fee; - }; - - let (tx_hash, tx_hex) = match coin.priv_key_policy { - EthPrivKeyPolicy::Iguana(_) | EthPrivKeyPolicy::HDWallet { .. } => { - // Todo: nonce_lock is still global for all addresses but this needs to be per address - let _nonce_lock = coin.nonce_lock.lock().await; - let (nonce, _) = coin - .clone() - .get_addr_nonce(my_address) - .compat() - .timeout_secs(30.) - .await? - .map_to_mm(WithdrawError::Transport)?; - - let tx = UnSignedEthTx { - nonce, - value: eth_value, - action: Action::Call(call_addr), - data, - gas, - gas_price, - }; - - let signed = tx.sign(key_pair.secret(), coin.chain_id); - let bytes = rlp::encode(&signed); - - (signed.hash, BytesJson::from(bytes.to_vec())) - }, - EthPrivKeyPolicy::Trezor => { - return MmError::err(WithdrawError::UnsupportedError( - "Trezor is not supported for EVM yet!".to_string(), - )) - }, - #[cfg(target_arch = "wasm32")] - EthPrivKeyPolicy::Metamask(_) => { - if !req.broadcast { - let error = "Set 'broadcast' to generate, sign and broadcast a transaction with MetaMask".to_string(); - return MmError::err(WithdrawError::BroadcastExpected(error)); - } - - let tx_to_send = TransactionRequest { - from: coin.my_address, - to: Some(to_addr), - gas: Some(gas), - gas_price: Some(gas_price), - value: Some(eth_value), - data: Some(data.clone().into()), - nonce: None, - ..TransactionRequest::default() - }; - - // Wait for 10 seconds for the transaction to appear on the RPC node. - let wait_rpc_timeout = 10_000; - let check_every = 1.; - - // Please note that this method may take a long time - // due to `wallet_switchEthereumChain` and `eth_sendTransaction` requests. - let tx_hash = coin.send_transaction(tx_to_send).await?; - - let signed_tx = coin - .wait_for_tx_appears_on_rpc(tx_hash, wait_rpc_timeout, check_every) - .await?; - let tx_hex = signed_tx - .map(|tx| BytesJson::from(rlp::encode(&tx).to_vec())) - // Return an empty `tx_hex` if the transaction is still not appeared on the RPC node. - .unwrap_or_default(); - (tx_hash, tx_hex) - }, - }; - - let tx_hash_bytes = BytesJson::from(tx_hash.0.to_vec()); - let tx_hash_str = format!("{:02x}", tx_hash_bytes); + StandardEthWithdraw::new(coin.clone(), req)?.build().await +} - let amount_decimal = u256_to_big_decimal(wei_amount, coin.decimals)?; - let mut spent_by_me = amount_decimal.clone(); - let received_by_me = if to_addr == my_address { - amount_decimal.clone() - } else { - 0.into() - }; - let fee_details = EthTxFeeDetails::new(gas, gas_price, fee_coin)?; - if coin.coin_type == EthCoinType::Eth { - spent_by_me += &fee_details.total_fee; - } - Ok(TransactionDetails { - to: vec![checksum_address(&format!("{:#02x}", to_addr))], - from: vec![checksum_address(&format!("{:#02x}", my_address))], - total_amount: amount_decimal, - my_balance_change: &received_by_me - &spent_by_me, - spent_by_me, - received_by_me, - tx_hex, - tx_hash: tx_hash_str, - block_height: 0, - fee_details: Some(fee_details.into()), - coin: coin.ticker.clone(), - internal_id: vec![].into(), - timestamp: now_sec(), - kmd_rewards: None, - transaction_type: Default::default(), - memo: None, - }) +#[async_trait] +impl InitWithdrawCoin for EthCoin { + async fn init_withdraw( + &self, + ctx: MmArc, + req: WithdrawRequest, + task_handle: WithdrawTaskHandleShared, + ) -> Result> { + InitEthWithdraw::new(ctx, self.clone(), req, task_handle)?.build().await + } } /// `withdraw_erc1155` function returns details of `ERC-1155` transaction including tx hex, @@ -846,7 +711,6 @@ pub async fn withdraw_erc1155(ctx: MmArc, withdraw_type: WithdrawErc1155) -> Wit let coin = lp_coinfind_or_err(&ctx, withdraw_type.chain.to_ticker()).await?; let (to_addr, token_addr, eth_coin) = get_valid_nft_addr_to_withdraw(coin, &withdraw_type.to, &withdraw_type.token_address)?; - let my_address_str = eth_coin.my_address()?; let token_id_str = &withdraw_type.token_id.to_string(); let wallet_amount = eth_coin.erc1155_balance(token_addr, token_id_str).await?; @@ -866,6 +730,7 @@ pub async fn withdraw_erc1155(ctx: MmArc, withdraw_type: WithdrawErc1155) -> Wit }); } + let my_address = eth_coin.derivation_method.single_addr_or_err().await?; let (eth_value, data, call_addr, fee_coin) = match eth_coin.coin_type { EthCoinType::Eth => { let function = ERC1155_CONTRACT.function("safeTransferFrom")?; @@ -874,7 +739,7 @@ pub async fn withdraw_erc1155(ctx: MmArc, withdraw_type: WithdrawErc1155) -> Wit let amount_u256 = U256::from_dec_str(&amount_dec.to_string()).map_to_mm(|e| NumConversError::new(format!("{:?}", e)))?; let data = function.encode_input(&[ - Token::Address(eth_coin.my_address), + Token::Address(my_address), Token::Address(to_addr), Token::Uint(token_id_u256), Token::Uint(amount_u256), @@ -894,14 +759,16 @@ pub async fn withdraw_erc1155(ctx: MmArc, withdraw_type: WithdrawErc1155) -> Wit withdraw_type.fee, eth_value, data.clone().into(), + my_address, call_addr, false, ) .await?; - let _nonce_lock = eth_coin.nonce_lock.lock().await; + let address_lock = eth_coin.get_address_lock(my_address.to_string()).await; + let _nonce_lock = address_lock.lock().await; let (nonce, _) = eth_coin .clone() - .get_addr_nonce(eth_coin.my_address) + .get_addr_nonce(my_address) .compat() .timeout_secs(30.) .await? @@ -910,21 +777,21 @@ pub async fn withdraw_erc1155(ctx: MmArc, withdraw_type: WithdrawErc1155) -> Wit let tx = UnSignedEthTx { nonce, value: eth_value, - action: Action::Call(call_addr), + action: Call(call_addr), data, gas, gas_price, }; let secret = eth_coin.priv_key_policy.activated_key_or_err()?.secret(); - let signed = tx.sign(secret, eth_coin.chain_id); + let signed = tx.sign(secret, Some(eth_coin.chain_id)); let signed_bytes = rlp::encode(&signed); let fee_details = EthTxFeeDetails::new(gas, gas_price, fee_coin)?; Ok(TransactionNftDetails { tx_hex: BytesJson::from(signed_bytes.to_vec()), tx_hash: format!("{:02x}", signed.tx_hash()), - from: vec![my_address_str], + from: vec![eth_coin.my_address()?], to: vec![withdraw_type.to], contract_type: ContractType::Erc1155, token_address: withdraw_type.token_address, @@ -945,11 +812,10 @@ pub async fn withdraw_erc721(ctx: MmArc, withdraw_type: WithdrawErc721) -> Withd let coin = lp_coinfind_or_err(&ctx, withdraw_type.chain.to_ticker()).await?; let (to_addr, token_addr, eth_coin) = get_valid_nft_addr_to_withdraw(coin, &withdraw_type.to, &withdraw_type.token_address)?; - let my_address_str = eth_coin.my_address()?; let token_id_str = &withdraw_type.token_id.to_string(); let token_owner = eth_coin.erc721_owner(token_addr, token_id_str).await?; - let my_address = eth_coin.my_address; + let my_address = eth_coin.derivation_method.single_addr_or_err().await?; if token_owner != my_address { return MmError::err(WithdrawError::MyAddressNotNftOwner { my_address: eth_addr_to_hex(&my_address), @@ -957,6 +823,7 @@ pub async fn withdraw_erc721(ctx: MmArc, withdraw_type: WithdrawErc721) -> Withd }); } + let my_address = eth_coin.derivation_method.single_addr_or_err().await?; let (eth_value, data, call_addr, fee_coin) = match eth_coin.coin_type { EthCoinType::Eth => { let function = ERC721_CONTRACT.function("safeTransferFrom")?; @@ -982,11 +849,14 @@ pub async fn withdraw_erc721(ctx: MmArc, withdraw_type: WithdrawErc721) -> Withd withdraw_type.fee, eth_value, data.clone().into(), + my_address, call_addr, false, ) .await?; - let _nonce_lock = eth_coin.nonce_lock.lock().await; + + let address_lock = eth_coin.get_address_lock(my_address.to_string()).await; + let _nonce_lock = address_lock.lock().await; let (nonce, _) = eth_coin .clone() .get_addr_nonce(my_address) @@ -998,21 +868,21 @@ pub async fn withdraw_erc721(ctx: MmArc, withdraw_type: WithdrawErc721) -> Withd let tx = UnSignedEthTx { nonce, value: eth_value, - action: Action::Call(call_addr), + action: Call(call_addr), data, gas, gas_price, }; let secret = eth_coin.priv_key_policy.activated_key_or_err()?.secret(); - let signed = tx.sign(secret, eth_coin.chain_id); + let signed = tx.sign(secret, Some(eth_coin.chain_id)); let signed_bytes = rlp::encode(&signed); let fee_details = EthTxFeeDetails::new(gas, gas_price, fee_coin)?; Ok(TransactionNftDetails { tx_hex: BytesJson::from(signed_bytes.to_vec()), tx_hash: format!("{:02x}", signed.tx_hash()), - from: vec![my_address_str], + from: vec![eth_coin.my_address()?], to: vec![withdraw_type.to], contract_type: ContractType::Erc721, token_address: withdraw_type.token_address, @@ -1062,32 +932,34 @@ impl SwapOps for EthCoin { ) } - fn send_maker_spends_taker_payment(&self, maker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { - Box::new( - self.spend_hash_time_locked_payment(maker_spends_payment_args) - .map(TransactionEnum::from), - ) + async fn send_maker_spends_taker_payment( + &self, + maker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { + self.spend_hash_time_locked_payment(maker_spends_payment_args) + .await + .map(TransactionEnum::from) } - fn send_taker_spends_maker_payment(&self, taker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { - Box::new( - self.spend_hash_time_locked_payment(taker_spends_payment_args) - .map(TransactionEnum::from), - ) + async fn send_taker_spends_maker_payment( + &self, + taker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { + self.spend_hash_time_locked_payment(taker_spends_payment_args) + .await + .map(TransactionEnum::from) } async fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { self.refund_hash_time_locked_payment(taker_refunds_payment_args) - .map(TransactionEnum::from) - .compat() .await + .map(TransactionEnum::from) } async fn send_maker_refunds_payment(&self, maker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { self.refund_hash_time_locked_payment(maker_refunds_payment_args) - .map(TransactionEnum::from) - .compat() .await + .map(TransactionEnum::from) } fn validate_fee(&self, validate_fee_args: ValidateFeeArgs<'_>) -> ValidatePaymentFut<()> { @@ -1564,11 +1436,12 @@ impl WatcherOps for EthCoin { ))); } + let my_address = selfi.derivation_method.single_addr_or_err().await?; let sender_input = get_function_input_data(&decoded, function, 4) .map_to_mm(ValidatePaymentError::TxDeserializationError)?; let expected_sender = match input.spend_type { WatcherSpendType::MakerPaymentSpend => maker_addr, - WatcherSpendType::TakerPaymentRefund => selfi.my_address, + WatcherSpendType::TakerPaymentRefund => my_address, }; if sender_input != Token::Address(expected_sender) { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( @@ -1581,7 +1454,7 @@ impl WatcherOps for EthCoin { let receiver_input = get_function_input_data(&decoded, function, 5) .map_to_mm(ValidatePaymentError::TxDeserializationError)?; let expected_receiver = match input.spend_type { - WatcherSpendType::MakerPaymentSpend => selfi.my_address, + WatcherSpendType::MakerPaymentSpend => my_address, WatcherSpendType::TakerPaymentRefund => maker_addr, }; if receiver_input != Token::Address(expected_receiver) { @@ -2043,14 +1916,20 @@ impl WatcherOps for EthCoin { #[async_trait] #[cfg_attr(test, mockable)] +#[async_trait] impl MarketCoinOps for EthCoin { fn ticker(&self) -> &str { &self.ticker[..] } fn my_address(&self) -> MmResult { - Ok(checksum_address(&format!("{:#02x}", self.my_address))) + match self.derivation_method() { + DerivationMethod::SingleAddress(my_address) => Ok(display_eth_address(my_address)), + DerivationMethod::HDWallet(_) => MmError::err(MyAddressError::UnexpectedDerivationMethod( + "'my_address' is deprecated for HD wallets".to_string(), + )), + } } - fn get_public_key(&self) -> Result> { + async fn get_public_key(&self) -> Result> { match self.priv_key_policy { EthPrivKeyPolicy::Iguana(ref key_pair) | EthPrivKeyPolicy::HDWallet { @@ -2060,7 +1939,19 @@ impl MarketCoinOps for EthCoin { let uncompressed_without_prefix = hex::encode(key_pair.public()); Ok(format!("04{}", uncompressed_without_prefix)) }, - EthPrivKeyPolicy::Trezor => MmError::err(UnexpectedDerivationMethod::Trezor), + EthPrivKeyPolicy::Trezor => { + let public_key = self + .deref() + .derivation_method + .hd_wallet() + .ok_or(UnexpectedDerivationMethod::ExpectedHDWallet)? + .get_enabled_address() + .await + .ok_or_else(|| UnexpectedDerivationMethod::InternalError("no enabled address".to_owned()))? + .pubkey(); + let uncompressed_without_prefix = hex::encode(public_key); + Ok(format!("04{}", uncompressed_without_prefix)) + }, #[cfg(target_arch = "wasm32")] EthPrivKeyPolicy::Metamask(ref metamask_policy) => { Ok(format!("{:02x}", metamask_policy.public_key_uncompressed)) @@ -2104,7 +1995,7 @@ impl MarketCoinOps for EthCoin { fn my_balance(&self) -> BalanceFut { let decimals = self.decimals; let fut = self - .my_balance() + .get_balance() .and_then(move |result| Ok(u256_to_big_decimal(result, decimals)?)) .map(|spendable| CoinBalance { spendable, @@ -2384,9 +2275,9 @@ impl MarketCoinOps for EthCoin { activated_key: ref key_pair, .. } => Ok(format!("{:#02x}", key_pair.secret())), - EthPrivKeyPolicy::Trezor => ERR!("'display_priv_key' doesn't support Trezor yet!"), + EthPrivKeyPolicy::Trezor => ERR!("'display_priv_key' is not supported for Hardware Wallets"), #[cfg(target_arch = "wasm32")] - EthPrivKeyPolicy::Metamask(_) => ERR!("'display_priv_key' doesn't support MetaMask"), + EthPrivKeyPolicy::Metamask(_) => ERR!("'display_priv_key' is not supported for MetaMask"), } } @@ -2398,6 +2289,8 @@ impl MarketCoinOps for EthCoin { let pow = self.decimals as u32; MmNumber::from(1) / MmNumber::from(10u64.pow(pow)) } + + fn is_trezor(&self) -> bool { self.priv_key_policy.is_trezor() } } pub fn signed_eth_tx_from_bytes(bytes: &[u8]) -> Result { @@ -2406,10 +2299,13 @@ pub fn signed_eth_tx_from_bytes(bytes: &[u8]) -> Result { Ok(signed) } +type AddressNonceLocks = Mutex>>>>; + // We can use a nonce lock shared between tokens using the same platform coin and the platform itself. // For example, ETH/USDT-ERC20 should use the same lock, but it will be different for BNB/USDT-BEP20. +// This lock is used to ensure that only one transaction is sent at a time per address. lazy_static! { - static ref NONCE_LOCK: Mutex>>> = Mutex::new(HashMap::new()); + static ref NONCE_LOCK: AddressNonceLocks = Mutex::new(HashMap::new()); } type EthTxFut = Box + Send + 'static>; @@ -2419,26 +2315,18 @@ type EthTxFut = Box + Sen /// This method polls for the latest nonce from the RPC nodes and uses it for the transaction to be signed. /// A `nonce_lock` is returned so that the caller doesn't release it until the transaction is sent and the /// address nonce is updated on RPC nodes. -async fn sign_transaction_with_keypair<'a>( - ctx: MmArc, - coin: &'a EthCoin, +async fn sign_transaction_with_keypair( + coin: &EthCoin, key_pair: &KeyPair, value: U256, action: Action, data: Vec, gas: U256, -) -> Result<(SignedEthTx, Vec, AsyncMutexGuard<'a, ()>), TransactionErr> { - let mut status = ctx.log.status_handle(); - macro_rules! tags { - () => { - &[&"sign"] - }; - } - let nonce_lock = coin.nonce_lock.lock().await; - status.status(tags!(), "get_addr_nonce…"); - let (nonce, web3_instances_with_latest_nonce) = - try_tx_s!(coin.clone().get_addr_nonce(coin.my_address).compat().await); - status.status(tags!(), "get_gas_price…"); + from_address: Address, +) -> Result<(SignedEthTx, Vec), TransactionErr> { + info!(target: "sign", "get_addr_nonce…"); + let (nonce, web3_instances_with_latest_nonce) = try_tx_s!(coin.clone().get_addr_nonce(from_address).compat().await); + info!(target: "sign", "get_gas_price…"); let gas_price = try_tx_s!(coin.get_gas_price().compat().await); let tx = UnSignedEthTx { @@ -2451,39 +2339,35 @@ async fn sign_transaction_with_keypair<'a>( }; Ok(( - tx.sign(key_pair.secret(), coin.chain_id), + tx.sign(key_pair.secret(), Some(coin.chain_id)), web3_instances_with_latest_nonce, - nonce_lock, )) } async fn sign_and_send_transaction_with_keypair( - ctx: MmArc, coin: &EthCoin, key_pair: &KeyPair, + address: Address, value: U256, action: Action, data: Vec, gas: U256, ) -> Result { - let mut status = ctx.log.status_handle(); - macro_rules! tags { - () => { - &[&"sign-and-send"] - }; - } - let (signed, web3_instances_with_latest_nonce, _nonce_lock) = - sign_transaction_with_keypair(ctx, coin, key_pair, value, action, data, gas).await?; + let my_address = try_tx_s!(coin.derivation_method.single_addr_or_err().await); + let address_lock = coin.get_address_lock(my_address.to_string()).await; + let _nonce_lock = address_lock.lock().await; + let (signed, web3_instances_with_latest_nonce) = + sign_transaction_with_keypair(coin, key_pair, value, action, data, gas, my_address).await?; let bytes = Bytes(rlp::encode(&signed).to_vec()); - status.status(tags!(), "send_raw_transaction…"); + info!(target: "sign-and-send", "send_raw_transaction…"); let futures = web3_instances_with_latest_nonce .into_iter() .map(|web3_instance| web3_instance.web3.eth().send_raw_transaction(bytes.clone())); try_tx_s!(select_ok(futures).await.map_err(|e| ERRL!("{}", e)), signed); - status.status(tags!(), "get_addr_nonce…"); - coin.wait_for_addr_nonce_increase(coin.my_address, signed.transaction.unsigned.nonce) + info!(target: "sign-and-send", "wait_for_tx_appears_on_rpc…"); + coin.wait_for_addr_nonce_increase(address, signed.transaction.unsigned.nonce) .await; Ok(signed) } @@ -2501,10 +2385,11 @@ async fn sign_and_send_transaction_with_metamask( Action::Call(to) => Some(to), }; + let my_address = try_tx_s!(coin.derivation_method.single_addr_or_err().await); let gas_price = try_tx_s!(coin.get_gas_price().compat().await); let tx_to_send = TransactionRequest { - from: coin.my_address, + from: my_address, to, gas: Some(gas), gas_price: Some(gas_price), @@ -2538,9 +2423,6 @@ async fn sign_and_send_transaction_with_metamask( /// Sign eth transaction async fn sign_raw_eth_tx(coin: &EthCoin, args: &SignEthTransactionParams) -> RawTransactionResult { - let ctx = MmArc::from_weak(&coin.ctx) - .ok_or("!ctx") - .map_to_mm(|err| RawTransactionError::TransactionError(err.to_string()))?; let value = wei_from_big_decimal(args.value.as_ref().unwrap_or(&BigDecimal::from(0)), coin.decimals)?; let action = if let Some(to) = &args.to { Call(Address::from_str(to).map_to_mm(|err| RawTransactionError::InvalidParam(err.to_string()))?) @@ -2555,9 +2437,16 @@ async fn sign_raw_eth_tx(coin: &EthCoin, args: &SignEthTransactionParams) -> Raw activated_key: ref key_pair, .. } => { - return sign_transaction_with_keypair(ctx, coin, key_pair, value, action, data, args.gas_limit) + let my_address = coin + .derivation_method + .single_addr_or_err() .await - .map(|(signed_tx, _, _)| RawTransactionRes { + .mm_err(|e| RawTransactionError::InternalError(e.to_string()))?; + let address_lock = coin.get_address_lock(my_address.to_string()).await; + let _nonce_lock = address_lock.lock().await; + return sign_transaction_with_keypair(coin, key_pair, value, action, data, args.gas_limit, my_address) + .await + .map(|(signed_tx, _)| RawTransactionRes { tx_hex: signed_tx.tx_hex().into(), }) .map_to_mm(|err| RawTransactionError::TransactionError(err.get_plain_text_format())); @@ -2658,32 +2547,29 @@ impl EthCoin { } /// Gets ETH traces from ETH node between addresses in `from_block` and `to_block` - fn eth_traces( + async fn eth_traces( &self, from_addr: Vec
, to_addr: Vec
, from_block: BlockNumber, to_block: BlockNumber, limit: Option, - ) -> Box, Error = String> + Send> { + ) -> Web3RpcResult> { let mut filter = TraceFilterBuilder::default() .from_address(from_addr) .to_address(to_addr) .from_block(from_block) .to_block(to_block); - if let Some(l) = limit { filter = filter.count(l); } + drop_mutability!(filter); - let coin = self.clone(); - - let fut = async move { coin.trace_filter(filter.build()).await.map_err(|e| ERRL!("{}", e)) }; - Box::new(fut.boxed().compat()) + self.trace_filter(filter.build()).await.map_to_mm(Web3RpcError::from) } /// Gets Transfer events from ERC20 smart contract `addr` between `from_block` and `to_block` - fn erc20_transfer_events( + async fn erc20_transfer_events( &self, contract: Address, from_addr: Option
, @@ -2691,26 +2577,23 @@ impl EthCoin { from_block: BlockNumber, to_block: BlockNumber, limit: Option, - ) -> Box, Error = String> + Send> { - let contract_event = try_fus!(ERC20_CONTRACT.event("Transfer")); + ) -> Web3RpcResult> { + let contract_event = ERC20_CONTRACT.event("Transfer")?; let topic0 = Some(vec![contract_event.signature()]); let topic1 = from_addr.map(|addr| vec![addr.into()]); let topic2 = to_addr.map(|addr| vec![addr.into()]); + let mut filter = FilterBuilder::default() .topics(topic0, topic1, topic2, None) .from_block(from_block) .to_block(to_block) .address(vec![contract]); - if let Some(l) = limit { filter = filter.limit(l); } + drop_mutability!(filter); - let coin = self.clone(); - - let fut = async move { coin.logs(filter.build()).await.map_err(|e| ERRL!("{}", e)) }; - - Box::new(fut.boxed().compat()) + self.logs(filter.build()).await.map_to_mm(Web3RpcError::from) } /// Downloads and saves ETH transaction history of my_address, relies on Parity trace_filter API @@ -2724,6 +2607,17 @@ impl EthCoin { // Also the Parity RPC server seem to get stuck while request in running (other requests performance is also lowered). let delta = U64::from(1000); + let my_address = match self.derivation_method.single_addr_or_err().await { + Ok(addr) => addr, + Err(e) => { + ctx.log.log( + "", + &[&"tx_history", &self.ticker], + &ERRL!("Error on getting my address: {}", e), + ); + return; + }, + }; let mut success_iteration = 0i32; loop { if ctx.is_stopping() { @@ -2751,7 +2645,7 @@ impl EthCoin { }, }; - let mut saved_traces = match self.load_saved_traces(ctx) { + let mut saved_traces = match self.load_saved_traces(ctx, my_address) { Some(traces) => traces, None => SavedTraces { traces: vec![], @@ -2787,13 +2681,12 @@ impl EthCoin { let from_traces_before_earliest = match self .eth_traces( - vec![self.my_address], + vec![my_address], vec![], BlockNumber::Number(before_earliest), BlockNumber::Number(saved_traces.earliest_block), None, ) - .compat() .await { Ok(traces) => traces, @@ -2811,12 +2704,11 @@ impl EthCoin { let to_traces_before_earliest = match self .eth_traces( vec![], - vec![self.my_address], + vec![my_address], BlockNumber::Number(before_earliest), BlockNumber::Number(saved_traces.earliest_block), None, ) - .compat() .await { Ok(traces) => traces, @@ -2843,19 +2735,18 @@ impl EthCoin { } else { 0.into() }; - self.store_eth_traces(ctx, &saved_traces); + self.store_eth_traces(ctx, my_address, &saved_traces); } if current_block > saved_traces.latest_block { let from_traces_after_latest = match self .eth_traces( - vec![self.my_address], + vec![my_address], vec![], BlockNumber::Number(saved_traces.latest_block + 1), BlockNumber::Number(current_block), None, ) - .compat() .await { Ok(traces) => traces, @@ -2873,12 +2764,11 @@ impl EthCoin { let to_traces_after_latest = match self .eth_traces( vec![], - vec![self.my_address], + vec![my_address], BlockNumber::Number(saved_traces.latest_block + 1), BlockNumber::Number(current_block), None, ) - .compat() .await { Ok(traces) => traces, @@ -2901,7 +2791,7 @@ impl EthCoin { saved_traces.traces.extend(to_traces_after_latest); saved_traces.latest_block = current_block; - self.store_eth_traces(ctx, &saved_traces); + self.store_eth_traces(ctx, my_address, &saved_traces); } saved_traces.traces.sort_by(|a, b| b.block_number.cmp(&a.block_number)); for trace in saved_traces.traces { @@ -2995,7 +2885,7 @@ impl EthCoin { let mut received_by_me = 0.into(); let mut spent_by_me = 0.into(); - if call_data.from == self.my_address { + if call_data.from == my_address { // ETH transfer is actually happening only if no error occurred if trace.error.is_none() { spent_by_me = total_amount.clone(); @@ -3005,7 +2895,7 @@ impl EthCoin { } } - if call_data.to == self.my_address { + if call_data.to == my_address { // ETH transfer is actually happening only if no error occurred if trace.error.is_none() { received_by_me = total_amount.clone(); @@ -3033,8 +2923,8 @@ impl EthCoin { spent_by_me, received_by_me, total_amount, - to: vec![checksum_address(&format!("{:#02x}", call_data.to))], - from: vec![checksum_address(&format!("{:#02x}", call_data.from))], + to: vec![display_eth_address(&call_data.to)], + from: vec![display_eth_address(&call_data.from)], coin: self.ticker.clone(), fee_details: fee_details.map(|d| d.into()), block_height: trace.block_number, @@ -3082,6 +2972,17 @@ impl EthCoin { async fn process_erc20_history(&self, token_addr: H160, ctx: &MmArc) { let delta = U64::from(10000); + let my_address = match self.derivation_method.single_addr_or_err().await { + Ok(addr) => addr, + Err(e) => { + ctx.log.log( + "", + &[&"tx_history", &self.ticker], + &ERRL!("Error on getting my address: {}", e), + ); + return; + }, + }; let mut success_iteration = 0i32; loop { if ctx.is_stopping() { @@ -3109,7 +3010,7 @@ impl EthCoin { }, }; - let mut saved_events = match self.load_saved_erc20_events(ctx) { + let mut saved_events = match self.load_saved_erc20_events(ctx, my_address) { Some(events) => events, None => SavedErc20Events { events: vec![], @@ -3134,13 +3035,12 @@ impl EthCoin { let from_events_before_earliest = match self .erc20_transfer_events( token_addr, - Some(self.my_address), + Some(my_address), None, BlockNumber::Number(before_earliest), BlockNumber::Number(saved_events.earliest_block - 1), None, ) - .compat() .await { Ok(events) => events, @@ -3159,12 +3059,11 @@ impl EthCoin { .erc20_transfer_events( token_addr, None, - Some(self.my_address), + Some(my_address), BlockNumber::Number(before_earliest), BlockNumber::Number(saved_events.earliest_block - 1), None, ) - .compat() .await { Ok(events) => events, @@ -3190,20 +3089,19 @@ impl EthCoin { } else { 0.into() }; - self.store_erc20_events(ctx, &saved_events); + self.store_erc20_events(ctx, my_address, &saved_events); } if current_block > saved_events.latest_block { let from_events_after_latest = match self .erc20_transfer_events( token_addr, - Some(self.my_address), + Some(my_address), None, BlockNumber::Number(saved_events.latest_block + 1), BlockNumber::Number(current_block), None, ) - .compat() .await { Ok(events) => events, @@ -3222,12 +3120,11 @@ impl EthCoin { .erc20_transfer_events( token_addr, None, - Some(self.my_address), + Some(my_address), BlockNumber::Number(saved_events.latest_block + 1), BlockNumber::Number(current_block), None, ) - .compat() .await { Ok(events) => events, @@ -3249,7 +3146,7 @@ impl EthCoin { saved_events.events.extend(from_events_after_latest); saved_events.events.extend(to_events_after_latest); saved_events.latest_block = current_block; - self.store_erc20_events(ctx, &saved_events); + self.store_erc20_events(ctx, my_address, &saved_events); } let all_events: HashMap<_, _> = saved_events @@ -3287,11 +3184,11 @@ impl EthCoin { let from_addr = H160::from(event.topics[1]); let to_addr = H160::from(event.topics[2]); - if from_addr == self.my_address { + if from_addr == my_address { spent_by_me = total_amount.clone(); } - if to_addr == self.my_address { + if to_addr == my_address { received_by_me = total_amount.clone(); } @@ -3397,8 +3294,8 @@ impl EthCoin { spent_by_me, received_by_me, total_amount, - to: vec![checksum_address(&format!("{:#02x}", to_addr))], - from: vec![checksum_address(&format!("{:#02x}", from_addr))], + to: vec![display_eth_address(&to_addr)], + from: vec![display_eth_address(&from_addr)], coin: self.ticker.clone(), fee_details: fee_details.map(|d| d.into()), block_height: block_number.as_u64(), @@ -3439,12 +3336,25 @@ impl EthCoin { } } } + + /// Retrieves the lock associated with a given address. + /// + /// This function is used to ensure that only one transaction is sent at a time per address. + /// If the address does not have an associated lock, a new one is created and stored. + async fn get_address_lock(&self, address: String) -> Arc> { + let address_lock = { + let mut lock = self.address_nonce_locks.lock().await; + lock.entry(address) + .or_insert_with(|| Arc::new(AsyncMutex::new(()))) + .clone() + }; + address_lock + } } #[cfg_attr(test, mockable)] impl EthCoin { pub(crate) fn sign_and_send_transaction(&self, value: U256, action: Action, data: Vec, gas: U256) -> EthTxFut { - let ctx = try_tx_fus!(MmArc::from_weak(&self.ctx).ok_or("!ctx")); let coin = self.clone(); let fut = async move { match coin.priv_key_policy { @@ -3452,8 +3362,15 @@ impl EthCoin { | EthPrivKeyPolicy::HDWallet { activated_key: ref key_pair, .. - } => sign_and_send_transaction_with_keypair(ctx, &coin, key_pair, value, action, data, gas).await, - EthPrivKeyPolicy::Trezor => Err(TransactionErr::Plain(ERRL!("Trezor is not supported for EVM yet!"))), + } => { + let address = coin + .derivation_method + .single_addr_or_err() + .await + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; + sign_and_send_transaction_with_keypair(&coin, key_pair, address, value, action, data, gas).await + }, + EthPrivKeyPolicy::Trezor => Err(TransactionErr::Plain(ERRL!("Trezor is not supported for swaps yet!"))), #[cfg(target_arch = "wasm32")] EthPrivKeyPolicy::Metamask(_) => { sign_and_send_transaction_with_metamask(coin, value, action, data, gas).await @@ -3465,7 +3382,7 @@ impl EthCoin { pub fn send_to_address(&self, address: Address, value: U256) -> EthTxFut { match &self.coin_type { - EthCoinType::Eth => self.sign_and_send_transaction(value, Action::Call(address), vec![], U256::from(21000)), + EthCoinType::Eth => self.sign_and_send_transaction(value, Call(address), vec![], U256::from(21000)), EthCoinType::Erc20 { platform: _, token_addr, @@ -3473,7 +3390,7 @@ impl EthCoin { let abi = try_tx_fus!(Contract::load(ERC20_ABI.as_bytes())); let function = try_tx_fus!(abi.function("transfer")); let data = try_tx_fus!(function.encode_input(&[Token::Address(address), Token::Uint(value)])); - self.sign_and_send_transaction(0.into(), Action::Call(*token_addr), data, U256::from(210_000)) + self.sign_and_send_transaction(0.into(), Call(*token_addr), data, U256::from(210_000)) }, EthCoinType::Nft { .. } => { return Box::new(futures01::future::err(TransactionErr::ProtocolNotSupported(ERRL!( @@ -3529,7 +3446,7 @@ impl EthCoin { ])), }; - self.sign_and_send_transaction(value, Action::Call(swap_contract_address), data, gas) + self.sign_and_send_transaction(value, Call(swap_contract_address), data, gas) }, EthCoinType::Erc20 { platform: _, @@ -3623,7 +3540,7 @@ impl EthCoin { .and_then(move |_| { arc.sign_and_send_transaction( value, - Action::Call(swap_contract_address), + Call(swap_contract_address), data, gas, ) @@ -3633,7 +3550,7 @@ impl EthCoin { } else { Box::new(arc.sign_and_send_transaction( value, - Action::Call(swap_contract_address), + Call(swap_contract_address), data, gas, )) @@ -3706,7 +3623,7 @@ impl EthCoin { clone.sign_and_send_transaction( 0.into(), - Action::Call(swap_contract_address), + Call(swap_contract_address), data, U256::from(ETH_GAS), ) @@ -3754,7 +3671,7 @@ impl EthCoin { ])); clone.sign_and_send_transaction( 0.into(), - Action::Call(swap_contract_address), + Call(swap_contract_address), data, U256::from(ETH_GAS), ) @@ -3828,7 +3745,7 @@ impl EthCoin { clone.sign_and_send_transaction( 0.into(), - Action::Call(swap_contract_address), + Call(swap_contract_address), data, U256::from(ETH_GAS), ) @@ -3879,7 +3796,7 @@ impl EthCoin { clone.sign_and_send_transaction( 0.into(), - Action::Call(swap_contract_address), + Call(swap_contract_address), data, U256::from(ETH_GAS), ) @@ -3894,247 +3811,232 @@ impl EthCoin { } } - fn spend_hash_time_locked_payment(&self, args: SpendPaymentArgs) -> EthTxFut { - let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(args.other_payment_tx)); - let payment = try_tx_fus!(SignedEthTx::new(tx)); - let swap_contract_address = try_tx_fus!(args.swap_contract_address.try_to_address()); + async fn spend_hash_time_locked_payment<'a>( + &self, + args: SpendPaymentArgs<'a>, + ) -> Result { + let tx: UnverifiedTransaction = try_tx_s!(rlp::decode(args.other_payment_tx)); + let payment = try_tx_s!(SignedEthTx::new(tx)); + let my_address = try_tx_s!(self.derivation_method.single_addr_or_err().await); + let swap_contract_address = try_tx_s!(args.swap_contract_address.try_to_address()); let function_name = get_function_name("receiverSpend", args.watcher_reward); - let spend_func = try_tx_fus!(SWAP_CONTRACT.function(&function_name)); + let spend_func = try_tx_s!(SWAP_CONTRACT.function(&function_name)); - let clone = self.clone(); let secret_vec = args.secret.to_vec(); let watcher_reward = args.watcher_reward; match self.coin_type { EthCoinType::Eth => { let function_name = get_function_name("ethPayment", watcher_reward); - let payment_func = try_tx_fus!(SWAP_CONTRACT.function(&function_name)); - let decoded = try_tx_fus!(decode_contract_call(payment_func, &payment.data)); + let payment_func = try_tx_s!(SWAP_CONTRACT.function(&function_name)); + let decoded = try_tx_s!(decode_contract_call(payment_func, &payment.data)); - let state_f = self.payment_status(swap_contract_address, decoded[0].clone()); - Box::new( - state_f - .map_err(TransactionErr::Plain) - .and_then(move |state| -> EthTxFut { - if state != U256::from(PaymentState::Sent as u8) { - return Box::new(futures01::future::err(TransactionErr::Plain(ERRL!( - "Payment {:?} state is not PAYMENT_STATE_SENT, got {}", - payment, - state - )))); - } + let state = try_tx_s!( + self.payment_status(swap_contract_address, decoded[0].clone()) + .compat() + .await + ); + if state != U256::from(PaymentState::Sent as u8) { + return Err(TransactionErr::Plain(ERRL!( + "Payment {:?} state is not PAYMENT_STATE_SENT, got {}", + payment, + state + ))); + } - let data = if watcher_reward { - try_tx_fus!(spend_func.encode_input(&[ - decoded[0].clone(), - Token::Uint(payment.value), - Token::FixedBytes(secret_vec), - Token::Address(Address::default()), - Token::Address(payment.sender()), - Token::Address(clone.my_address), - decoded[4].clone(), - decoded[5].clone(), - decoded[6].clone(), - ])) - } else { - try_tx_fus!(spend_func.encode_input(&[ - decoded[0].clone(), - Token::Uint(payment.value), - Token::FixedBytes(secret_vec), - Token::Address(Address::default()), - Token::Address(payment.sender()), - ])) - }; + let data = if watcher_reward { + try_tx_s!(spend_func.encode_input(&[ + decoded[0].clone(), + Token::Uint(payment.value), + Token::FixedBytes(secret_vec), + Token::Address(Address::default()), + Token::Address(payment.sender()), + Token::Address(my_address), + decoded[4].clone(), + decoded[5].clone(), + decoded[6].clone(), + ])) + } else { + try_tx_s!(spend_func.encode_input(&[ + decoded[0].clone(), + Token::Uint(payment.value), + Token::FixedBytes(secret_vec), + Token::Address(Address::default()), + Token::Address(payment.sender()), + ])) + }; - clone.sign_and_send_transaction( - 0.into(), - Action::Call(swap_contract_address), - data, - U256::from(ETH_GAS), - ) - }), - ) + self.sign_and_send_transaction(0.into(), Call(swap_contract_address), data, U256::from(ETH_GAS)) + .compat() + .await }, EthCoinType::Erc20 { platform: _, token_addr, } => { let function_name = get_function_name("erc20Payment", watcher_reward); - let payment_func = try_tx_fus!(SWAP_CONTRACT.function(&function_name)); + let payment_func = try_tx_s!(SWAP_CONTRACT.function(&function_name)); - let decoded = try_tx_fus!(decode_contract_call(payment_func, &payment.data)); - let state_f = self.payment_status(swap_contract_address, decoded[0].clone()); + let decoded = try_tx_s!(decode_contract_call(payment_func, &payment.data)); + let state = try_tx_s!( + self.payment_status(swap_contract_address, decoded[0].clone()) + .compat() + .await + ); + if state != U256::from(PaymentState::Sent as u8) { + return Err(TransactionErr::Plain(ERRL!( + "Payment {:?} state is not PAYMENT_STATE_SENT, got {}", + payment, + state + ))); + } - Box::new( - state_f - .map_err(TransactionErr::Plain) - .and_then(move |state| -> EthTxFut { - if state != U256::from(PaymentState::Sent as u8) { - return Box::new(futures01::future::err(TransactionErr::Plain(ERRL!( - "Payment {:?} state is not PAYMENT_STATE_SENT, got {}", - payment, - state - )))); - } - let data = if watcher_reward { - try_tx_fus!(spend_func.encode_input(&[ - decoded[0].clone(), - decoded[1].clone(), - Token::FixedBytes(secret_vec), - Token::Address(token_addr), - Token::Address(payment.sender()), - Token::Address(clone.my_address), - decoded[6].clone(), - decoded[7].clone(), - decoded[8].clone(), - ])) - } else { - try_tx_fus!(spend_func.encode_input(&[ - decoded[0].clone(), - decoded[1].clone(), - Token::FixedBytes(secret_vec), - Token::Address(token_addr), - Token::Address(payment.sender()), - ])) - }; + let data = if watcher_reward { + try_tx_s!(spend_func.encode_input(&[ + decoded[0].clone(), + decoded[1].clone(), + Token::FixedBytes(secret_vec), + Token::Address(token_addr), + Token::Address(payment.sender()), + Token::Address(my_address), + decoded[6].clone(), + decoded[7].clone(), + decoded[8].clone(), + ])) + } else { + try_tx_s!(spend_func.encode_input(&[ + decoded[0].clone(), + decoded[1].clone(), + Token::FixedBytes(secret_vec), + Token::Address(token_addr), + Token::Address(payment.sender()), + ])) + }; - clone.sign_and_send_transaction( - 0.into(), - Action::Call(swap_contract_address), - data, - U256::from(ETH_GAS), - ) - }), - ) + self.sign_and_send_transaction(0.into(), Call(swap_contract_address), data, U256::from(ETH_GAS)) + .compat() + .await }, EthCoinType::Nft { .. } => { - return Box::new(futures01::future::err(TransactionErr::ProtocolNotSupported(ERRL!( + return Err(TransactionErr::ProtocolNotSupported(ERRL!( "Nft Protocol is not supported!" - )))) + ))) }, } } - fn refund_hash_time_locked_payment(&self, args: RefundPaymentArgs) -> EthTxFut { - let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(args.payment_tx)); - let payment = try_tx_fus!(SignedEthTx::new(tx)); - let swap_contract_address = try_tx_fus!(args.swap_contract_address.try_to_address()); + async fn refund_hash_time_locked_payment<'a>( + &self, + args: RefundPaymentArgs<'a>, + ) -> Result { + let tx: UnverifiedTransaction = try_tx_s!(rlp::decode(args.payment_tx)); + let payment = try_tx_s!(SignedEthTx::new(tx)); + let my_address = try_tx_s!(self.derivation_method.single_addr_or_err().await); + let swap_contract_address = try_tx_s!(args.swap_contract_address.try_to_address()); let function_name = get_function_name("senderRefund", args.watcher_reward); - let refund_func = try_tx_fus!(SWAP_CONTRACT.function(&function_name)); + let refund_func = try_tx_s!(SWAP_CONTRACT.function(&function_name)); let watcher_reward = args.watcher_reward; - let clone = self.clone(); - match self.coin_type { EthCoinType::Eth => { let function_name = get_function_name("ethPayment", watcher_reward); - let payment_func = try_tx_fus!(SWAP_CONTRACT.function(&function_name)); + let payment_func = try_tx_s!(SWAP_CONTRACT.function(&function_name)); - let decoded = try_tx_fus!(decode_contract_call(payment_func, &payment.data)); + let decoded = try_tx_s!(decode_contract_call(payment_func, &payment.data)); - let state_f = self.payment_status(swap_contract_address, decoded[0].clone()); - Box::new( - state_f - .map_err(TransactionErr::Plain) - .and_then(move |state| -> EthTxFut { - if state != U256::from(PaymentState::Sent as u8) { - return Box::new(futures01::future::err(TransactionErr::Plain(ERRL!( - "Payment {:?} state is not PAYMENT_STATE_SENT, got {}", - payment, - state - )))); - } + let state = try_tx_s!( + self.payment_status(swap_contract_address, decoded[0].clone()) + .compat() + .await + ); + if state != U256::from(PaymentState::Sent as u8) { + return Err(TransactionErr::Plain(ERRL!( + "Payment {:?} state is not PAYMENT_STATE_SENT, got {}", + payment, + state + ))); + } - let value = payment.value; - let data = if watcher_reward { - try_tx_fus!(refund_func.encode_input(&[ - decoded[0].clone(), - Token::Uint(value), - decoded[2].clone(), - Token::Address(Address::default()), - Token::Address(clone.my_address), - decoded[1].clone(), - decoded[4].clone(), - decoded[5].clone(), - decoded[6].clone(), - ])) - } else { - try_tx_fus!(refund_func.encode_input(&[ - decoded[0].clone(), - Token::Uint(value), - decoded[2].clone(), - Token::Address(Address::default()), - decoded[1].clone(), - ])) - }; + let value = payment.value; + let data = if watcher_reward { + try_tx_s!(refund_func.encode_input(&[ + decoded[0].clone(), + Token::Uint(value), + decoded[2].clone(), + Token::Address(Address::default()), + Token::Address(my_address), + decoded[1].clone(), + decoded[4].clone(), + decoded[5].clone(), + decoded[6].clone(), + ])) + } else { + try_tx_s!(refund_func.encode_input(&[ + decoded[0].clone(), + Token::Uint(value), + decoded[2].clone(), + Token::Address(Address::default()), + decoded[1].clone(), + ])) + }; - clone.sign_and_send_transaction( - 0.into(), - Action::Call(swap_contract_address), - data, - U256::from(ETH_GAS), - ) - }), - ) + self.sign_and_send_transaction(0.into(), Call(swap_contract_address), data, U256::from(ETH_GAS)) + .compat() + .await }, EthCoinType::Erc20 { platform: _, token_addr, } => { let function_name = get_function_name("erc20Payment", watcher_reward); - let payment_func = try_tx_fus!(SWAP_CONTRACT.function(&function_name)); + let payment_func = try_tx_s!(SWAP_CONTRACT.function(&function_name)); - let decoded = try_tx_fus!(decode_contract_call(payment_func, &payment.data)); - let state_f = self.payment_status(swap_contract_address, decoded[0].clone()); - Box::new( - state_f - .map_err(TransactionErr::Plain) - .and_then(move |state| -> EthTxFut { - if state != U256::from(PaymentState::Sent as u8) { - return Box::new(futures01::future::err(TransactionErr::Plain(ERRL!( - "Payment {:?} state is not PAYMENT_STATE_SENT, got {}", - payment, - state - )))); - } + let decoded = try_tx_s!(decode_contract_call(payment_func, &payment.data)); + let state = try_tx_s!( + self.payment_status(swap_contract_address, decoded[0].clone()) + .compat() + .await + ); + if state != U256::from(PaymentState::Sent as u8) { + return Err(TransactionErr::Plain(ERRL!( + "Payment {:?} state is not PAYMENT_STATE_SENT, got {}", + payment, + state + ))); + } - let data = if watcher_reward { - try_tx_fus!(refund_func.encode_input(&[ - decoded[0].clone(), - decoded[1].clone(), - decoded[4].clone(), - Token::Address(token_addr), - Token::Address(clone.my_address), - decoded[3].clone(), - decoded[6].clone(), - decoded[7].clone(), - decoded[8].clone(), - ])) - } else { - try_tx_fus!(refund_func.encode_input(&[ - decoded[0].clone(), - decoded[1].clone(), - decoded[4].clone(), - Token::Address(token_addr), - decoded[3].clone(), - ])) - }; + let data = if watcher_reward { + try_tx_s!(refund_func.encode_input(&[ + decoded[0].clone(), + decoded[1].clone(), + decoded[4].clone(), + Token::Address(token_addr), + Token::Address(my_address), + decoded[3].clone(), + decoded[6].clone(), + decoded[7].clone(), + decoded[8].clone(), + ])) + } else { + try_tx_s!(refund_func.encode_input(&[ + decoded[0].clone(), + decoded[1].clone(), + decoded[4].clone(), + Token::Address(token_addr), + decoded[3].clone(), + ])) + }; - clone.sign_and_send_transaction( - 0.into(), - Action::Call(swap_contract_address), - data, - U256::from(ETH_GAS), - ) - }), - ) + self.sign_and_send_transaction(0.into(), Call(swap_contract_address), data, U256::from(ETH_GAS)) + .compat() + .await }, EthCoinType::Nft { .. } => { - return Box::new(futures01::future::err(TransactionErr::ProtocolNotSupported(ERRL!( + return Err(TransactionErr::ProtocolNotSupported(ERRL!( "Nft Protocol is not supported yet!" - )))) + ))) }, } } @@ -4148,7 +4050,7 @@ impl EthCoin { let function = ERC20_CONTRACT.function("balanceOf")?; let data = function.encode_input(&[Token::Address(address)])?; - let res = coin.call_request(*token_addr, None, Some(data.into())).await?; + let res = coin.call_request(address, *token_addr, None, Some(data.into())).await?; let decoded = function.decode_output(&res.0)?; match decoded[0] { Token::Uint(number) => Ok(number), @@ -4166,14 +4068,26 @@ impl EthCoin { Box::new(fut.boxed().compat()) } - fn my_balance(&self) -> BalanceFut { self.address_balance(self.my_address) } + fn get_balance(&self) -> BalanceFut { + let coin = self.clone(); + let fut = async move { + let my_address = coin.derivation_method.single_addr_or_err().await?; + coin.address_balance(my_address).compat().await + }; + Box::new(fut.boxed().compat()) + } - pub async fn get_tokens_balance_list(&self) -> Result, MmError> { + pub async fn get_tokens_balance_list_for_address( + &self, + address: Address, + ) -> Result> { let coin = || self; let mut requests = Vec::new(); for (token_ticker, info) in self.get_erc_tokens_infos() { let fut = async move { - let balance_as_u256 = coin().get_token_balance_by_address(info.token_address).await?; + let balance_as_u256 = coin() + .get_token_balance_for_address(address, info.token_address) + .await?; let balance_as_big_decimal = u256_to_big_decimal(balance_as_u256, info.decimals)?; let balance = CoinBalance::new(balance_as_big_decimal); Ok((token_ticker, balance)) @@ -4184,11 +4098,21 @@ impl EthCoin { try_join_all(requests).await.map(|res| res.into_iter().collect()) } - async fn get_token_balance_by_address(&self, token_address: Address) -> Result> { - let coin = self.clone(); + pub async fn get_tokens_balance_list(&self) -> Result> { + let my_address = self.derivation_method.single_addr_or_err().await?; + self.get_tokens_balance_list_for_address(my_address).await + } + + async fn get_token_balance_for_address( + &self, + address: Address, + token_address: Address, + ) -> Result> { let function = ERC20_CONTRACT.function("balanceOf")?; - let data = function.encode_input(&[Token::Address(coin.my_address)])?; - let res = coin.call_request(token_address, None, Some(data.into())).await?; + let data = function.encode_input(&[Token::Address(address)])?; + let res = self + .call_request(address, token_address, None, Some(data.into())) + .await?; let decoded = function.decode_output(&res.0)?; match decoded[0] { @@ -4200,14 +4124,22 @@ impl EthCoin { } } + async fn get_token_balance(&self, token_address: Address) -> Result> { + let my_address = self.derivation_method.single_addr_or_err().await?; + self.get_token_balance_for_address(my_address, token_address).await + } + async fn erc1155_balance(&self, token_addr: Address, token_id: &str) -> MmResult { let wallet_amount_uint = match self.coin_type { EthCoinType::Eth | EthCoinType::Nft { .. } => { let function = ERC1155_CONTRACT.function("balanceOf")?; let token_id_u256 = U256::from_dec_str(token_id).map_to_mm(|e| NumConversError::new(format!("{:?}", e)))?; - let data = function.encode_input(&[Token::Address(self.my_address), Token::Uint(token_id_u256)])?; - let result = self.call_request(token_addr, None, Some(data.into())).await?; + let my_address = self.derivation_method.single_addr_or_err().await?; + let data = function.encode_input(&[Token::Address(my_address), Token::Uint(token_id_u256)])?; + let result = self + .call_request(my_address, token_addr, None, Some(data.into())) + .await?; let decoded = function.decode_output(&result.0)?; match decoded[0] { Token::Uint(number) => number, @@ -4234,7 +4166,10 @@ impl EthCoin { let token_id_u256 = U256::from_dec_str(token_id).map_to_mm(|e| NumConversError::new(format!("{:?}", e)))?; let data = function.encode_input(&[Token::Uint(token_id_u256)])?; - let result = self.call_request(token_addr, None, Some(data.into())).await?; + let my_address = self.derivation_method.single_addr_or_err().await?; + let result = self + .call_request(my_address, token_addr, None, Some(data.into())) + .await?; let decoded = function.decode_output(&result.0)?; match decoded[0] { Token::Address(owner) => owner, @@ -4274,12 +4209,14 @@ impl EthCoin { /// because [`CallRequest::from`] is set to [`EthCoinImpl::my_address`]. fn estimate_gas_for_contract_call(&self, contract_addr: Address, call_data: Bytes) -> Web3RpcFut { let coin = self.clone(); - Box::new(coin.get_gas_price().and_then(move |gas_price| { + let fut = async move { + let my_address = coin.derivation_method.single_addr_or_err().await?; + let gas_price = coin.get_gas_price().compat().await?; let eth_value = U256::zero(); let estimate_gas_req = CallRequest { value: Some(eth_value), data: Some(call_data), - from: Some(coin.my_address), + from: Some(my_address), to: Some(contract_addr), gas: None, // gas price must be supplied because some smart contracts base their @@ -4288,25 +4225,33 @@ impl EthCoin { ..CallRequest::default() }; coin.estimate_gas_wrapper(estimate_gas_req) - .map_to_mm_fut(Web3RpcError::from) - })) + .compat() + .await + .map_to_mm(Web3RpcError::from) + }; + Box::new(fut.boxed().compat()) } fn eth_balance(&self) -> BalanceFut { let coin = self.clone(); - - let fut = async move { coin.balance(coin.my_address, Some(BlockNumber::Latest)).await }; - Box::new(fut.boxed().compat().map_to_mm_fut(BalanceError::from)) + let fut = async move { + let my_address = coin.derivation_method.single_addr_or_err().await?; + coin.balance(my_address, Some(BlockNumber::Latest)) + .await + .map_to_mm(BalanceError::from) + }; + Box::new(fut.boxed().compat()) } pub(crate) async fn call_request( &self, + from: Address, to: Address, value: Option, data: Option, ) -> Result { let request = CallRequest { - from: Some(self.my_address), + from: Some(from), to: Some(to), gas: None, gas_price: None, @@ -4327,9 +4272,12 @@ impl EthCoin { )), EthCoinType::Erc20 { ref token_addr, .. } => { let function = ERC20_CONTRACT.function("allowance")?; - let data = function.encode_input(&[Token::Address(coin.my_address), Token::Address(spender)])?; + let my_address = coin.derivation_method.single_addr_or_err().await?; + let data = function.encode_input(&[Token::Address(my_address), Token::Address(spender)])?; - let res = coin.call_request(*token_addr, None, Some(data.into())).await?; + let res = coin + .call_request(my_address, *token_addr, None, Some(data.into())) + .await?; let decoded = function.decode_output(&res.0)?; match decoded[0] { @@ -4401,7 +4349,7 @@ impl EthCoin { .await ); - coin.sign_and_send_transaction(0.into(), Action::Call(token_addr), data, gas_limit) + coin.sign_and_send_transaction(0.into(), Call(token_addr), data, gas_limit) .compat() .await }; @@ -4500,6 +4448,7 @@ impl EthCoin { ))); } + let my_address = selfi.derivation_method.single_addr_or_err().await?; match &selfi.coin_type { EthCoinType::Eth => { let mut expected_value = trade_amount; @@ -4526,11 +4475,11 @@ impl EthCoin { ))); } - if decoded[1] != Token::Address(selfi.my_address) { + if decoded[1] != Token::Address(my_address) { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "Payment tx receiver arg {:?} is invalid, expected {:?}", decoded[1], - Token::Address(selfi.my_address) + Token::Address(my_address) ))); } @@ -4630,11 +4579,11 @@ impl EthCoin { ))); } - if decoded[3] != Token::Address(selfi.my_address) { + if decoded[3] != Token::Address(my_address) { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "Payment tx receiver arg {:?} is invalid, expected {:?}", decoded[3], - Token::Address(selfi.my_address), + Token::Address(my_address), ))); } @@ -4746,9 +4695,18 @@ impl EthCoin { let data = try_fus!(function.encode_input(&[token])); let coin = self.clone(); - let fut = async move { coin.call_request(swap_contract_address, None, Some(data.into())).await }; + let fut = async move { + let my_address = coin + .derivation_method + .single_addr_or_err() + .await + .map_err(|e| ERRL!("{}", e))?; + coin.call_request(my_address, swap_contract_address, None, Some(data.into())) + .await + .map_err(|e| ERRL!("{}", e)) + }; - Box::new(fut.boxed().compat().map_err(|e| ERRL!("{}", e)).and_then(move |bytes| { + Box::new(fut.boxed().compat().and_then(move |bytes| { let decoded_tokens = try_s!(function.decode_output(&bytes.0)); let state = decoded_tokens .get(2) @@ -4932,7 +4890,7 @@ impl EthCoin { Ok((new_nonce, _)) if new_nonce > prev_nonce => Ready(()), Ok((_nonce, _)) => Retry(()), Err(e) => { - error!("Error getting {} {} nonce: {}", self.ticker(), self.my_address, e); + error!("Error getting {} {} nonce: {}", self.ticker(), addr, e); Retry(()) }, } @@ -5282,7 +5240,7 @@ impl MmCoin for EthCoin { // estimate gas for the `approve` contract call // Pass a dummy spender. Let's use `my_address`. - let spender = self.my_address; + let spender = self.derivation_method.single_addr_or_err().await?; let approve_function = ERC20_CONTRACT.function("approve")?; let approve_data = approve_function.encode_input(&[Token::Address(spender), Token::Uint(value)])?; let approve_gas_limit = self @@ -5355,12 +5313,13 @@ impl MmCoin for EthCoin { EthCoinType::Nft { .. } => return MmError::err(TradePreimageError::NftProtocolNotSupported), }; + let my_address = self.derivation_method.single_addr_or_err().await?; let gas_price = self.get_gas_price().compat().await?; let gas_price = increase_gas_price_by_stage(gas_price, &stage); let estimate_gas_req = CallRequest { value: Some(eth_value), data: Some(data.clone().into()), - from: Some(self.my_address), + from: Some(my_address), to: Some(*call_addr), gas: None, // gas price must be supplied because some smart contracts base their @@ -5692,7 +5651,7 @@ fn signed_tx_from_web3_tx(transaction: Web3Transaction) -> Result Action::Call(addr), + Some(addr) => Call(addr), None => Action::Create, }, }, @@ -5806,8 +5765,9 @@ fn rpc_event_handlers_for_eth_transport(ctx: &MmArc, ticker: String) -> Vec Arc> { Arc::new(AsyncMutex::new(())) } +fn new_nonce_lock() -> HashMap>> { HashMap::new() } +/// Activate eth coin or erc20 token from coin config and private key build policy pub async fn eth_coin_from_conf_and_request( ctx: &MmArc, ticker: &str, @@ -5838,12 +5798,12 @@ pub async fn eth_coin_from_conf_and_request( } let contract_supports_watchers = req["contract_supports_watchers"].as_bool().unwrap_or_default(); - let path_to_address = try_s!(json::from_value::>( + let path_to_address = try_s!(json::from_value::>( req["path_to_address"].clone() )) .unwrap_or_default(); - let (my_address, key_pair) = - try_s!(build_address_and_priv_key_policy(conf, priv_key_policy, &path_to_address).await); + let (key_pair, derivation_method) = + try_s!(build_address_and_priv_key_policy(ctx, ticker, conf, priv_key_policy, &path_to_address, None).await); let mut web3_instances = vec![]; let event_handlers = rpc_event_handlers_for_eth_transport(ctx, ticker.to_string()); @@ -5946,6 +5906,12 @@ pub async fn eth_coin_from_conf_and_request( let sign_message_prefix: Option = json::from_value(conf["sign_message_prefix"].clone()).unwrap_or(None); + let chain_id = try_s!(conf["chain_id"] + .as_u64() + .ok_or_else(|| format!("chain_id is not set for {}", ticker))); + + let trezor_coin: Option = json::from_value(conf["trezor_coin"].clone()).unwrap_or(None); + let initial_history_state = if req["tx_history"].as_bool().unwrap_or(false) { HistorySyncState::NotStarted } else { @@ -5961,9 +5927,11 @@ pub async fn eth_coin_from_conf_and_request( EthCoinType::Erc20 { platform, .. } | EthCoinType::Nft { platform } => String::from(platform), }; - let nonce_lock = { + let address_nonce_locks = { let mut map = NONCE_LOCK.lock().unwrap(); - map.entry(key_lock).or_insert_with(new_nonce_lock).clone() + Arc::new(AsyncMutex::new( + map.entry(key_lock).or_insert_with(new_nonce_lock).clone(), + )) }; // Create an abortable system linked to the `MmCtx` so if the context is stopped via `MmArc::stop`, @@ -5972,7 +5940,7 @@ pub async fn eth_coin_from_conf_and_request( let coin = EthCoinImpl { priv_key_policy: key_pair, - my_address, + derivation_method: Arc::new(derivation_method), coin_type, sign_message_prefix, swap_contract_address, @@ -5987,9 +5955,10 @@ pub async fn eth_coin_from_conf_and_request( history_sync_state: Mutex::new(initial_history_state), ctx: ctx.weak(), required_confirmations, - chain_id: conf["chain_id"].as_u64(), + chain_id, + trezor_coin, logs_block_range: conf["logs_block_range"].as_u64().unwrap_or(DEFAULT_LOGS_BLOCK_RANGE), - nonce_lock, + address_nonce_locks, erc20_tokens_infos: Default::default(), nfts_infos: Default::default(), abortable_system, @@ -6040,6 +6009,10 @@ pub fn eth_addr_to_hex(address: &Address) -> String { format!("{:#02x}", address /// The input must be 0x prefixed hex string fn is_valid_checksum_addr(addr: &str) -> bool { addr == checksum_address(addr) } +/// `display_eth_address` converts Address to mixed-case checksum form. +#[inline] +pub fn display_eth_address(addr: &Address) -> String { checksum_address(ð_addr_to_hex(addr)) } + fn increase_by_percent_one_gwei(num: U256, percent: u64) -> U256 { let one_gwei = U256::from(10u64.pow(9)); let percent = (num / U256::from(100)) * U256::from(percent); @@ -6071,13 +6044,13 @@ fn increase_gas_price_by_stage(gas_price: U256, level: &FeeApproxStage) -> U256 /// Represents errors that can occur while retrieving an Ethereum address. #[derive(Clone, Debug, Deserialize, Display, PartialEq, Serialize)] pub enum GetEthAddressError { - PrivKeyPolicyNotAllowed(PrivKeyPolicyNotAllowed), + UnexpectedDerivationMethod(UnexpectedDerivationMethod), EthActivationV2Error(EthActivationV2Error), Internal(String), } -impl From for GetEthAddressError { - fn from(e: PrivKeyPolicyNotAllowed) -> Self { GetEthAddressError::PrivKeyPolicyNotAllowed(e) } +impl From for GetEthAddressError { + fn from(e: UnexpectedDerivationMethod) -> Self { GetEthAddressError::UnexpectedDerivationMethod(e) } } impl From for GetEthAddressError { @@ -6088,24 +6061,37 @@ impl From for GetEthAddressError { fn from(e: CryptoCtxError) -> Self { GetEthAddressError::Internal(e.to_string()) } } +// Todo: `get_eth_address` should be removed since NFT is now part of the coins ctx. /// `get_eth_address` returns wallet address for coin with `ETH` protocol type. /// Note: result address has mixed-case checksum form. pub async fn get_eth_address( ctx: &MmArc, conf: &Json, ticker: &str, - path_to_address: &StandardHDCoinAddress, + path_to_address: &HDPathAccountToAddressId, ) -> MmResult { - let priv_key_policy = PrivKeyBuildPolicy::detect_priv_key_policy(ctx)?; - // Convert `PrivKeyBuildPolicy` to `EthPrivKeyBuildPolicy` if it's possible. - let priv_key_policy = EthPrivKeyBuildPolicy::try_from(priv_key_policy)?; + let crypto_ctx = CryptoCtx::from_ctx(ctx)?; + let priv_key_policy = if crypto_ctx.hw_ctx().is_some() { + PrivKeyBuildPolicy::Trezor + } else { + PrivKeyBuildPolicy::detect_priv_key_policy(ctx)? + } + .into(); - let (my_address, ..) = build_address_and_priv_key_policy(conf, priv_key_policy, path_to_address).await?; - let wallet_address = checksum_address(&format!("{:#02x}", my_address)); + let (_, derivation_method) = + build_address_and_priv_key_policy(ctx, ticker, conf, priv_key_policy, path_to_address, None).await?; + let my_address = match derivation_method { + EthDerivationMethod::SingleAddress(my_address) => my_address, + EthDerivationMethod::HDWallet(_) => { + return Err(MmError::new(GetEthAddressError::UnexpectedDerivationMethod( + UnexpectedDerivationMethod::UnsupportedError("HDWallet is not supported for NFT yet!".to_owned()), + ))); + }, + }; Ok(MyWalletAddress { coin: ticker.to_owned(), - wallet_address, + wallet_address: display_eth_address(&my_address), }) } @@ -6172,6 +6158,7 @@ async fn get_eth_gas_details( fee: Option, eth_value: U256, data: Bytes, + sender_address: Address, call_addr: Address, fungible_max: bool, ) -> MmResult { @@ -6195,7 +6182,7 @@ async fn get_eth_gas_details( let estimate_gas_req = CallRequest { value: Some(eth_value_for_estimate), data: Some(data), - from: Some(eth_coin.my_address), + from: Some(sender_address), to: Some(call_addr), gas: None, // gas price must be supplied because some smart contracts base their @@ -6257,6 +6244,7 @@ impl From for EthNftAssocTypesError { fn from(e: ParseContractTypeError) -> Self { EthNftAssocTypesError::ParseContractTypeError(e) } } +#[async_trait] impl ParseCoinAssocTypes for EthCoin { type Address = Address; type AddressParseError = MmError; @@ -6269,7 +6257,17 @@ impl ParseCoinAssocTypes for EthCoin { type Sig = Signature; type SigParseError = MmError; - fn my_addr(&self) -> &Self::Address { &self.my_address } + async fn my_addr(&self) -> Self::Address { + match self.derivation_method() { + DerivationMethod::SingleAddress(addr) => *addr, + // Todo: Expect should not fail but we need to handle it properly + DerivationMethod::HDWallet(hd_wallet) => hd_wallet + .get_enabled_address() + .await + .expect("Getting enabled address should not fail!") + .address(), + } + } fn parse_address(&self, address: &str) -> Result { Address::from_str(address).map_to_mm(|e| EthAssocTypesError::InvalidHexString(e.to_string())) @@ -6373,3 +6371,114 @@ impl MakerNftSwapOpsV2 for EthCoin { todo!() } } + +impl CoinWithPrivKeyPolicy for EthCoin { + type KeyPair = KeyPair; + + fn priv_key_policy(&self) -> &PrivKeyPolicy { &self.priv_key_policy } +} + +impl CoinWithDerivationMethod for EthCoin { + fn derivation_method(&self) -> &DerivationMethod, Self::HDWallet> { &self.derivation_method } +} + +#[async_trait] +impl IguanaBalanceOps for EthCoin { + type BalanceObject = CoinBalanceMap; + + async fn iguana_balances(&self) -> BalanceResult { + let platform_balance = self.my_balance().compat().await?; + let token_balances = self.get_tokens_balance_list().await?; + let mut balances = CoinBalanceMap::new(); + balances.insert(self.ticker().to_string(), platform_balance); + balances.extend(token_balances.into_iter()); + Ok(balances) + } +} + +#[async_trait] +impl GetNewAddressRpcOps for EthCoin { + type BalanceObject = CoinBalanceMap; + async fn get_new_address_rpc_without_conf( + &self, + params: GetNewAddressParams, + ) -> MmResult, GetNewAddressRpcError> { + get_new_address::common_impl::get_new_address_rpc_without_conf(self, params).await + } + + async fn get_new_address_rpc( + &self, + params: GetNewAddressParams, + confirm_address: &ConfirmAddress, + ) -> MmResult, GetNewAddressRpcError> + where + ConfirmAddress: HDConfirmAddress, + { + get_new_address::common_impl::get_new_address_rpc(self, params, confirm_address).await + } +} + +#[async_trait] +impl AccountBalanceRpcOps for EthCoin { + type BalanceObject = CoinBalanceMap; + + async fn account_balance_rpc( + &self, + params: AccountBalanceParams, + ) -> MmResult, HDAccountBalanceRpcError> { + account_balance::common_impl::account_balance_rpc(self, params).await + } +} + +#[async_trait] +impl InitAccountBalanceRpcOps for EthCoin { + type BalanceObject = CoinBalanceMap; + + async fn init_account_balance_rpc( + &self, + params: InitAccountBalanceParams, + ) -> MmResult, HDAccountBalanceRpcError> { + init_account_balance::common_impl::init_account_balance_rpc(self, params).await + } +} + +#[async_trait] +impl InitScanAddressesRpcOps for EthCoin { + type BalanceObject = CoinBalanceMap; + + async fn init_scan_for_new_addresses_rpc( + &self, + params: ScanAddressesParams, + ) -> MmResult, HDAccountBalanceRpcError> { + init_scan_for_new_addresses::common_impl::scan_for_new_addresses_rpc(self, params).await + } +} + +#[async_trait] +impl InitCreateAccountRpcOps for EthCoin { + type BalanceObject = CoinBalanceMap; + + async fn init_create_account_rpc( + &self, + params: CreateNewAccountParams, + state: CreateAccountState, + xpub_extractor: Option, + ) -> MmResult, CreateAccountRpcError> + where + XPubExtractor: HDXPubExtractor + Send, + { + init_create_account::common_impl::init_create_new_account_rpc(self, params, state, xpub_extractor).await + } + + async fn revert_creating_account(&self, account_id: u32) { + init_create_account::common_impl::revert_creating_account(self, account_id).await + } +} + +/// Converts and extended public key derived using BIP32 to an Ethereum public key. +pub fn pubkey_from_extended(extended_pubkey: &Secp256k1ExtendedPublicKey) -> Public { + let serialized = extended_pubkey.public_key().serialize_uncompressed(); + let mut pubkey_uncompressed = Public::default(); + pubkey_uncompressed.as_mut().copy_from_slice(&serialized[1..]); + pubkey_uncompressed +} diff --git a/mm2src/coins/eth/eth_balance_events.rs b/mm2src/coins/eth/eth_balance_events.rs index b6c1441d73..231aa68507 100644 --- a/mm2src/coins/eth/eth_balance_events.rs +++ b/mm2src/coins/eth/eth_balance_events.rs @@ -1,6 +1,7 @@ use async_trait::async_trait; use common::{executor::{AbortSettings, SpawnAbortable, Timer}, log, Future01CompatExt}; +use ethereum_types::Address; use futures::{channel::oneshot::{self, Receiver, Sender}, stream::FuturesUnordered, StreamExt}; @@ -8,22 +9,33 @@ use instant::Instant; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::MmError; use mm2_event_stream::{behaviour::{EventBehaviour, EventInitStatus}, - Event, EventStreamConfiguration}; + ErrorEventName, Event, EventName, EventStreamConfiguration}; use mm2_number::BigDecimal; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use super::EthCoin; use crate::{eth::{u256_to_big_decimal, Erc20TokenInfo}, - BalanceError, MmCoin}; + BalanceError, CoinWithDerivationMethod, MmCoin}; + +struct BalanceData { + ticker: String, + address: String, + balance: BigDecimal, +} + +struct BalanceFetchError { + ticker: String, + address: String, + error: MmError, +} + +type BalanceResult = Result; /// This implementation differs from others, as they immediately return /// an error if any of the requests fails. This one completes all futures /// and returns their results individually. -async fn get_all_balance_results_concurrently( - coin: &EthCoin, -) -> Vec)>> { +async fn get_all_balance_results_concurrently(coin: &EthCoin, addresses: HashSet
) -> Vec { let mut tokens = coin.get_erc_tokens_infos(); - // Workaround for performance purposes. // // Unlike tokens, the platform coin length is constant (=1). Instead of creating a generic @@ -31,80 +43,148 @@ async fn get_all_balance_results_concurrently( // the platform coin to Erc20TokenInfo so that we can use the token list right away without // additional mapping. tokens.insert(coin.ticker.clone(), Erc20TokenInfo { - token_address: coin.my_address, + // This is a dummy value, since there is no token address for the platform coin. + // In the fetch_balance function, we check if the token_ticker is equal to this + // coin's ticker to avoid using token_address to fetch the balance + // and to use address_balance instead. + token_address: Address::default(), decimals: coin.decimals, }); + drop_mutability!(tokens); - let jobs = tokens - .into_iter() - .map(|(token_ticker, info)| async move { fetch_balance(coin, token_ticker, &info).await }) - .collect::>(); + let mut all_jobs = FuturesUnordered::new(); - jobs.collect().await + for address in addresses { + let jobs = tokens.iter().map(|(token_ticker, info)| { + let coin = coin.clone(); + let token_ticker = token_ticker.clone(); + let info = info.clone(); + async move { fetch_balance(&coin, address, token_ticker, &info).await } + }); + + all_jobs.extend(jobs); + } + + all_jobs.collect().await } async fn fetch_balance( coin: &EthCoin, + address: Address, token_ticker: String, info: &Erc20TokenInfo, -) -> Result<(String, BigDecimal), (String, MmError)> { +) -> Result { let (balance_as_u256, decimals) = if token_ticker == coin.ticker { ( - coin.address_balance(coin.my_address) + coin.address_balance(address) .compat() .await - .map_err(|e| (token_ticker.clone(), e))?, + .map_err(|error| BalanceFetchError { + ticker: token_ticker.clone(), + address: address.to_string(), + error, + })?, coin.decimals, ) } else { ( - coin.get_token_balance_by_address(info.token_address) + coin.get_token_balance(info.token_address) .await - .map_err(|e| (token_ticker.clone(), e))?, + .map_err(|error| BalanceFetchError { + ticker: token_ticker.clone(), + address: address.to_string(), + error, + })?, info.decimals, ) }; - let balance_as_big_decimal = - u256_to_big_decimal(balance_as_u256, decimals).map_err(|e| (token_ticker.clone(), e.into()))?; - - Ok((token_ticker, balance_as_big_decimal)) + let balance_as_big_decimal = u256_to_big_decimal(balance_as_u256, decimals).map_err(|e| BalanceFetchError { + ticker: token_ticker.clone(), + address: address.to_string(), + error: e.into(), + })?; + + Ok(BalanceData { + ticker: token_ticker, + address: address.to_string(), + balance: balance_as_big_decimal, + }) } #[async_trait] impl EventBehaviour for EthCoin { - const EVENT_NAME: &'static str = "COIN_BALANCE"; - const ERROR_EVENT_NAME: &'static str = "COIN_BALANCE_ERROR"; + fn event_name() -> EventName { EventName::CoinBalance } + + fn error_event_name() -> ErrorEventName { ErrorEventName::CoinBalanceError } async fn handle(self, interval: f64, tx: oneshot::Sender) { const RECEIVER_DROPPED_MSG: &str = "Receiver is dropped, which should never happen."; async fn start_polling(coin: EthCoin, ctx: MmArc, interval: f64) { - let mut cache: HashMap = HashMap::new(); + async fn sleep_remaining_time(interval: f64, now: Instant) { + // If the interval is x seconds, + // our goal is to broadcast changed balances every x seconds. + // To achieve this, we need to subtract the time complexity of each iteration. + // Given that an iteration already takes 80% of the interval, + // this will lead to inconsistency in the events. + let remaining_time = interval - now.elapsed().as_secs_f64(); + // Not worth to make a call for less than `0.1` durations + if remaining_time >= 0.1 { + Timer::sleep(remaining_time).await; + } + } + + let mut cache: HashMap> = HashMap::new(); loop { let now = Instant::now(); + let addresses = match coin.all_addresses().await { + Ok(addresses) => addresses, + Err(e) => { + log::error!("Failed getting addresses for {}. Error: {}", coin.ticker, e); + let e = serde_json::to_value(e).expect("Serialization shouldn't fail."); + ctx.stream_channel_controller + .broadcast(Event::new( + format!("{}:{}", EthCoin::error_event_name(), coin.ticker), + e.to_string(), + )) + .await; + sleep_remaining_time(interval, now).await; + continue; + }, + }; + let mut balance_updates = vec![]; - for result in get_all_balance_results_concurrently(&coin).await { + for result in get_all_balance_results_concurrently(&coin, addresses).await { match result { - Ok((ticker, balance)) => { - if Some(&balance) == cache.get(&ticker) { + Ok(res) => { + if Some(&res.balance) == cache.get(&res.ticker).and_then(|map| map.get(&res.address)) { continue; } balance_updates.push(json!({ - "ticker": ticker, - "balance": { "spendable": balance, "unspendable": BigDecimal::default() } + "ticker": res.ticker, + "address": res.address, + "balance": { "spendable": res.balance, "unspendable": BigDecimal::default() } })); - cache.insert(ticker.to_owned(), balance); + cache + .entry(res.ticker.clone()) + .or_insert_with(HashMap::new) + .insert(res.address, res.balance); }, - Err((ticker, e)) => { - log::error!("Failed getting balance for '{ticker}' with {interval} interval. Error: {e}"); - let e = serde_json::to_value(e).expect("Serialization should't fail."); + Err(err) => { + log::error!( + "Failed getting balance for '{}:{}' with {interval} interval. Error: {}", + err.ticker, + err.address, + err.error + ); + let e = serde_json::to_value(err.error).expect("Serialization shouldn't fail."); ctx.stream_channel_controller .broadcast(Event::new( - format!("{}:{}", EthCoin::ERROR_EVENT_NAME, ticker), + format!("{}:{}:{}", EthCoin::error_event_name(), err.ticker, err.address), e.to_string(), )) .await; @@ -115,21 +195,13 @@ impl EventBehaviour for EthCoin { if !balance_updates.is_empty() { ctx.stream_channel_controller .broadcast(Event::new( - EthCoin::EVENT_NAME.to_string(), + EthCoin::event_name().to_string(), json!(balance_updates).to_string(), )) .await; } - // If the interval is x seconds, our goal is to broadcast changed balances every x seconds. - // To achieve this, we need to subtract the time complexity of each iteration. - // Given that an iteration already takes 80% of the interval, this will lead to inconsistency - // in the events. - let remaining_time = interval - now.elapsed().as_secs_f64(); - // Not worth to make a call for less than `0.1` durations - if remaining_time >= 0.1 { - Timer::sleep(remaining_time).await; - } + sleep_remaining_time(interval, now).await; } } @@ -149,13 +221,13 @@ impl EventBehaviour for EthCoin { } async fn spawn_if_active(self, config: &EventStreamConfiguration) -> EventInitStatus { - if let Some(event) = config.get_event(Self::EVENT_NAME) { - log::info!("{} event is activated for {}", Self::EVENT_NAME, self.ticker,); + if let Some(event) = config.get_event(&Self::event_name()) { + log::info!("{} event is activated for {}", Self::event_name(), self.ticker,); let (tx, rx): (Sender, Receiver) = oneshot::channel(); let fut = self.clone().handle(event.stream_interval_seconds, tx); let settings = - AbortSettings::info_on_abort(format!("{} event is stopped for {}.", Self::EVENT_NAME, self.ticker)); + AbortSettings::info_on_abort(format!("{} event is stopped for {}.", Self::event_name(), self.ticker)); self.spawner().spawn_with_settings(fut, settings); rx.await.unwrap_or_else(|e| { diff --git a/mm2src/coins/eth/eth_hd_wallet.rs b/mm2src/coins/eth/eth_hd_wallet.rs new file mode 100644 index 0000000000..60e3942dc6 --- /dev/null +++ b/mm2src/coins/eth/eth_hd_wallet.rs @@ -0,0 +1,180 @@ +use super::*; +use crate::coin_balance::HDAddressBalanceScanner; +use crate::hd_wallet::{ExtractExtendedPubkey, HDAccount, HDAddress, HDExtractPubkeyError, HDWallet, HDXPubExtractor, + TrezorCoinError}; +use async_trait::async_trait; +use bip32::DerivationPath; +use crypto::Secp256k1ExtendedPublicKey; +use ethereum_types::{Address, Public}; + +pub type EthHDAddress = HDAddress; +pub type EthHDAccount = HDAccount; +pub type EthHDWallet = HDWallet; + +#[async_trait] +impl ExtractExtendedPubkey for EthCoin { + type ExtendedPublicKey = Secp256k1ExtendedPublicKey; + + async fn extract_extended_pubkey( + &self, + xpub_extractor: Option, + derivation_path: DerivationPath, + ) -> MmResult + where + XPubExtractor: HDXPubExtractor + Send, + { + extract_extended_pubkey_impl(self, xpub_extractor, derivation_path).await + } +} + +#[async_trait] +impl HDWalletCoinOps for EthCoin { + type HDWallet = EthHDWallet; + + fn address_formatter(&self) -> fn(&HDCoinAddress) -> String { display_eth_address } + + fn address_from_extended_pubkey( + &self, + extended_pubkey: &Secp256k1ExtendedPublicKey, + derivation_path: DerivationPath, + ) -> HDCoinHDAddress { + let pubkey = pubkey_from_extended(extended_pubkey); + let address = public_to_address(&pubkey); + EthHDAddress { + address, + pubkey, + derivation_path, + } + } + + fn trezor_coin(&self) -> MmResult { + self.trezor_coin.clone().or_mm_err(|| { + let ticker = self.ticker(); + let error = format!("'{ticker}' coin has 'trezor_coin' field as `None` in the coins config"); + TrezorCoinError::Internal(error) + }) + } +} + +impl HDCoinWithdrawOps for EthCoin {} + +#[async_trait] +#[cfg_attr(test, mockable)] +impl HDAddressBalanceScanner for EthCoin { + type Address = Address; + + async fn is_address_used(&self, address: &Self::Address) -> BalanceResult { + // Count calculates the number of transactions sent from the address whether it's for ERC20 or ETH. + // If the count is greater than 0, then the address is used. + // If the count is 0, then we check for the balance of the address to make sure there was no received transactions. + let count = self.transaction_count(*address, None).await?; + if count > U256::zero() { + return Ok(true); + } + + // We check for platform balance only first to reduce the number of requests to the node. + // If this is a token added using init_token, then we check for this token balance only, and + // we don't check for platform balance or other tokens that was added before. + let platform_balance = self.address_balance(*address).compat().await?; + if !platform_balance.is_zero() { + return Ok(true); + } + + // This is done concurrently which increases the cost of the requests to the node. but it's better than doing it sequentially to reduce the time. + let token_balance_map = self.get_tokens_balance_list_for_address(*address).await?; + Ok(token_balance_map.values().any(|balance| !balance.get_total().is_zero())) + } +} + +#[async_trait] +impl HDWalletBalanceOps for EthCoin { + type HDAddressScanner = Self; + type BalanceObject = CoinBalanceMap; + + async fn produce_hd_address_scanner(&self) -> BalanceResult { Ok(self.clone()) } + + async fn enable_hd_wallet( + &self, + hd_wallet: &Self::HDWallet, + xpub_extractor: Option, + params: EnabledCoinBalanceParams, + path_to_address: &HDPathAccountToAddressId, + ) -> MmResult, EnableCoinBalanceError> + where + XPubExtractor: HDXPubExtractor + Send, + { + coin_balance::common_impl::enable_hd_wallet(self, hd_wallet, xpub_extractor, params, path_to_address).await + } + + async fn scan_for_new_addresses( + &self, + hd_wallet: &Self::HDWallet, + hd_account: &mut HDCoinHDAccount, + address_scanner: &Self::HDAddressScanner, + gap_limit: u32, + ) -> BalanceResult>> { + scan_for_new_addresses_impl( + self, + hd_wallet, + hd_account, + address_scanner, + Bip44Chain::External, + gap_limit, + ) + .await + } + + async fn all_known_addresses_balances( + &self, + hd_account: &HDCoinHDAccount, + ) -> BalanceResult>> { + let external_addresses = hd_account + .known_addresses_number(Bip44Chain::External) + // A UTXO coin should support both [`Bip44Chain::External`] and [`Bip44Chain::Internal`]. + .mm_err(|e| BalanceError::Internal(e.to_string()))?; + + self.known_addresses_balances_with_ids(hd_account, Bip44Chain::External, 0..external_addresses) + .await + } + + async fn known_address_balance(&self, address: &HDBalanceAddress) -> BalanceResult { + let balance = self + .address_balance(*address) + .and_then(move |result| Ok(u256_to_big_decimal(result, self.decimals())?)) + .compat() + .await?; + + let coin_balance = CoinBalance { + spendable: balance, + unspendable: BigDecimal::from(0), + }; + + let mut balances = CoinBalanceMap::new(); + balances.insert(self.ticker().to_string(), coin_balance); + let token_balances = self.get_tokens_balance_list_for_address(*address).await?; + balances.extend(token_balances); + Ok(balances) + } + + async fn known_addresses_balances( + &self, + addresses: Vec>, + ) -> BalanceResult, Self::BalanceObject)>> { + let mut balance_futs = Vec::new(); + for address in addresses { + let fut = async move { + let balance = self.known_address_balance(&address).await?; + Ok((address, balance)) + }; + balance_futs.push(fut); + } + try_join_all(balance_futs).await + } + + async fn prepare_addresses_for_balance_stream_if_enabled( + &self, + _addresses: HashSet, + ) -> MmResult<(), String> { + Ok(()) + } +} diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index 9fc7d903e0..961f9a6e54 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -1,10 +1,11 @@ use super::*; +use crate::eth::for_tests::{eth_coin_for_test, eth_coin_from_keypair}; use crate::{DexFee, IguanaPrivKey}; use common::{block_on, now_sec}; #[cfg(not(target_arch = "wasm32"))] use ethkey::{Generator, Random}; -use mm2_core::mm_ctx::{MmArc, MmCtxBuilder}; -use mm2_test_helpers::for_tests::{eth_testnet_conf, ETH_MAINNET_NODE, ETH_SEPOLIA_NODE, ETH_SEPOLIA_SWAP_CONTRACT, +use mm2_core::mm_ctx::MmCtxBuilder; +use mm2_test_helpers::for_tests::{ETH_MAINNET_CHAIN_ID, ETH_MAINNET_NODE, ETH_SEPOLIA_CHAIN_ID, ETH_SEPOLIA_NODES, ETH_SEPOLIA_TOKEN_CONTRACT}; use mocktopus::mocking::*; @@ -24,74 +25,6 @@ fn check_sum(addr: &str, expected: &str) { assert_eq!(expected, actual); } -fn eth_coin_for_test( - coin_type: EthCoinType, - urls: &[&str], - fallback_swap_contract: Option
, -) -> (MmArc, EthCoin) { - let key_pair = KeyPair::from_secret_slice( - &hex::decode("0dbc09312ec67cf775c00e72dd88c9a7c4b7452d4ee84ee7ca0bb55c4be35446").unwrap(), - ) - .unwrap(); - eth_coin_from_keypair(coin_type, urls, fallback_swap_contract, key_pair) -} - -fn eth_coin_from_keypair( - coin_type: EthCoinType, - urls: &[&str], - fallback_swap_contract: Option
, - key_pair: KeyPair, -) -> (MmArc, EthCoin) { - let mut web3_instances = vec![]; - for url in urls.iter() { - let node = HttpTransportNode { - uri: url.parse().unwrap(), - gui_auth: false, - }; - - let transport = Web3Transport::new_http(node); - let web3 = Web3::new(transport); - - web3_instances.push(Web3Instance { web3, is_parity: false }); - } - - drop_mutability!(web3_instances); - - let conf = json!({ "coins": [eth_testnet_conf()] }); - let ctx = MmCtxBuilder::new().with_conf(conf).into_mm_arc(); - let ticker = match coin_type { - EthCoinType::Eth => "ETH".to_string(), - EthCoinType::Erc20 { .. } => "JST".to_string(), - EthCoinType::Nft { ref platform } => platform.to_string(), - }; - - let eth_coin = EthCoin(Arc::new(EthCoinImpl { - coin_type, - decimals: 18, - gas_station_url: None, - gas_station_decimals: ETH_GAS_STATION_DECIMALS, - history_sync_state: Mutex::new(HistorySyncState::NotEnabled), - gas_station_policy: GasStationPricePolicy::MeanAverageFast, - my_address: key_pair.address(), - sign_message_prefix: Some(String::from("Ethereum Signed Message:\n")), - priv_key_policy: key_pair.into(), - swap_contract_address: Address::from_str(ETH_SEPOLIA_SWAP_CONTRACT).unwrap(), - fallback_swap_contract, - contract_supports_watchers: false, - ticker, - web3_instances: AsyncMutex::new(web3_instances), - ctx: ctx.weak(), - required_confirmations: 1.into(), - chain_id: None, - logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, - nonce_lock: new_nonce_lock(), - erc20_tokens_infos: Default::default(), - nfts_infos: Default::default(), - abortable_system: AbortableQueue::default(), - })); - (ctx, eth_coin) -} - #[test] /// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md#test-cases fn test_check_sum_address() { @@ -225,7 +158,13 @@ fn test_wait_for_payment_spend_timeout() { EthCoin::current_block.mock_safe(|_| MockResult::Return(Box::new(futures01::future::ok(900)))); let key_pair = Random.generate().unwrap(); - let (_ctx, coin) = eth_coin_from_keypair(EthCoinType::Eth, ETH_SEPOLIA_NODE, None, key_pair); + let (_ctx, coin) = eth_coin_from_keypair( + EthCoinType::Eth, + ETH_SEPOLIA_NODES, + None, + key_pair, + ETH_SEPOLIA_CHAIN_ID, + ); let wait_until = now_sec() - 1; let from_block = 1; @@ -293,9 +232,9 @@ fn test_gas_station() { #[cfg(not(target_arch = "wasm32"))] #[test] fn test_withdraw_impl_manual_fee() { - let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, &["http://dummy.dummy"], None); + let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, &["http://dummy.dummy"], None, ETH_SEPOLIA_CHAIN_ID); - EthCoin::my_balance.mock_safe(|_| { + EthCoin::address_balance.mock_safe(|_, _| { let balance = wei_from_big_decimal(&1000000000.into(), 18).unwrap(); MockResult::Return(Box::new(futures01::future::ok(balance))) }); @@ -313,7 +252,7 @@ fn test_withdraw_impl_manual_fee() { }), memo: None, }; - coin.my_balance().wait().unwrap(); + coin.get_balance().wait().unwrap(); let tx_details = block_on(withdraw_impl(coin, withdraw_req)).unwrap(); let expected = Some( @@ -338,9 +277,10 @@ fn test_withdraw_impl_fee_details() { }, &["http://dummy.dummy"], None, + ETH_SEPOLIA_CHAIN_ID, ); - EthCoin::my_balance.mock_safe(|_| { + EthCoin::address_balance.mock_safe(|_, _| { let balance = wei_from_big_decimal(&1000000000.into(), 18).unwrap(); MockResult::Return(Box::new(futures01::future::ok(balance))) }); @@ -358,7 +298,7 @@ fn test_withdraw_impl_fee_details() { }), memo: None, }; - coin.my_balance().wait().unwrap(); + coin.get_balance().wait().unwrap(); let tx_details = block_on(withdraw_impl(coin, withdraw_req)).unwrap(); let expected = Some( @@ -405,7 +345,7 @@ fn get_sender_trade_preimage() { EthCoin::get_gas_price.mock_safe(|_| MockResult::Return(Box::new(futures01::future::ok(GAS_PRICE.into())))); - let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, &["http://dummy.dummy"], None); + let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, &["http://dummy.dummy"], None, ETH_SEPOLIA_CHAIN_ID); let actual = block_on(coin.get_sender_trade_fee( TradePreimageValue::UpperBound(150.into()), @@ -465,6 +405,7 @@ fn get_erc20_sender_trade_preimage() { }, &["http://dummy.dummy"], None, + ETH_SEPOLIA_CHAIN_ID, ); // value is allowed @@ -521,7 +462,7 @@ fn get_erc20_sender_trade_preimage() { fn get_receiver_trade_preimage() { EthCoin::get_gas_price.mock_safe(|_| MockResult::Return(Box::new(futures01::future::ok(GAS_PRICE.into())))); - let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, &["http://dummy.dummy"], None); + let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, &["http://dummy.dummy"], None, ETH_SEPOLIA_CHAIN_ID); let amount = u256_to_big_decimal((ETH_GAS * GAS_PRICE).into(), 18).expect("!u256_to_big_decimal"); let expected_fee = TradeFee { coin: "ETH".to_owned(), @@ -555,7 +496,7 @@ fn test_get_fee_to_send_taker_fee() { let dex_fee_amount = u256_to_big_decimal(DEX_FEE_AMOUNT.into(), 18).expect("!u256_to_big_decimal"); - let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, &["http://dummy.dummy"], None); + let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, &["http://dummy.dummy"], None, ETH_SEPOLIA_CHAIN_ID); let actual = block_on(coin.get_fee_to_send_taker_fee( DexFee::Standard(MmNumber::from(dex_fee_amount.clone())), FeeApproxStage::WithoutApprox, @@ -570,6 +511,7 @@ fn test_get_fee_to_send_taker_fee() { }, &["http://dummy.dummy"], None, + ETH_SEPOLIA_CHAIN_ID, ); let actual = block_on(coin.get_fee_to_send_taker_fee( DexFee::Standard(MmNumber::from(dex_fee_amount)), @@ -598,6 +540,7 @@ fn test_get_fee_to_send_taker_fee_insufficient_balance() { }, &[ETH_MAINNET_NODE], None, + ETH_MAINNET_CHAIN_ID, ); let dex_fee_amount = u256_to_big_decimal(DEX_FEE_AMOUNT.into(), 18).expect("!u256_to_big_decimal"); @@ -615,7 +558,7 @@ fn test_get_fee_to_send_taker_fee_insufficient_balance() { #[test] fn validate_dex_fee_invalid_sender_eth() { - let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, &[ETH_MAINNET_NODE], None); + let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, &[ETH_MAINNET_NODE], None, ETH_MAINNET_CHAIN_ID); // the real dex fee sent on mainnet // https://etherscan.io/tx/0x7e9ca16c85efd04ee5e31f2c1914b48f5606d6f9ce96ecce8c96d47d6857278f let tx = block_on(block_on(coin.web3()).unwrap().eth().transaction(TransactionId::Hash( @@ -649,6 +592,7 @@ fn validate_dex_fee_invalid_sender_erc() { }, &[ETH_MAINNET_NODE], None, + ETH_MAINNET_CHAIN_ID, ); // the real dex fee sent on mainnet // https://etherscan.io/tx/0xd6403b41c79f9c9e9c83c03d920ee1735e7854d85d94cef48d95dfeca95cd600 @@ -685,7 +629,7 @@ fn sender_compressed_pub(tx: &SignedEthTx) -> [u8; 33] { #[test] fn validate_dex_fee_eth_confirmed_before_min_block() { - let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, &[ETH_MAINNET_NODE], None); + let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, &[ETH_MAINNET_NODE], None, ETH_MAINNET_CHAIN_ID); // the real dex fee sent on mainnet // https://etherscan.io/tx/0x7e9ca16c85efd04ee5e31f2c1914b48f5606d6f9ce96ecce8c96d47d6857278f let tx = block_on(block_on(coin.web3()).unwrap().eth().transaction(TransactionId::Hash( @@ -721,6 +665,7 @@ fn validate_dex_fee_erc_confirmed_before_min_block() { }, &[ETH_MAINNET_NODE], None, + ETH_MAINNET_CHAIN_ID, ); // the real dex fee sent on mainnet // https://etherscan.io/tx/0xd6403b41c79f9c9e9c83c03d920ee1735e7854d85d94cef48d95dfeca95cd600 @@ -751,7 +696,7 @@ fn validate_dex_fee_erc_confirmed_before_min_block() { #[test] fn test_negotiate_swap_contract_addr_no_fallback() { - let (_, coin) = eth_coin_for_test(EthCoinType::Eth, &[ETH_MAINNET_NODE], None); + let (_, coin) = eth_coin_for_test(EthCoinType::Eth, &[ETH_MAINNET_NODE], None, ETH_MAINNET_CHAIN_ID); let input = None; let error = coin.negotiate_swap_contract_addr(input).unwrap_err().into_inner(); @@ -780,7 +725,12 @@ fn test_negotiate_swap_contract_addr_no_fallback() { fn test_negotiate_swap_contract_addr_has_fallback() { let fallback = Address::from_str("0x8500AFc0bc5214728082163326C2FF0C73f4a871").unwrap(); - let (_, coin) = eth_coin_for_test(EthCoinType::Eth, &[ETH_MAINNET_NODE], Some(fallback)); + let (_, coin) = eth_coin_for_test( + EthCoinType::Eth, + &[ETH_MAINNET_NODE], + Some(fallback), + ETH_MAINNET_CHAIN_ID, + ); let input = None; let result = coin.negotiate_swap_contract_addr(input).unwrap(); @@ -845,7 +795,7 @@ fn polygon_check_if_my_payment_sent() { )) .unwrap(); - log!("{:02x}", coin.my_address); + log!("{}", coin.my_address().unwrap()); let secret_hash = hex::decode("fc33114b389f0ee1212abf2867e99e89126f4860").unwrap(); let swap_contract_address = "9130b257d37a52e52f21054c4da3450c72f595ce".into(); @@ -871,7 +821,13 @@ fn polygon_check_if_my_payment_sent() { #[test] fn test_message_hash() { let key_pair = Random.generate().unwrap(); - let (_ctx, coin) = eth_coin_from_keypair(EthCoinType::Eth, ETH_SEPOLIA_NODE, None, key_pair); + let (_ctx, coin) = eth_coin_from_keypair( + EthCoinType::Eth, + ETH_SEPOLIA_NODES, + None, + key_pair, + ETH_SEPOLIA_CHAIN_ID, + ); let message_hash = coin.sign_message_hash("test").unwrap(); assert_eq!( @@ -886,7 +842,13 @@ fn test_sign_verify_message() { &hex::decode("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f").unwrap(), ) .unwrap(); - let (_ctx, coin) = eth_coin_from_keypair(EthCoinType::Eth, ETH_SEPOLIA_NODE, None, key_pair); + let (_ctx, coin) = eth_coin_from_keypair( + EthCoinType::Eth, + ETH_SEPOLIA_NODES, + None, + key_pair, + ETH_SEPOLIA_CHAIN_ID, + ); let message = "test"; let signature = coin.sign_message(message).unwrap(); @@ -905,7 +867,7 @@ fn test_eth_extract_secret() { platform: "ETH".to_string(), token_addr: Address::from_str("0xc0eb7aed740e1796992a08962c15661bdeb58003").unwrap(), }; - let (_ctx, coin) = eth_coin_from_keypair(coin_type, &["http://dummy.dummy"], None, key_pair); + let (_ctx, coin) = eth_coin_from_keypair(coin_type, &["http://dummy.dummy"], None, key_pair, ETH_SEPOLIA_CHAIN_ID); // raw transaction bytes of https://ropsten.etherscan.io/tx/0xcb7c14d3ff309996d582400369393b6fa42314c52245115d4a3f77f072c36da9 let tx_bytes = &[ diff --git a/mm2src/coins/eth/eth_wasm_tests.rs b/mm2src/coins/eth/eth_wasm_tests.rs index 32d412aadc..24838ab9e0 100644 --- a/mm2src/coins/eth/eth_wasm_tests.rs +++ b/mm2src/coins/eth/eth_wasm_tests.rs @@ -2,7 +2,7 @@ use super::*; use crate::lp_coininit; use crypto::CryptoCtx; use mm2_core::mm_ctx::MmCtxBuilder; -use mm2_test_helpers::for_tests::{ETH_SEPOLIA_NODE, ETH_SEPOLIA_SWAP_CONTRACT}; +use mm2_test_helpers::for_tests::{ETH_SEPOLIA_NODES, ETH_SEPOLIA_SWAP_CONTRACT}; use wasm_bindgen_test::*; use web_sys::console; @@ -20,6 +20,7 @@ async fn init_eth_coin_helper() -> Result<(MmArc, MmCoinEnum), String> { "coin": "ETH", "name": "ethereum", "fname": "Ethereum", + "chain_id": 1337, "protocol":{ "type": "ETH" }, @@ -36,7 +37,7 @@ async fn init_eth_coin_helper() -> Result<(MmArc, MmCoinEnum), String> { .unwrap(); let req = json!({ - "urls":ETH_SEPOLIA_NODE, + "urls":ETH_SEPOLIA_NODES, "swap_contract_address":ETH_SEPOLIA_SWAP_CONTRACT }); Ok((ctx.clone(), lp_coininit(&ctx, "ETH", &req).await?)) diff --git a/mm2src/coins/eth/eth_withdraw.rs b/mm2src/coins/eth/eth_withdraw.rs new file mode 100644 index 0000000000..d059065985 --- /dev/null +++ b/mm2src/coins/eth/eth_withdraw.rs @@ -0,0 +1,480 @@ +use super::{checksum_address, get_eth_gas_details, u256_to_big_decimal, wei_from_big_decimal, EthCoinType, + EthDerivationMethod, EthPrivKeyPolicy, Public, WithdrawError, WithdrawRequest, WithdrawResult, + ERC20_CONTRACT, H160, H256}; +use crate::eth::{Action, Address, EthTxFeeDetails, KeyPair, SignedEthTx, UnSignedEthTx}; +use crate::hd_wallet::{HDCoinWithdrawOps, HDWalletOps, WithdrawFrom, WithdrawSenderAddress}; +use crate::rpc_command::init_withdraw::{WithdrawInProgressStatus, WithdrawTaskHandleShared}; +use crate::{BytesJson, CoinWithDerivationMethod, EthCoin, GetWithdrawSenderAddress, PrivKeyPolicy, TransactionDetails}; +use async_trait::async_trait; +use bip32::DerivationPath; +use common::custom_futures::timeout::FutureTimerExt; +use common::now_sec; +use crypto::hw_rpc_task::HwRpcTaskAwaitingStatus; +use crypto::trezor::trezor_rpc_task::{TrezorRequestStatuses, TrezorRpcTaskProcessor}; +use crypto::{CryptoCtx, HwRpcError}; +use ethabi::Token; +use futures::compat::Future01CompatExt; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::map_mm_error::MapMmError; +use mm2_err_handle::mm_error::MmResult; +use mm2_err_handle::prelude::{MapToMmResult, MmError, OrMmError}; +use std::ops::Deref; +use std::sync::Arc; +#[cfg(target_arch = "wasm32")] +use web3::types::TransactionRequest; + +/// `EthWithdraw` trait provides methods for withdrawing Ethereum and ERC20 tokens. +/// This allows different implementations of withdrawal logic for different types of wallets. +#[async_trait] +pub trait EthWithdraw +where + Self: Sized + Sync, +{ + /// A getter for the coin that implements this trait. + fn coin(&self) -> &EthCoin; + + /// A getter for the withdrawal request. + fn request(&self) -> &WithdrawRequest; + + /// Executes the logic that should be performed just before generating a transaction. + #[allow(clippy::result_large_err)] + fn on_generating_transaction(&self) -> Result<(), MmError>; + + /// Executes the logic that should be performed just before finishing the withdrawal. + #[allow(clippy::result_large_err)] + fn on_finishing(&self) -> Result<(), MmError>; + + /// Signs the transaction with a Trezor hardware wallet. + async fn sign_tx_with_trezor( + &self, + derivation_path: &DerivationPath, + unsigned_tx: &UnSignedEthTx, + ) -> Result>; + + /// Transforms the `from` parameter of the withdrawal request into an address. + async fn get_from_address(&self, req: &WithdrawRequest) -> Result> { + let coin = self.coin(); + match req.from { + Some(_) => Ok(coin.get_withdraw_sender_address(req).await?.address), + None => Ok(coin.derivation_method.single_addr_or_err().await?), + } + } + + /// Gets the key pair for the address from which the withdrawal is made. + #[allow(clippy::result_large_err)] + fn get_key_pair(&self, req: &WithdrawRequest) -> Result> { + let coin = self.coin(); + if coin.priv_key_policy.is_trezor() { + return MmError::err(WithdrawError::InternalError("no keypair for hw wallet".to_owned())); + } + + match req.from { + Some(ref from) => { + let derivation_path = self.get_from_derivation_path(from)?; + let raw_priv_key = coin + .priv_key_policy + .hd_wallet_derived_priv_key_or_err(&derivation_path)?; + KeyPair::from_secret_slice(raw_priv_key.as_slice()) + .map_to_mm(|e| WithdrawError::InternalError(e.to_string())) + }, + None => coin + .priv_key_policy + .activated_key_or_err() + .mm_err(|e| WithdrawError::InternalError(e.to_string())) + .cloned(), + } + } + + /// Gets the derivation path for the address from which the withdrawal is made using the `from` parameter. + #[allow(clippy::result_large_err)] + fn get_from_derivation_path(&self, from: &WithdrawFrom) -> Result> { + let coin = self.coin(); + let path_to_coin = &coin.deref().derivation_method.hd_wallet_or_err()?.derivation_path; + let path_to_address = from.to_address_path(path_to_coin.coin_type())?; + let derivation_path = path_to_address.to_derivation_path(path_to_coin)?; + Ok(derivation_path) + } + + /// Gets the derivation path for the address from which the withdrawal is made using the withdrawal request. + async fn get_withdraw_derivation_path( + &self, + req: &WithdrawRequest, + ) -> Result> { + let coin = self.coin(); + match req.from { + Some(ref from) => self.get_from_derivation_path(from), + None => { + let default_hd_address = &coin + .deref() + .derivation_method + .hd_wallet_or_err()? + .get_enabled_address() + .await + .ok_or_else(|| WithdrawError::InternalError("no enabled address".to_owned()))?; + Ok(default_hd_address.derivation_path.clone()) + }, + } + } + + /// Signs the transaction and returns the transaction hash and the signed transaction. + async fn sign_withdraw_tx( + &self, + req: &WithdrawRequest, + unsigned_tx: UnSignedEthTx, + ) -> Result<(H256, BytesJson), MmError> { + let coin = self.coin(); + match coin.priv_key_policy { + EthPrivKeyPolicy::Iguana(_) | EthPrivKeyPolicy::HDWallet { .. } => { + let key_pair = self.get_key_pair(req)?; + let signed = unsigned_tx.sign(key_pair.secret(), Some(coin.chain_id)); + let bytes = rlp::encode(&signed); + + Ok((signed.hash, BytesJson::from(bytes.to_vec()))) + }, + EthPrivKeyPolicy::Trezor => { + let derivation_path = self.get_withdraw_derivation_path(req).await?; + let signed = self.sign_tx_with_trezor(&derivation_path, &unsigned_tx).await?; + let bytes = rlp::encode(&signed); + Ok((signed.hash, BytesJson::from(bytes.to_vec()))) + }, + #[cfg(target_arch = "wasm32")] + EthPrivKeyPolicy::Metamask(_) => MmError::err(WithdrawError::InternalError("invalid policy".to_owned())), + } + } + + /// Sends the transaction and returns the transaction hash and the signed transaction. + /// This method should only be used when withdrawing using an external wallet like MetaMask. + #[cfg(target_arch = "wasm32")] + async fn send_withdraw_tx( + &self, + req: &WithdrawRequest, + tx_to_send: TransactionRequest, + ) -> Result<(H256, BytesJson), MmError> { + let coin = self.coin(); + match coin.priv_key_policy { + EthPrivKeyPolicy::Metamask(_) => { + if !req.broadcast { + let error = + "Set 'broadcast' to generate, sign and broadcast a transaction with MetaMask".to_string(); + return MmError::err(WithdrawError::BroadcastExpected(error)); + } + + // Wait for 10 seconds for the transaction to appear on the RPC node. + let wait_rpc_timeout = 10_000; + let check_every = 1.; + + // Please note that this method may take a long time + // due to `wallet_switchEthereumChain` and `eth_sendTransaction` requests. + let tx_hash = coin.send_transaction(tx_to_send).await?; + + let signed_tx = coin + .wait_for_tx_appears_on_rpc(tx_hash, wait_rpc_timeout, check_every) + .await?; + let tx_hex = signed_tx + .map(|signed_tx| BytesJson::from(rlp::encode(&signed_tx).to_vec())) + // Return an empty `tx_hex` if the transaction is still not appeared on the RPC node. + .unwrap_or_default(); + Ok((tx_hash, tx_hex)) + }, + EthPrivKeyPolicy::Iguana(_) | EthPrivKeyPolicy::HDWallet { .. } | EthPrivKeyPolicy::Trezor => { + MmError::err(WithdrawError::InternalError("invalid policy".to_owned())) + }, + } + } + + /// Builds the withdrawal transaction and returns the transaction details. + async fn build(self) -> WithdrawResult { + let coin = self.coin(); + let ticker = coin.deref().ticker.clone(); + let req = self.request().clone(); + + let to_addr = coin + .address_from_str(&req.to) + .map_to_mm(WithdrawError::InvalidAddress)?; + let my_address = self.get_from_address(&req).await?; + + self.on_generating_transaction()?; + + let my_balance = coin.address_balance(my_address).compat().await?; + let my_balance_dec = u256_to_big_decimal(my_balance, coin.decimals)?; + + let (mut wei_amount, dec_amount) = if req.max { + (my_balance, my_balance_dec.clone()) + } else { + let wei_amount = wei_from_big_decimal(&req.amount, coin.decimals)?; + (wei_amount, req.amount.clone()) + }; + if wei_amount > my_balance { + return MmError::err(WithdrawError::NotSufficientBalance { + coin: coin.ticker.clone(), + available: my_balance_dec.clone(), + required: dec_amount, + }); + }; + let (mut eth_value, data, call_addr, fee_coin) = match &coin.coin_type { + EthCoinType::Eth => (wei_amount, vec![], to_addr, ticker.as_str()), + EthCoinType::Erc20 { platform, token_addr } => { + let function = ERC20_CONTRACT.function("transfer")?; + let data = function.encode_input(&[Token::Address(to_addr), Token::Uint(wei_amount)])?; + (0.into(), data, *token_addr, platform.as_str()) + }, + EthCoinType::Nft { .. } => return MmError::err(WithdrawError::NftProtocolNotSupported), + }; + let eth_value_dec = u256_to_big_decimal(eth_value, coin.decimals)?; + + let (gas, gas_price) = get_eth_gas_details( + coin, + req.fee.clone(), + eth_value, + data.clone().into(), + my_address, + call_addr, + false, + ) + .await?; + let total_fee = gas * gas_price; + let total_fee_dec = u256_to_big_decimal(total_fee, coin.decimals)?; + + if req.max && coin.coin_type == EthCoinType::Eth { + if eth_value < total_fee || wei_amount < total_fee { + return MmError::err(WithdrawError::AmountTooLow { + amount: eth_value_dec, + threshold: total_fee_dec, + }); + } + eth_value -= total_fee; + wei_amount -= total_fee; + }; + drop_mutability!(eth_value); + drop_mutability!(wei_amount); + + let (tx_hash, tx_hex) = match coin.priv_key_policy { + EthPrivKeyPolicy::Iguana(_) | EthPrivKeyPolicy::HDWallet { .. } | EthPrivKeyPolicy::Trezor => { + let address_lock = coin.get_address_lock(my_address.to_string()).await; + let _nonce_lock = address_lock.lock().await; + let (nonce, _) = coin + .clone() + .get_addr_nonce(my_address) + .compat() + .timeout_secs(30.) + .await? + .map_to_mm(WithdrawError::Transport)?; + + let unsigned_tx = UnSignedEthTx { + nonce, + value: eth_value, + action: Action::Call(call_addr), + data: data.clone(), + gas, + gas_price, + }; + self.sign_withdraw_tx(&req, unsigned_tx).await? + }, + #[cfg(target_arch = "wasm32")] + EthPrivKeyPolicy::Metamask(_) => { + let tx_to_send = TransactionRequest { + from: my_address, + to: Some(to_addr), + gas: Some(gas), + gas_price: Some(gas_price), + value: Some(eth_value), + data: Some(data.into()), + nonce: None, + ..TransactionRequest::default() + }; + self.send_withdraw_tx(&req, tx_to_send).await? + }, + }; + + self.on_finishing()?; + let tx_hash_bytes = BytesJson::from(tx_hash.0.to_vec()); + let tx_hash_str = format!("{:02x}", tx_hash_bytes); + + let amount_decimal = u256_to_big_decimal(wei_amount, coin.decimals)?; + let mut spent_by_me = amount_decimal.clone(); + let received_by_me = if to_addr == my_address { + amount_decimal.clone() + } else { + 0.into() + }; + let fee_details = EthTxFeeDetails::new(gas, gas_price, fee_coin)?; + if coin.coin_type == EthCoinType::Eth { + spent_by_me += &fee_details.total_fee; + } + Ok(TransactionDetails { + to: vec![checksum_address(&format!("{:#02x}", to_addr))], + from: vec![checksum_address(&format!("{:#02x}", my_address))], + total_amount: amount_decimal, + my_balance_change: &received_by_me - &spent_by_me, + spent_by_me, + received_by_me, + tx_hex, + tx_hash: tx_hash_str, + block_height: 0, + fee_details: Some(fee_details.into()), + coin: coin.ticker.clone(), + internal_id: vec![].into(), + timestamp: now_sec(), + kmd_rewards: None, + transaction_type: Default::default(), + memo: None, + }) + } +} + +/// Eth withdraw version with user interaction support +pub struct InitEthWithdraw { + ctx: MmArc, + coin: EthCoin, + task_handle: WithdrawTaskHandleShared, + req: WithdrawRequest, +} + +#[async_trait] +impl EthWithdraw for InitEthWithdraw { + fn coin(&self) -> &EthCoin { &self.coin } + + fn request(&self) -> &WithdrawRequest { &self.req } + + fn on_generating_transaction(&self) -> Result<(), MmError> { + Ok(self + .task_handle + .update_in_progress_status(WithdrawInProgressStatus::GeneratingTransaction)?) + } + + fn on_finishing(&self) -> Result<(), MmError> { + Ok(self + .task_handle + .update_in_progress_status(WithdrawInProgressStatus::Finishing)?) + } + + async fn sign_tx_with_trezor( + &self, + derivation_path: &DerivationPath, + unsigned_tx: &UnSignedEthTx, + ) -> Result> { + let coin = self.coin(); + let crypto_ctx = CryptoCtx::from_ctx(&self.ctx)?; + let hw_ctx = crypto_ctx + .hw_ctx() + .or_mm_err(|| WithdrawError::HwError(HwRpcError::NoTrezorDeviceAvailable))?; + let trezor_statuses = TrezorRequestStatuses { + on_button_request: WithdrawInProgressStatus::FollowHwDeviceInstructions, + on_pin_request: HwRpcTaskAwaitingStatus::EnterTrezorPin, + on_passphrase_request: HwRpcTaskAwaitingStatus::EnterTrezorPassphrase, + on_ready: WithdrawInProgressStatus::FollowHwDeviceInstructions, + }; + let sign_processor = TrezorRpcTaskProcessor::new(self.task_handle.clone(), trezor_statuses); + let sign_processor = Arc::new(sign_processor); + let mut trezor_session = hw_ctx.trezor(sign_processor).await?; + let unverified_tx = trezor_session + .sign_eth_tx(derivation_path, unsigned_tx, coin.chain_id) + .await?; + Ok(SignedEthTx::new(unverified_tx).map_to_mm(|err| WithdrawError::InternalError(err.to_string()))?) + } +} + +#[allow(clippy::result_large_err)] +impl InitEthWithdraw { + pub fn new( + ctx: MmArc, + coin: EthCoin, + req: WithdrawRequest, + task_handle: WithdrawTaskHandleShared, + ) -> Result> { + Ok(InitEthWithdraw { + ctx, + coin, + task_handle, + req, + }) + } +} + +/// Simple eth withdraw version without user interaction support +pub struct StandardEthWithdraw { + coin: EthCoin, + req: WithdrawRequest, +} + +#[async_trait] +impl EthWithdraw for StandardEthWithdraw { + fn coin(&self) -> &EthCoin { &self.coin } + + fn request(&self) -> &WithdrawRequest { &self.req } + + fn on_generating_transaction(&self) -> Result<(), MmError> { Ok(()) } + + fn on_finishing(&self) -> Result<(), MmError> { Ok(()) } + + async fn sign_tx_with_trezor( + &self, + _derivation_path: &DerivationPath, + _unsigned_tx: &UnSignedEthTx, + ) -> Result> { + async { + Err(MmError::new(WithdrawError::UnsupportedError(String::from( + "Trezor not supported for legacy RPC", + )))) + } + .await + } +} + +#[allow(clippy::result_large_err)] +impl StandardEthWithdraw { + pub fn new(coin: EthCoin, req: WithdrawRequest) -> Result> { + Ok(StandardEthWithdraw { coin, req }) + } +} + +#[async_trait] +impl GetWithdrawSenderAddress for EthCoin { + type Address = Address; + type Pubkey = Public; + + async fn get_withdraw_sender_address( + &self, + req: &WithdrawRequest, + ) -> MmResult, WithdrawError> { + eth_get_withdraw_from_address(self, req).await + } +} + +async fn eth_get_withdraw_from_address( + coin: &EthCoin, + req: &WithdrawRequest, +) -> MmResult, WithdrawError> { + match coin.derivation_method() { + EthDerivationMethod::SingleAddress(my_address) => eth_get_withdraw_iguana_sender(coin, req, my_address), + EthDerivationMethod::HDWallet(hd_wallet) => { + let from = req.from.clone().or_mm_err(|| WithdrawError::FromAddressNotFound)?; + coin.get_withdraw_hd_sender(hd_wallet, &from) + .await + .mm_err(WithdrawError::from) + }, + } +} + +#[allow(clippy::result_large_err)] +fn eth_get_withdraw_iguana_sender( + coin: &EthCoin, + req: &WithdrawRequest, + my_address: &Address, +) -> MmResult, WithdrawError> { + if req.from.is_some() { + let error = "'from' is not supported if the coin is initialized with an Iguana private key"; + return MmError::err(WithdrawError::UnexpectedFromAddress(error.to_owned())); + } + + let pubkey = match coin.priv_key_policy { + PrivKeyPolicy::Iguana(ref key_pair) => key_pair.public(), + _ => return MmError::err(WithdrawError::InternalError("not iguana private key policy".to_owned())), + }; + + Ok(WithdrawSenderAddress { + address: *my_address, + pubkey: *pubkey, + derivation_path: None, + }) +} diff --git a/mm2src/coins/eth/for_tests.rs b/mm2src/coins/eth/for_tests.rs new file mode 100644 index 0000000000..11a29fc741 --- /dev/null +++ b/mm2src/coins/eth/for_tests.rs @@ -0,0 +1,79 @@ +#[cfg(not(target_arch = "wasm32"))] use super::*; +use mm2_core::mm_ctx::{MmArc, MmCtxBuilder}; +#[cfg(not(target_arch = "wasm32"))] +use mm2_test_helpers::for_tests::{eth_sepolia_conf, ETH_SEPOLIA_SWAP_CONTRACT}; + +lazy_static! { + static ref MM_CTX: MmArc = MmCtxBuilder::new().into_mm_arc(); +} + +#[cfg(not(target_arch = "wasm32"))] +pub(crate) fn eth_coin_for_test( + coin_type: EthCoinType, + urls: &[&str], + fallback_swap_contract: Option
, + chain_id: u64, +) -> (MmArc, EthCoin) { + let key_pair = KeyPair::from_secret_slice( + &hex::decode("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f").unwrap(), + ) + .unwrap(); + eth_coin_from_keypair(coin_type, urls, fallback_swap_contract, key_pair, chain_id) +} + +#[cfg(not(target_arch = "wasm32"))] +pub(crate) fn eth_coin_from_keypair( + coin_type: EthCoinType, + urls: &[&str], + fallback_swap_contract: Option
, + key_pair: KeyPair, + chain_id: u64, +) -> (MmArc, EthCoin) { + let mut web3_instances = vec![]; + for url in urls.iter() { + let node = HttpTransportNode { + uri: url.parse().unwrap(), + gui_auth: false, + }; + let transport = Web3Transport::new_http(node); + let web3 = Web3::new(transport); + web3_instances.push(Web3Instance { web3, is_parity: false }); + } + drop_mutability!(web3_instances); + + let conf = json!({ "coins": [eth_sepolia_conf()] }); + let ctx = MmCtxBuilder::new().with_conf(conf).into_mm_arc(); + let ticker = match coin_type { + EthCoinType::Eth => "ETH".to_string(), + EthCoinType::Erc20 { .. } => "JST".to_string(), + EthCoinType::Nft { ref platform } => platform.to_string(), + }; + let my_address = key_pair.address(); + + let eth_coin = EthCoin(Arc::new(EthCoinImpl { + coin_type, + decimals: 18, + gas_station_url: None, + gas_station_decimals: ETH_GAS_STATION_DECIMALS, + history_sync_state: Mutex::new(HistorySyncState::NotEnabled), + gas_station_policy: GasStationPricePolicy::MeanAverageFast, + sign_message_prefix: Some(String::from("Ethereum Signed Message:\n")), + priv_key_policy: key_pair.into(), + derivation_method: Arc::new(DerivationMethod::SingleAddress(my_address)), + swap_contract_address: Address::from_str(ETH_SEPOLIA_SWAP_CONTRACT).unwrap(), + fallback_swap_contract, + contract_supports_watchers: false, + ticker, + web3_instances: AsyncMutex::new(web3_instances), + ctx: ctx.weak(), + required_confirmations: 1.into(), + chain_id, + trezor_coin: None, + logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, + address_nonce_locks: Arc::new(AsyncMutex::new(new_nonce_lock())), + erc20_tokens_infos: Default::default(), + nfts_infos: Arc::new(Default::default()), + abortable_system: AbortableQueue::default(), + })); + (ctx, eth_coin) +} diff --git a/mm2src/coins/eth/nft_swap_v2/mod.rs b/mm2src/coins/eth/nft_swap_v2/mod.rs index 83488deada..67ec624ff0 100644 --- a/mm2src/coins/eth/nft_swap_v2/mod.rs +++ b/mm2src/coins/eth/nft_swap_v2/mod.rs @@ -34,7 +34,7 @@ impl EthCoin { match &self.coin_type { EthCoinType::Nft { .. } => { - let data = try_tx_s!(self.prepare_nft_maker_payment_v2_data(&args, htlc_data)); + let data = try_tx_s!(self.prepare_nft_maker_payment_v2_data(&args, htlc_data).await); self.sign_and_send_transaction( 0.into(), Action::Call(*args.nft_swap_info.token_address), @@ -174,7 +174,7 @@ impl EthCoin { todo!() } - fn prepare_nft_maker_payment_v2_data( + async fn prepare_nft_maker_payment_v2_data( &self, args: &SendNftMakerPaymentArgs<'_, Self>, htlc_data: Vec, @@ -185,7 +185,7 @@ impl EthCoin { let amount_u256 = U256::from_dec_str(&args.amount.to_string()) .map_err(|e| PrepareTxDataError::Internal(e.to_string()))?; let data = function.encode_input(&[ - Token::Address(*self.my_addr()), + Token::Address(self.my_addr().await), Token::Address(*args.nft_swap_info.swap_contract_address), Token::Uint(U256::from(args.nft_swap_info.token_id)), Token::Uint(amount_u256), @@ -196,7 +196,7 @@ impl EthCoin { ContractType::Erc721 => { let function = erc721_transfer_with_data()?; let data = function.encode_input(&[ - Token::Address(*self.my_addr()), + Token::Address(self.my_addr().await), Token::Address(*args.nft_swap_info.swap_contract_address), Token::Uint(U256::from(args.nft_swap_info.token_id)), Token::Bytes(htlc_data), @@ -236,7 +236,9 @@ impl EthCoin { let function_name = state_type.as_str(); let function = contract_abi.function(function_name)?; let data = function.encode_input(&[swap_id])?; - let bytes = self.call_request(swap_address, None, Some(data.into())).await?; + let bytes = self + .call_request(self.my_addr().await, swap_address, None, Some(data.into())) + .await?; let decoded_tokens = function.decode_output(&bytes.0)?; let state = decoded_tokens .get(2) diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index 1573a17cf7..5a79e19f9e 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -1,15 +1,18 @@ use super::*; +use crate::hd_wallet::{load_hd_accounts_from_storage, HDAccountsMutex, HDPathAccountToAddressId, HDWalletCoinStorage, + HDWalletStorageError, DEFAULT_GAP_LIMIT}; use crate::nft::get_nfts_for_activation; use crate::nft::nft_errors::{GetNftInfoError, ParseChainTypeError}; use crate::nft::nft_structs::Chain; #[cfg(target_arch = "wasm32")] use crate::EthMetamaskPolicy; use common::executor::AbortedError; -use crypto::{CryptoCtxError, StandardHDCoinAddress}; +use crypto::{trezor::TrezorError, Bip32Error, CryptoCtxError, HwError}; use enum_derives::EnumFromTrait; use instant::Instant; use mm2_err_handle::common_errors::WithInternal; #[cfg(target_arch = "wasm32")] use mm2_metamask::{from_metamask_error, MetamaskError, MetamaskRpcError, WithMetamaskRpcError}; +use rpc_task::RpcTaskError; use std::sync::atomic::Ordering; use url::Url; use web3_transport::websocket_transport::WebsocketTransport; @@ -20,9 +23,9 @@ pub enum EthActivationV2Error { InvalidPayload(String), InvalidSwapContractAddr(String), InvalidFallbackSwapContract(String), - #[display(fmt = "Expected either 'chain_id' or 'rpc_chain_id' to be set")] - #[cfg(target_arch = "wasm32")] - ExpectedRpcChainId, + InvalidPathToAddress(String), + #[display(fmt = "`chain_id` should be set for evm coins or tokens")] + ChainIdNotSet, #[display(fmt = "Platform coin {} activation failed. {}", ticker, error)] ActivationFailed { ticker: String, @@ -37,6 +40,7 @@ pub enum EthActivationV2Error { PrivKeyPolicyNotAllowed(PrivKeyPolicyNotAllowed), #[display(fmt = "Failed spawning balance events. Error: {_0}")] FailedSpawningBalanceEvents(String), + HDWalletStorageError(String), #[cfg(target_arch = "wasm32")] #[from_trait(WithMetamaskRpcError::metamask_rpc_error)] #[display(fmt = "{}", _0)] @@ -45,6 +49,16 @@ pub enum EthActivationV2Error { #[display(fmt = "Internal: {}", _0)] InternalError(String), Transport(String), + UnexpectedDerivationMethod(UnexpectedDerivationMethod), + CoinDoesntSupportTrezor, + HwContextNotInitialized, + #[display(fmt = "Initialization task has timed out {:?}", duration)] + TaskTimedOut { + duration: Duration, + }, + HwError(HwRpcError), + #[display(fmt = "Hardware wallet must be called within rpc task framework")] + InvalidHardwareWalletCall, } impl From for EthActivationV2Error { @@ -72,6 +86,34 @@ impl From for EthActivationV2Error { EthTokenActivationError::Transport(err) | EthTokenActivationError::ClientConnectionFailed(err) => { EthActivationV2Error::Transport(err) }, + EthTokenActivationError::UnexpectedDerivationMethod(err) => { + EthActivationV2Error::UnexpectedDerivationMethod(err) + }, + } + } +} + +impl From for EthActivationV2Error { + fn from(e: HDWalletStorageError) -> Self { EthActivationV2Error::HDWalletStorageError(e.to_string()) } +} + +impl From for EthActivationV2Error { + fn from(e: HwError) -> Self { EthActivationV2Error::InternalError(e.to_string()) } +} + +impl From for EthActivationV2Error { + fn from(e: Bip32Error) -> Self { EthActivationV2Error::InternalError(e.to_string()) } +} + +impl From for EthActivationV2Error { + fn from(e: TrezorError) -> Self { EthActivationV2Error::InternalError(e.to_string()) } +} + +impl From for EthActivationV2Error { + fn from(rpc_err: RpcTaskError) -> Self { + match rpc_err { + RpcTaskError::Timeout(duration) => EthActivationV2Error::TaskTimedOut { duration }, + internal_error => EthActivationV2Error::InternalError(internal_error.to_string()), } } } @@ -85,10 +127,25 @@ impl From for EthActivationV2Error { fn from(e: ParseChainTypeError) -> Self { EthActivationV2Error::InternalError(e.to_string()) } } +impl From for EthActivationV2Error { + fn from(e: EnableCoinBalanceError) -> Self { + match e { + EnableCoinBalanceError::NewAddressDerivingError(err) => { + EthActivationV2Error::InternalError(err.to_string()) + }, + EnableCoinBalanceError::NewAccountCreationError(err) => { + EthActivationV2Error::InternalError(err.to_string()) + }, + EnableCoinBalanceError::BalanceError(err) => EthActivationV2Error::CouldNotFetchBalance(err.to_string()), + } + } +} + /// An alternative to `crate::PrivKeyActivationPolicy`, typical only for ETH coin. #[derive(Clone, Deserialize)] pub enum EthPrivKeyActivationPolicy { ContextPrivKey, + Trezor, #[cfg(target_arch = "wasm32")] Metamask, } @@ -97,6 +154,10 @@ impl Default for EthPrivKeyActivationPolicy { fn default() -> Self { EthPrivKeyActivationPolicy::ContextPrivKey } } +impl EthPrivKeyActivationPolicy { + pub fn is_hw_policy(&self) -> bool { matches!(self, EthPrivKeyActivationPolicy::Trezor) } +} + #[derive(Clone, Debug, Deserialize, Serialize)] pub enum EthRpcMode { Default, @@ -126,8 +187,11 @@ pub struct EthActivationV2Request { pub required_confirmations: Option, #[serde(default)] pub priv_key_policy: EthPrivKeyActivationPolicy, + #[serde(flatten)] + pub enable_params: EnabledCoinBalanceParams, #[serde(default)] - pub path_to_address: StandardHDCoinAddress, + pub path_to_address: HDPathAccountToAddressId, + pub gap_limit: Option, } #[derive(Clone, Deserialize)] @@ -137,7 +201,7 @@ pub struct EthNode { pub gui_auth: bool, } -#[derive(Serialize, SerializeErrorType)] +#[derive(Display, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] pub enum EthTokenActivationError { InternalError(String), @@ -145,6 +209,7 @@ pub enum EthTokenActivationError { CouldNotFetchBalance(String), InvalidPayload(String), Transport(String), + UnexpectedDerivationMethod(UnexpectedDerivationMethod), } impl From for EthTokenActivationError { @@ -155,6 +220,10 @@ impl From for EthTokenActivationError { fn from(err: MyAddressError) -> Self { Self::InternalError(err.to_string()) } } +impl From for EthTokenActivationError { + fn from(e: UnexpectedDerivationMethod) -> Self { EthTokenActivationError::UnexpectedDerivationMethod(e) } +} + impl From for EthTokenActivationError { fn from(e: GetNftInfoError) -> Self { match e { @@ -201,6 +270,28 @@ pub struct Erc20TokenActivationRequest { pub required_confirmations: Option, } +/// Holds ERC-20 token-specific activation parameters when using the task manager for activation. +#[derive(Clone, Deserialize)] +pub struct InitErc20TokenActivationRequest { + /// The number of confirmations required for swap transactions. + pub required_confirmations: Option, + /// Parameters for HD wallet account and addresses initialization. + #[serde(flatten)] + pub enable_params: EnabledCoinBalanceParams, + /// This determines which Address of the HD account to be used for swaps for this Token. + /// If not specified, the first non-change address for the first account is used. + #[serde(default)] + pub path_to_address: HDPathAccountToAddressId, +} + +impl From for Erc20TokenActivationRequest { + fn from(req: InitErc20TokenActivationRequest) -> Self { + Erc20TokenActivationRequest { + required_confirmations: req.required_confirmations, + } + } +} + /// Encapsulates the request parameters for NFT activation, specifying the provider to be used. #[derive(Clone, Deserialize)] pub struct NftActivationRequest { @@ -221,11 +312,21 @@ pub enum EthTokenProtocol { } /// Details for an ERC-20 token protocol. +#[derive(Clone)] pub struct Erc20Protocol { pub platform: String, pub token_addr: Address, } +impl From for CoinProtocol { + fn from(erc20_protocol: Erc20Protocol) -> Self { + CoinProtocol::ERC20 { + platform: erc20_protocol.platform, + contract_address: erc20_protocol.token_addr.to_string(), + } + } +} + /// Details for an NFT protocol. #[derive(Debug)] pub struct NftProtocol { @@ -291,7 +392,10 @@ impl EthCoin { let token = EthCoinImpl { priv_key_policy: self.priv_key_policy.clone(), - my_address: self.my_address, + // We inherit the derivation method from the parent/platform coin + // If we want a new wallet for each token we can add this as an option in the future + // storage ticker will be the platform coin ticker + derivation_method: self.derivation_method.clone(), coin_type: EthCoinType::Erc20 { platform: protocol.platform, token_addr: protocol.token_addr, @@ -310,8 +414,9 @@ impl EthCoin { ctx: self.ctx.clone(), required_confirmations, chain_id: self.chain_id, + trezor_coin: self.trezor_coin.clone(), logs_block_range: self.logs_block_range, - nonce_lock: self.nonce_lock.clone(), + address_nonce_locks: self.address_nonce_locks.clone(), erc20_tokens_infos: Default::default(), nfts_infos: Default::default(), abortable_system, @@ -336,7 +441,9 @@ impl EthCoin { // all spawned futures related to global Non-Fungible Token will be aborted as well. let abortable_system = self.abortable_system.create_subsystem()?; - let nft_infos = get_nfts_for_activation(&chain, &self.my_address, url).await?; + // Todo: support HD wallet for NFTs, currently we get nfts for enabled address only and there might be some issues when activating NFTs while ETH is activated with HD wallet + let my_address = self.derivation_method.single_addr_or_err().await?; + let nft_infos = get_nfts_for_activation(&chain, &my_address, url).await?; let global_nft = EthCoinImpl { ticker, @@ -344,7 +451,7 @@ impl EthCoin { platform: self.ticker.clone(), }, priv_key_policy: self.priv_key_policy.clone(), - my_address: self.my_address, + derivation_method: self.derivation_method.clone(), sign_message_prefix: self.sign_message_prefix.clone(), swap_contract_address: self.swap_contract_address, fallback_swap_contract: self.fallback_swap_contract, @@ -358,8 +465,9 @@ impl EthCoin { required_confirmations: AtomicU64::new(self.required_confirmations.load(Ordering::Relaxed)), ctx: self.ctx.clone(), chain_id: self.chain_id, + trezor_coin: self.trezor_coin.clone(), logs_block_range: self.logs_block_range, - nonce_lock: self.nonce_lock.clone(), + address_nonce_locks: self.address_nonce_locks.clone(), erc20_tokens_infos: Default::default(), nfts_infos: Arc::new(AsyncMutex::new(nft_infos)), abortable_system, @@ -368,15 +476,15 @@ impl EthCoin { } } +/// Activate eth coin from coin config and private key build policy, +/// version 2 of the activation function, with no intrinsic tokens creation pub async fn eth_coin_from_conf_and_request_v2( ctx: &MmArc, ticker: &str, conf: &Json, req: EthActivationV2Request, - priv_key_policy: EthPrivKeyBuildPolicy, + priv_key_build_policy: EthPrivKeyBuildPolicy, ) -> MmResult { - let ticker = ticker.to_string(); - if req.swap_contract_address == Address::default() { return Err(EthActivationV2Error::InvalidSwapContractAddr( "swap_contract_address can't be zero address".to_string(), @@ -393,12 +501,17 @@ pub async fn eth_coin_from_conf_and_request_v2( } } - let (my_address, priv_key_policy) = - build_address_and_priv_key_policy(conf, priv_key_policy, &req.path_to_address).await?; - let my_address_str = checksum_address(&format!("{:02x}", my_address)); - - let chain_id = conf["chain_id"].as_u64(); - + let (priv_key_policy, derivation_method) = build_address_and_priv_key_policy( + ctx, + ticker, + conf, + priv_key_build_policy, + &req.path_to_address, + req.gap_limit, + ) + .await?; + + let chain_id = conf["chain_id"].as_u64().ok_or(EthActivationV2Error::ChainIdNotSet)?; let web3_instances = match (req.rpc_mode, &priv_key_policy) { ( EthRpcMode::Default, @@ -407,23 +520,38 @@ pub async fn eth_coin_from_conf_and_request_v2( activated_key: key_pair, .. }, - ) => build_web3_instances(ctx, ticker.clone(), my_address_str, key_pair, req.nodes.clone()).await?, + ) => { + let auth_address = key_pair.address(); + let auth_address_str = display_eth_address(&auth_address); + build_web3_instances(ctx, ticker.to_string(), auth_address_str, key_pair, req.nodes.clone()).await? + }, (EthRpcMode::Default, EthPrivKeyPolicy::Trezor) => { - return MmError::err(EthActivationV2Error::PrivKeyPolicyNotAllowed( - PrivKeyPolicyNotAllowed::HardwareWalletNotSupported, - )); + let crypto_ctx = CryptoCtx::from_ctx(ctx)?; + let secp256k1_key_pair = crypto_ctx.mm2_internal_key_pair(); + let auth_key_pair = KeyPair::from_secret_slice(secp256k1_key_pair.private_ref()) + .map_to_mm(|_| EthActivationV2Error::InternalError("could not get internal keypair".to_string()))?; + let auth_address = auth_key_pair.address(); + let auth_address_str = display_eth_address(&auth_address); + build_web3_instances( + ctx, + ticker.to_string(), + auth_address_str, + &auth_key_pair, + req.nodes.clone(), + ) + .await? }, #[cfg(target_arch = "wasm32")] (EthRpcMode::Metamask, EthPrivKeyPolicy::Metamask(_)) => { - let chain_id = chain_id - .or_else(|| conf["rpc_chain_id"].as_u64()) - .or_mm_err(|| EthActivationV2Error::ExpectedRpcChainId)?; - build_metamask_transport(ctx, ticker.clone(), chain_id).await? + build_metamask_transport(ctx, ticker.to_string(), chain_id).await? }, #[cfg(target_arch = "wasm32")] (EthRpcMode::Default, EthPrivKeyPolicy::Metamask(_)) | (EthRpcMode::Metamask, _) => { let error = r#"priv_key_policy="Metamask" and rpc_mode="Metamask" should be used both"#.to_string(); - return MmError::err(EthActivationV2Error::ActivationFailed { ticker, error }); + return MmError::err(EthActivationV2Error::ActivationFailed { + ticker: ticker.to_string(), + error, + }); }, }; @@ -439,9 +567,13 @@ pub async fn eth_coin_from_conf_and_request_v2( let sign_message_prefix: Option = json::from_value(conf["sign_message_prefix"].clone()).ok(); - let nonce_lock = { + let trezor_coin: Option = json::from_value(conf["trezor_coin"].clone()).ok(); + + let address_nonce_locks = { let mut map = NONCE_LOCK.lock().unwrap(); - map.entry(ticker.clone()).or_insert_with(new_nonce_lock).clone() + Arc::new(AsyncMutex::new( + map.entry(ticker.to_string()).or_insert_with(new_nonce_lock).clone(), + )) }; // Create an abortable system linked to the `MmCtx` so if the app is stopped on `MmArc::stop`, @@ -450,14 +582,14 @@ pub async fn eth_coin_from_conf_and_request_v2( let coin = EthCoinImpl { priv_key_policy, - my_address, + derivation_method: Arc::new(derivation_method), coin_type: EthCoinType::Eth, sign_message_prefix, swap_contract_address: req.swap_contract_address, fallback_swap_contract: req.fallback_swap_contract, contract_supports_watchers: req.contract_supports_watchers, decimals: ETH_DECIMALS, - ticker, + ticker: ticker.to_string(), gas_station_url: req.gas_station_url, gas_station_decimals: req.gas_station_decimals.unwrap_or(ETH_GAS_STATION_DECIMALS), gas_station_policy: req.gas_station_policy, @@ -466,8 +598,9 @@ pub async fn eth_coin_from_conf_and_request_v2( ctx: ctx.weak(), required_confirmations, chain_id, + trezor_coin, logs_block_range: conf["logs_block_range"].as_u64().unwrap_or(DEFAULT_LOGS_BLOCK_RANGE), - nonce_lock, + address_nonce_locks, erc20_tokens_infos: Default::default(), nfts_infos: Default::default(), abortable_system, @@ -485,31 +618,88 @@ pub async fn eth_coin_from_conf_and_request_v2( /// This function expects either [`PrivKeyBuildPolicy::IguanaPrivKey`] /// or [`PrivKeyBuildPolicy::GlobalHDAccount`], otherwise returns `PrivKeyPolicyNotAllowed` error. pub(crate) async fn build_address_and_priv_key_policy( + ctx: &MmArc, + ticker: &str, conf: &Json, - priv_key_policy: EthPrivKeyBuildPolicy, - path_to_address: &StandardHDCoinAddress, -) -> MmResult<(Address, EthPrivKeyPolicy), EthActivationV2Error> { - match priv_key_policy { + priv_key_build_policy: EthPrivKeyBuildPolicy, + path_to_address: &HDPathAccountToAddressId, + gap_limit: Option, +) -> MmResult<(EthPrivKeyPolicy, EthDerivationMethod), EthActivationV2Error> { + match priv_key_build_policy { EthPrivKeyBuildPolicy::IguanaPrivKey(iguana) => { let key_pair = KeyPair::from_secret_slice(iguana.as_slice()) .map_to_mm(|e| EthActivationV2Error::InternalError(e.to_string()))?; - Ok((key_pair.address(), EthPrivKeyPolicy::Iguana(key_pair))) + let address = key_pair.address(); + let derivation_method = DerivationMethod::SingleAddress(address); + Ok((EthPrivKeyPolicy::Iguana(key_pair), derivation_method)) }, EthPrivKeyBuildPolicy::GlobalHDAccount(global_hd_ctx) => { // Consider storing `derivation_path` at `EthCoinImpl`. - let derivation_path = json::from_value(conf["derivation_path"].clone()) + let path_to_coin = json::from_value(conf["derivation_path"].clone()) .map_to_mm(|e| EthActivationV2Error::ErrorDeserializingDerivationPath(e.to_string()))?; let raw_priv_key = global_hd_ctx - .derive_secp256k1_secret(&derivation_path, path_to_address) + .derive_secp256k1_secret( + &path_to_address + .to_derivation_path(&path_to_coin) + .mm_err(|e| EthActivationV2Error::InvalidPathToAddress(e.to_string()))?, + ) .mm_err(|e| EthActivationV2Error::InternalError(e.to_string()))?; - let activated_key_pair = KeyPair::from_secret_slice(raw_priv_key.as_slice()) + let activated_key = KeyPair::from_secret_slice(raw_priv_key.as_slice()) .map_to_mm(|e| EthActivationV2Error::InternalError(e.to_string()))?; let bip39_secp_priv_key = global_hd_ctx.root_priv_key().clone(); - Ok((activated_key_pair.address(), EthPrivKeyPolicy::HDWallet { - derivation_path, - activated_key: activated_key_pair, - bip39_secp_priv_key, - })) + + let hd_wallet_rmd160 = *ctx.rmd160(); + let hd_wallet_storage = HDWalletCoinStorage::init_with_rmd160(ctx, ticker.to_string(), hd_wallet_rmd160) + .await + .mm_err(EthActivationV2Error::from)?; + let accounts = load_hd_accounts_from_storage(&hd_wallet_storage, &path_to_coin).await?; + let gap_limit = gap_limit.unwrap_or(DEFAULT_GAP_LIMIT); + let hd_wallet = EthHDWallet { + hd_wallet_rmd160, + hd_wallet_storage, + derivation_path: path_to_coin.clone(), + accounts: HDAccountsMutex::new(accounts), + enabled_address: *path_to_address, + gap_limit, + }; + let derivation_method = DerivationMethod::HDWallet(hd_wallet); + Ok(( + EthPrivKeyPolicy::HDWallet { + path_to_coin, + activated_key, + bip39_secp_priv_key, + }, + derivation_method, + )) + }, + EthPrivKeyBuildPolicy::Trezor => { + let path_to_coin = json::from_value(conf["derivation_path"].clone()) + .map_to_mm(|e| EthActivationV2Error::ErrorDeserializingDerivationPath(e.to_string()))?; + + let trezor_coin: Option = json::from_value(conf["trezor_coin"].clone()).ok(); + if trezor_coin.is_none() { + return MmError::err(EthActivationV2Error::CoinDoesntSupportTrezor); + } + let crypto_ctx = CryptoCtx::from_ctx(ctx)?; + let hw_ctx = crypto_ctx + .hw_ctx() + .or_mm_err(|| EthActivationV2Error::HwContextNotInitialized)?; + let hd_wallet_rmd160 = hw_ctx.rmd160(); + let hd_wallet_storage = HDWalletCoinStorage::init_with_rmd160(ctx, ticker.to_string(), hd_wallet_rmd160) + .await + .mm_err(EthActivationV2Error::from)?; + let accounts = load_hd_accounts_from_storage(&hd_wallet_storage, &path_to_coin).await?; + let gap_limit = gap_limit.unwrap_or(DEFAULT_GAP_LIMIT); + let hd_wallet = EthHDWallet { + hd_wallet_rmd160, + hd_wallet_storage, + derivation_path: path_to_coin.clone(), + accounts: HDAccountsMutex::new(accounts), + enabled_address: *path_to_address, + gap_limit, + }; + let derivation_method = DerivationMethod::HDWallet(hd_wallet); + Ok((EthPrivKeyPolicy::Trezor, derivation_method)) }, #[cfg(target_arch = "wasm32")] EthPrivKeyBuildPolicy::Metamask(metamask_ctx) => { @@ -517,11 +707,11 @@ pub(crate) async fn build_address_and_priv_key_policy( let public_key_uncompressed = metamask_ctx.eth_account_pubkey_uncompressed(); let public_key = compress_public_key(public_key_uncompressed)?; Ok(( - address, EthPrivKeyPolicy::Metamask(EthMetamaskPolicy { public_key, public_key_uncompressed, }), + DerivationMethod::SingleAddress(address), )) }, } diff --git a/mm2src/coins/eth/web3_transport/metamask_transport.rs b/mm2src/coins/eth/web3_transport/metamask_transport.rs index a586f9e7f9..45f8bca74d 100644 --- a/mm2src/coins/eth/web3_transport/metamask_transport.rs +++ b/mm2src/coins/eth/web3_transport/metamask_transport.rs @@ -8,13 +8,15 @@ use std::sync::atomic::AtomicBool; use std::sync::Arc; use web3::{RequestId, Transport}; -pub(crate) struct MetamaskEthConfig { +/// Configuration for working with the MetaMask wallet. +pub struct MetamaskEthConfig { /// The `ChainId` that the MetaMask wallet should be targeted on each RPC. pub chain_id: u64, } +/// Transport layer for interacting with the MetaMask wallet. #[derive(Clone)] -pub(crate) struct MetamaskTransport { +pub struct MetamaskTransport { inner: Arc, pub(crate) last_request_failed: Arc, } diff --git a/mm2src/coins/eth/web3_transport/mod.rs b/mm2src/coins/eth/web3_transport/mod.rs index 1ec688f25d..30cb8b8dcb 100644 --- a/mm2src/coins/eth/web3_transport/mod.rs +++ b/mm2src/coins/eth/web3_transport/mod.rs @@ -19,8 +19,9 @@ pub(crate) mod websocket_transport; pub(crate) type Web3SendOut = BoxFuture<'static, Result>; +/// The transport layer for interacting with a Web3 provider. #[derive(Clone, Debug)] -pub(crate) enum Web3Transport { +pub enum Web3Transport { Http(http_transport::HttpTransport), Websocket(websocket_transport::WebsocketTransport), #[cfg(target_arch = "wasm32")] diff --git a/mm2src/coins/hd_wallet.rs b/mm2src/coins/hd_wallet.rs deleted file mode 100644 index 6124393ef1..0000000000 --- a/mm2src/coins/hd_wallet.rs +++ /dev/null @@ -1,435 +0,0 @@ -use crate::hd_confirm_address::{HDConfirmAddress, HDConfirmAddressError}; -use crate::hd_pubkey::HDXPubExtractor; -use crate::hd_wallet_storage::HDWalletStorageError; -use crate::{BalanceError, WithdrawError}; -use async_trait::async_trait; -use crypto::{Bip32DerPathError, Bip32Error, Bip44Chain, ChildNumber, DerivationPath, HwError, StandardHDPath, - StandardHDPathError}; -use derive_more::Display; -use itertools::Itertools; -use mm2_err_handle::prelude::*; -use rpc_task::RpcTaskError; -use serde::Serialize; -use std::collections::BTreeMap; - -pub use futures::lock::{MappedMutexGuard as AsyncMappedMutexGuard, Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard}; - -pub type HDAccountsMap = BTreeMap; -pub type HDAccountsMutex = AsyncMutex>; -pub type HDAccountsMut<'a, HDAccount> = AsyncMutexGuard<'a, HDAccountsMap>; -pub type HDAccountMut<'a, HDAccount> = AsyncMappedMutexGuard<'a, HDAccountsMap, HDAccount>; - -pub type AddressDerivingResult = MmResult; - -const DEFAULT_ADDRESS_LIMIT: u32 = ChildNumber::HARDENED_FLAG; -const DEFAULT_ACCOUNT_LIMIT: u32 = ChildNumber::HARDENED_FLAG; -const DEFAULT_RECEIVER_CHAIN: Bip44Chain = Bip44Chain::External; - -#[derive(Debug, Display)] -pub enum AddressDerivingError { - #[display(fmt = "Coin doesn't support the given BIP44 chain: {:?}", chain)] - InvalidBip44Chain { - chain: Bip44Chain, - }, - #[display(fmt = "BIP32 address deriving error: {}", _0)] - Bip32Error(Bip32Error), - Internal(String), -} - -impl From for AddressDerivingError { - fn from(e: InvalidBip44ChainError) -> Self { AddressDerivingError::InvalidBip44Chain { chain: e.chain } } -} - -impl From for AddressDerivingError { - fn from(e: Bip32Error) -> Self { AddressDerivingError::Bip32Error(e) } -} - -impl From for BalanceError { - fn from(e: AddressDerivingError) -> Self { BalanceError::Internal(e.to_string()) } -} - -impl From for WithdrawError { - fn from(e: AddressDerivingError) -> Self { - match e { - AddressDerivingError::InvalidBip44Chain { .. } | AddressDerivingError::Bip32Error(_) => { - WithdrawError::UnexpectedFromAddress(e.to_string()) - }, - AddressDerivingError::Internal(internal) => WithdrawError::InternalError(internal), - } - } -} - -#[derive(Display)] -pub enum NewAddressDerivingError { - #[display(fmt = "Addresses limit reached. Max number of addresses: {}", max_addresses_number)] - AddressLimitReached { max_addresses_number: u32 }, - #[display(fmt = "Coin doesn't support the given BIP44 chain: {:?}", chain)] - InvalidBip44Chain { chain: Bip44Chain }, - #[display(fmt = "BIP32 address deriving error: {}", _0)] - Bip32Error(Bip32Error), - #[display(fmt = "Wallet storage error: {}", _0)] - WalletStorageError(HDWalletStorageError), - #[display(fmt = "Internal error: {}", _0)] - Internal(String), -} - -impl From for NewAddressDerivingError { - fn from(e: Bip32Error) -> Self { NewAddressDerivingError::Bip32Error(e) } -} - -impl From for NewAddressDerivingError { - fn from(e: AddressDerivingError) -> Self { - match e { - AddressDerivingError::InvalidBip44Chain { chain } => NewAddressDerivingError::InvalidBip44Chain { chain }, - AddressDerivingError::Bip32Error(bip32) => NewAddressDerivingError::Bip32Error(bip32), - AddressDerivingError::Internal(internal) => NewAddressDerivingError::Internal(internal), - } - } -} - -impl From for NewAddressDerivingError { - fn from(e: InvalidBip44ChainError) -> Self { NewAddressDerivingError::InvalidBip44Chain { chain: e.chain } } -} - -impl From for NewAddressDerivingError { - fn from(e: AccountUpdatingError) -> Self { - match e { - AccountUpdatingError::AddressLimitReached { max_addresses_number } => { - NewAddressDerivingError::AddressLimitReached { max_addresses_number } - }, - AccountUpdatingError::InvalidBip44Chain(e) => NewAddressDerivingError::from(e), - AccountUpdatingError::WalletStorageError(storage) => NewAddressDerivingError::WalletStorageError(storage), - } - } -} - -pub enum NewAddressDeriveConfirmError { - DeriveError(NewAddressDerivingError), - ConfirmError(HDConfirmAddressError), -} - -impl From for NewAddressDeriveConfirmError { - fn from(e: HDConfirmAddressError) -> Self { NewAddressDeriveConfirmError::ConfirmError(e) } -} - -impl From for NewAddressDeriveConfirmError { - fn from(e: NewAddressDerivingError) -> Self { NewAddressDeriveConfirmError::DeriveError(e) } -} - -impl From for NewAddressDeriveConfirmError { - fn from(e: AccountUpdatingError) -> Self { - NewAddressDeriveConfirmError::DeriveError(NewAddressDerivingError::from(e)) - } -} - -impl From for NewAddressDeriveConfirmError { - fn from(e: InvalidBip44ChainError) -> Self { - NewAddressDeriveConfirmError::DeriveError(NewAddressDerivingError::from(e)) - } -} - -#[derive(Display)] -pub enum NewAccountCreatingError { - #[display(fmt = "Hardware Wallet context is not initialized")] - HwContextNotInitialized, - #[display(fmt = "HD wallet is unavailable")] - HDWalletUnavailable, - #[display( - fmt = "Coin doesn't support Trezor hardware wallet. Please consider adding the 'trezor_coin' field to the coins config" - )] - CoinDoesntSupportTrezor, - RpcTaskError(RpcTaskError), - HardwareWalletError(HwError), - #[display(fmt = "Accounts limit reached. Max number of accounts: {}", max_accounts_number)] - AccountLimitReached { - max_accounts_number: u32, - }, - #[display(fmt = "Error saving HD account to storage: {}", _0)] - ErrorSavingAccountToStorage(String), - #[display(fmt = "Internal error: {}", _0)] - Internal(String), -} - -impl From for NewAccountCreatingError { - fn from(e: Bip32DerPathError) -> Self { - NewAccountCreatingError::Internal(StandardHDPathError::from(e).to_string()) - } -} - -impl From for NewAccountCreatingError { - fn from(e: HDWalletStorageError) -> Self { - match e { - HDWalletStorageError::ErrorSaving(e) | HDWalletStorageError::ErrorSerializing(e) => { - NewAccountCreatingError::ErrorSavingAccountToStorage(e) - }, - HDWalletStorageError::HDWalletUnavailable => NewAccountCreatingError::HDWalletUnavailable, - HDWalletStorageError::Internal(internal) => NewAccountCreatingError::Internal(internal), - other => NewAccountCreatingError::Internal(other.to_string()), - } - } -} - -/// Currently, we suppose that ETH/ERC20/QRC20 don't have [`Bip44Chain::Internal`] addresses. -#[derive(Display)] -#[display(fmt = "Coin doesn't support the given BIP44 chain: {:?}", chain)] -pub struct InvalidBip44ChainError { - pub chain: Bip44Chain, -} - -#[derive(Display)] -pub enum AccountUpdatingError { - AddressLimitReached { max_addresses_number: u32 }, - InvalidBip44Chain(InvalidBip44ChainError), - WalletStorageError(HDWalletStorageError), -} - -impl From for AccountUpdatingError { - fn from(e: InvalidBip44ChainError) -> Self { AccountUpdatingError::InvalidBip44Chain(e) } -} - -impl From for AccountUpdatingError { - fn from(e: HDWalletStorageError) -> Self { AccountUpdatingError::WalletStorageError(e) } -} - -impl From for BalanceError { - fn from(e: AccountUpdatingError) -> Self { - let error = e.to_string(); - match e { - AccountUpdatingError::AddressLimitReached { .. } | AccountUpdatingError::InvalidBip44Chain(_) => { - // Account updating is expected to be called after `address_id` and `chain` validation. - BalanceError::Internal(format!("Unexpected internal error: {}", error)) - }, - AccountUpdatingError::WalletStorageError(_) => BalanceError::WalletStorageError(error), - } - } -} - -#[derive(Clone)] -pub struct HDAddress { - pub address: Address, - pub pubkey: Pubkey, - pub derivation_path: DerivationPath, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct HDAccountAddressId { - pub account_id: u32, - pub chain: Bip44Chain, - pub address_id: u32, -} - -impl From for HDAccountAddressId { - fn from(der_path: StandardHDPath) -> Self { - HDAccountAddressId { - account_id: der_path.account_id(), - chain: der_path.chain(), - address_id: der_path.address_id(), - } - } -} - -#[derive(Clone, Eq, Hash, PartialEq)] -pub struct HDAddressId { - pub chain: Bip44Chain, - pub address_id: u32, -} - -#[async_trait] -pub trait HDWalletCoinOps { - type Address: Send + Sync; - type Pubkey: Send; - type HDWallet: HDWalletOps; - type HDAccount: HDAccountOps; - - /// Derives an address from the given info. - async fn derive_address( - &self, - hd_account: &Self::HDAccount, - chain: Bip44Chain, - address_id: u32, - ) -> AddressDerivingResult> { - self.derive_addresses(hd_account, std::iter::once(HDAddressId { chain, address_id })) - .await? - .into_iter() - .exactly_one() - // Unfortunately, we can't use [`MapToMmResult::map_to_mm`] due to unsatisfied trait bounds, - // and it's easier to use [`Result::map_err`] instead of adding more trait bounds to this method. - .map_err(|e| MmError::new(AddressDerivingError::Internal(e.to_string()))) - } - - /// Derives HD addresses from the given info. - async fn derive_addresses( - &self, - hd_account: &Self::HDAccount, - address_ids: Ids, - ) -> AddressDerivingResult>> - where - Ids: Iterator + Send; - - async fn derive_known_addresses( - &self, - hd_account: &Self::HDAccount, - chain: Bip44Chain, - ) -> AddressDerivingResult>> { - let known_addresses_number = hd_account.known_addresses_number(chain)?; - let address_ids = (0..known_addresses_number) - .into_iter() - .map(|address_id| HDAddressId { chain, address_id }); - self.derive_addresses(hd_account, address_ids).await - } - - /// Generates a new address and updates the corresponding number of used `hd_account` addresses. - async fn generate_new_address( - &self, - hd_wallet: &Self::HDWallet, - hd_account: &mut Self::HDAccount, - chain: Bip44Chain, - ) -> MmResult, NewAddressDerivingError> { - let inner_impl::NewAddress { - address, - new_known_addresses_number, - } = inner_impl::generate_new_address_immutable(self, hd_wallet, hd_account, chain).await?; - - self.set_known_addresses_number(hd_wallet, hd_account, chain, new_known_addresses_number) - .await?; - Ok(address) - } - - /// Generates a new address, requests the user to confirm if it's the same as on the HW device, - /// and then updates the corresponding number of used `hd_account` addresses. - async fn generate_and_confirm_new_address( - &self, - hd_wallet: &Self::HDWallet, - hd_account: &mut Self::HDAccount, - chain: Bip44Chain, - confirm_address: &ConfirmAddress, - ) -> MmResult, NewAddressDeriveConfirmError> - where - ConfirmAddress: HDConfirmAddress; - - /// Creates a new HD account, registers it within the given `hd_wallet` - /// and returns a mutable reference to the registered account. - async fn create_new_account<'a, XPubExtractor>( - &self, - hd_wallet: &'a Self::HDWallet, - xpub_extractor: &XPubExtractor, - ) -> MmResult, NewAccountCreatingError> - where - XPubExtractor: HDXPubExtractor; - - async fn set_known_addresses_number( - &self, - hd_wallet: &Self::HDWallet, - hd_account: &mut Self::HDAccount, - chain: Bip44Chain, - new_known_addresses_number: u32, - ) -> MmResult<(), AccountUpdatingError>; -} - -#[async_trait] -pub trait HDWalletOps: Send + Sync { - type HDAccount: HDAccountOps + Clone + Send; - - fn coin_type(&self) -> u32; - - fn gap_limit(&self) -> u32; - - /// Returns limit on the number of addresses. - fn address_limit(&self) -> u32 { DEFAULT_ADDRESS_LIMIT } - - /// Returns limit on the number of accounts. - fn account_limit(&self) -> u32 { DEFAULT_ACCOUNT_LIMIT } - - /// Returns a BIP44 chain that is considered as default for receiver addresses. - fn default_receiver_chain(&self) -> Bip44Chain { DEFAULT_RECEIVER_CHAIN } - - fn get_accounts_mutex(&self) -> &HDAccountsMutex; - - /// Returns a copy of an account by the given `account_id` if it's activated. - async fn get_account(&self, account_id: u32) -> Option { - let accounts = self.get_accounts_mutex().lock().await; - accounts.get(&account_id).cloned() - } - - /// Returns a mutable reference to an account by the given `account_id` if it's activated. - async fn get_account_mut(&self, account_id: u32) -> Option> { - let accounts = self.get_accounts_mutex().lock().await; - if !accounts.contains_key(&account_id) { - return None; - } - - Some(AsyncMutexGuard::map(accounts, |accounts| { - accounts - .get_mut(&account_id) - .expect("getting an element should never fail due to the checks above") - })) - } - - /// Returns copies of all activated accounts. - async fn get_accounts(&self) -> HDAccountsMap { self.get_accounts_mutex().lock().await.clone() } - - /// Returns a mutable reference to all activated accounts. - async fn get_accounts_mut(&self) -> HDAccountsMut<'_, Self::HDAccount> { self.get_accounts_mutex().lock().await } - - async fn remove_account_if_last(&self, account_id: u32) -> Option { - let mut x = self.get_accounts_mutex().lock().await; - // `BTreeMap::last_entry` is still unstable. - let (last_account_id, _) = x.iter().last()?; - if *last_account_id == account_id { - x.remove(&account_id) - } else { - None - } - } -} - -pub trait HDAccountOps: Send + Sync { - /// Returns a number of used addresses of this account - /// or an `InvalidBip44ChainError` error if the coin doesn't support the given `chain`. - fn known_addresses_number(&self, chain: Bip44Chain) -> MmResult; - - /// Returns a derivation path of this account. - fn account_derivation_path(&self) -> DerivationPath; - - /// Returns an index of this account. - fn account_id(&self) -> u32; - - /// Returns true if the given address is known at this time. - fn is_address_activated(&self, chain: Bip44Chain, address_id: u32) -> MmResult { - let is_activated = address_id < self.known_addresses_number(chain)?; - Ok(is_activated) - } -} - -pub(crate) mod inner_impl { - use super::*; - - pub struct NewAddress { - pub address: HDAddress, - pub new_known_addresses_number: u32, - } - - /// Generates a new address without updating a corresponding number of used `hd_account` addresses. - pub async fn generate_new_address_immutable( - coin: &Coin, - hd_wallet: &Coin::HDWallet, - hd_account: &Coin::HDAccount, - chain: Bip44Chain, - ) -> MmResult, NewAddressDerivingError> - where - Coin: HDWalletCoinOps + ?Sized + Sync, - { - let known_addresses_number = hd_account.known_addresses_number(chain)?; - // Address IDs start from 0, so the `known_addresses_number = last_known_address_id + 1`. - let new_address_id = known_addresses_number; - let max_addresses_number = hd_wallet.address_limit(); - if new_address_id >= max_addresses_number { - return MmError::err(NewAddressDerivingError::AddressLimitReached { max_addresses_number }); - } - let address = coin.derive_address(hd_account, chain, new_address_id).await?; - Ok(NewAddress { - address, - new_known_addresses_number: known_addresses_number + 1, - }) - } -} diff --git a/mm2src/coins/hd_wallet/account_ops.rs b/mm2src/coins/hd_wallet/account_ops.rs new file mode 100644 index 0000000000..f619753b6b --- /dev/null +++ b/mm2src/coins/hd_wallet/account_ops.rs @@ -0,0 +1,51 @@ +use super::{HDAddressOps, HDAddressesCache, InvalidBip44ChainError}; +use crypto::{Bip44Chain, DerivationPath, HDPathToAccount, Secp256k1ExtendedPublicKey}; +use mm2_err_handle::prelude::*; + +/// `HDAccountOps` Trait +/// +/// Defines operations associated with an HD (Hierarchical Deterministic) account. +/// In the context of BIP-44 derivation paths, an HD account corresponds to the third level (`account'`) +/// in the structure `m / purpose' / coin_type' / account' / chain (or change) / address_index`. +/// This allows for segregating funds into different accounts under the same seed, +/// with each account having multiple chains (often representing internal and external addresses). +/// +/// Implementors of this trait should provide details about such HD account like its specific derivation path, known addresses, and its index. +pub trait HDAccountOps { + type HDAddress: HDAddressOps + Clone + Send; + + /// A constructor for any type that implements `HDAccountOps`. + fn new( + account_id: u32, + account_extended_pubkey: Secp256k1ExtendedPublicKey, + account_derivation_path: HDPathToAccount, + ) -> Self; + + /// Returns the limit on the number of addresses that can be added to an account. + fn address_limit(&self) -> u32; + + /// Returns the number of known addresses for this account for a specific chain/change + /// (internal/external) path. + fn known_addresses_number(&self, chain: Bip44Chain) -> MmResult; + + /// Sets the number of known addresses for this account for a specific chain/change + /// (internal/external) path. + fn set_known_addresses_number(&mut self, chain: Bip44Chain, new_known_addresses_number: u32); + + /// Returns the derivation path associated with this account. + fn account_derivation_path(&self) -> DerivationPath; + + /// Returns the index of this account. + /// The account index is used as part of the derivation path, + /// following the pattern `m/purpose'/coin'/account'`. + fn account_id(&self) -> u32; + + /// Checks if a specific address is activated (known) for this account at the present time. + fn is_address_activated(&self, chain: Bip44Chain, address_id: u32) -> MmResult; + + /// Fetches the derived/cached addresses. + fn derived_addresses(&self) -> &HDAddressesCache; + + /// Fetches the extended public key associated with this account. + fn extended_pubkey(&self) -> &Secp256k1ExtendedPublicKey; +} diff --git a/mm2src/coins/hd_wallet/address_ops.rs b/mm2src/coins/hd_wallet/address_ops.rs new file mode 100644 index 0000000000..45c38e717c --- /dev/null +++ b/mm2src/coins/hd_wallet/address_ops.rs @@ -0,0 +1,18 @@ +use bip32::DerivationPath; +use std::fmt::Display; +use std::hash::Hash; + +/// `HDAddressOps` Trait +/// +/// Defines operations associated with an HD (Hierarchical Deterministic) address. +/// In the context of BIP-44 derivation paths, an HD address corresponds to the fifth level (`address_index`) +/// in the structure `m / purpose' / coin_type' / account' / chain (or change) / address_index`. +/// This allows for managing individual addresses within a specific account and chain. +pub trait HDAddressOps { + type Address: Clone + Display + Eq + Hash + Send + Sync; + type Pubkey: Clone; + + fn address(&self) -> Self::Address; + fn pubkey(&self) -> Self::Pubkey; + fn derivation_path(&self) -> &DerivationPath; +} diff --git a/mm2src/coins/hd_wallet/coin_ops.rs b/mm2src/coins/hd_wallet/coin_ops.rs new file mode 100644 index 0000000000..4291b1809f --- /dev/null +++ b/mm2src/coins/hd_wallet/coin_ops.rs @@ -0,0 +1,241 @@ +use super::{inner_impl, AccountUpdatingError, AddressDerivingError, AddressDerivingResult, HDAccountOps, + HDCoinAddress, HDCoinHDAccount, HDCoinHDAddress, HDConfirmAddress, HDWalletOps, + NewAddressDeriveConfirmError, NewAddressDerivingError}; +use crate::hd_wallet::{HDAddressOps, HDWalletStorageOps, TrezorCoinError}; +use async_trait::async_trait; +use bip32::{ChildNumber, DerivationPath}; +use crypto::{Bip44Chain, Secp256k1ExtendedPublicKey}; +use itertools::Itertools; +use mm2_err_handle::mm_error::{MmError, MmResult}; +use std::collections::HashMap; + +/// Unique identifier for an HD address within an account. +#[derive(Clone, Eq, Hash, PartialEq)] +pub struct HDAddressId { + pub chain: Bip44Chain, + pub address_id: u32, +} + +/// `HDWalletCoinOps` defines operations that coins should support to have HD wallet functionalities. +/// This trait outlines fundamental operations like address derivation, account creation, and more. +#[async_trait] +pub trait HDWalletCoinOps { + /// Any type that represents a Hierarchical Deterministic (HD) wallet. + type HDWallet: HDWalletOps + HDWalletStorageOps + Send + Sync; + + /// Returns a formatter function for address representation. + /// Useful when an address has multiple display formats. + /// For example, Ethereum addresses can be fully displayed or truncated. + /// By default, the formatter uses the Display trait of the address type, which truncates Ethereum addresses. + /// Implement this function if a different display format is required. + fn address_formatter(&self) -> fn(&HDCoinAddress) -> String { |address| address.to_string() } + + /// Derives an address for the coin that implements this trait from an extended public key and a derivation path. + fn address_from_extended_pubkey( + &self, + extended_pubkey: &Secp256k1ExtendedPublicKey, + derivation_path: DerivationPath, + ) -> HDCoinHDAddress; + + /// Retrieves an HD address from the cache or derives it if it hasn't been derived yet. + fn derive_address_with_cache( + &self, + hd_account: &HDCoinHDAccount, + hd_addresses_cache: &mut HashMap>, + hd_address_id: HDAddressId, + ) -> AddressDerivingResult> { + // Check if the given HD address has been derived already. + if let Some(hd_address) = hd_addresses_cache.get(&hd_address_id) { + return Ok(hd_address.clone()); + } + + let change_child = hd_address_id.chain.to_child_number(); + let address_id_child = ChildNumber::from(hd_address_id.address_id); + + let derived_pubkey = hd_account + .extended_pubkey() + .derive_child(change_child)? + .derive_child(address_id_child)?; + + let mut derivation_path = hd_account.account_derivation_path(); + derivation_path.push(change_child); + derivation_path.push(address_id_child); + drop_mutability!(derivation_path); + let hd_address = self.address_from_extended_pubkey(&derived_pubkey, derivation_path); + + // Cache the derived `hd_address`. + hd_addresses_cache.insert(hd_address_id, hd_address.clone()); + Ok(hd_address) + } + + /// Derives a single HD address for a given account, chain, and address identifier. + async fn derive_address( + &self, + hd_account: &HDCoinHDAccount, + chain: Bip44Chain, + address_id: u32, + ) -> AddressDerivingResult> { + self.derive_addresses(hd_account, std::iter::once(HDAddressId { chain, address_id })) + .await? + .into_iter() + .exactly_one() + // Unfortunately, we can't use [`MapToMmResult::map_to_mm`] due to unsatisfied trait bounds, + // and it's easier to use [`Result::map_err`] instead of adding more trait bounds to this method. + .map_err(|e| MmError::new(AddressDerivingError::Internal(e.to_string()))) + } + + /// Derives a set of HD addresses for a coin using the specified HD account and address identifiers. + #[cfg(not(target_arch = "wasm32"))] + async fn derive_addresses( + &self, + hd_account: &HDCoinHDAccount, + address_ids: Ids, + ) -> AddressDerivingResult>> + where + Ids: Iterator + Send, + { + let mut hd_addresses_cache_guard = hd_account.derived_addresses().lock().await; + let hd_addresses_cache = &mut *hd_addresses_cache_guard; + address_ids + .map(|hd_address_id| self.derive_address_with_cache(hd_account, hd_addresses_cache, hd_address_id)) + .collect() + } + + // Todo: combine both implementations once worker threads are supported in WASM + /// [`HDWalletCoinOps::derive_addresses`] WASM implementation. + /// + /// # Important + /// + /// This function locks [`HDAddressesCache::cache`] mutex at each iteration. + /// + /// # Performance + /// + /// Locking the [`HDAddressesCache::cache`] mutex at each iteration may significantly degrade performance. + /// But this is required at least for now due the facts that: + /// 1) mm2 runs in the same thread as `KomodoPlatform/air_dex` runs; + /// 2) [`ExtendedPublicKey::derive_child`] is a synchronous operation, and it takes a long time. + /// So we need to periodically invoke Javascript runtime to handle UI events and other asynchronous tasks. + #[cfg(target_arch = "wasm32")] + async fn derive_addresses( + &self, + hd_account: &HDCoinHDAccount, + address_ids: Ids, + ) -> AddressDerivingResult>> + where + Ids: Iterator + Send, + { + let mut result = Vec::new(); + for hd_address_id in address_ids { + let mut hd_addresses_cache = hd_account.derived_addresses().lock().await; + + let hd_address = self.derive_address_with_cache(hd_account, &mut hd_addresses_cache, hd_address_id)?; + result.push(hd_address); + } + + Ok(result) + } + + /// Retrieves or derives known HD addresses for a specific account and chain. + /// Essentially, this retrieves addresses that have been interacted with in the past. + async fn derive_known_addresses( + &self, + hd_account: &HDCoinHDAccount, + chain: Bip44Chain, + ) -> AddressDerivingResult>> { + let known_addresses_number = hd_account.known_addresses_number(chain)?; + let address_ids = (0..known_addresses_number) + .into_iter() + .map(|address_id| HDAddressId { chain, address_id }); + self.derive_addresses(hd_account, address_ids).await + } + + /// Generates a new address for a coin and updates the corresponding number of used `hd_account` addresses. + async fn generate_new_address( + &self, + hd_wallet: &Self::HDWallet, + hd_account: &mut HDCoinHDAccount, + chain: Bip44Chain, + ) -> MmResult, NewAddressDerivingError> { + let inner_impl::NewAddress { + hd_address: address, + new_known_addresses_number, + } = inner_impl::generate_new_address_immutable(self, hd_account, chain).await?; + + self.set_known_addresses_number(hd_wallet, hd_account, chain, new_known_addresses_number) + .await?; + Ok(address) + } + + /// Generates a new address with an added confirmation step. + /// This method prompts the user to verify if the derived address matches + /// the hardware wallet display, ensuring security and accuracy when + /// dealing with hardware wallets. + async fn generate_and_confirm_new_address( + &self, + hd_wallet: &Self::HDWallet, + hd_account: &mut HDCoinHDAccount, + chain: Bip44Chain, + confirm_address: &ConfirmAddress, + ) -> MmResult, NewAddressDeriveConfirmError> + where + ConfirmAddress: HDConfirmAddress, + { + use super::inner_impl; + + let inner_impl::NewAddress { + hd_address, + new_known_addresses_number, + } = inner_impl::generate_new_address_immutable(self, hd_account, chain).await?; + + let trezor_coin = self.trezor_coin()?; + let derivation_path = hd_address.derivation_path().clone(); + let expected_address = hd_address.address().to_string(); + // Ask the user to confirm if the given `expected_address` is the same as on the HW display. + confirm_address + .confirm_address(trezor_coin, derivation_path, expected_address) + .await?; + + let actual_known_addresses_number = hd_account.known_addresses_number(chain)?; + // Check if the actual `known_addresses_number` hasn't been changed while we waited for the user confirmation. + // If the actual value is greater than the new one, we don't need to update. + if actual_known_addresses_number < new_known_addresses_number { + self.set_known_addresses_number(hd_wallet, hd_account, chain, new_known_addresses_number) + .await?; + } + + Ok(hd_address) + } + + /// Updates the count of known addresses for a specified HD account and chain/change path. + /// This is useful for tracking the number of created addresses. + async fn set_known_addresses_number( + &self, + hd_wallet: &Self::HDWallet, + hd_account: &mut HDCoinHDAccount, + chain: Bip44Chain, + new_known_addresses_number: u32, + ) -> MmResult<(), AccountUpdatingError> { + let max_addresses_number = hd_account.address_limit(); + if new_known_addresses_number >= max_addresses_number { + return MmError::err(AccountUpdatingError::AddressLimitReached { max_addresses_number }); + } + match chain { + Bip44Chain::External => { + hd_wallet + .update_external_addresses_number(hd_account.account_id(), new_known_addresses_number) + .await? + }, + Bip44Chain::Internal => { + hd_wallet + .update_internal_addresses_number(hd_account.account_id(), new_known_addresses_number) + .await? + }, + } + hd_account.set_known_addresses_number(chain, new_known_addresses_number); + + Ok(()) + } + + /// Returns the Trezor coin name for this coin. + fn trezor_coin(&self) -> MmResult; +} diff --git a/mm2src/coins/hd_confirm_address.rs b/mm2src/coins/hd_wallet/confirm_address.rs similarity index 79% rename from mm2src/coins/hd_confirm_address.rs rename to mm2src/coins/hd_wallet/confirm_address.rs index deccbac75b..39bb286322 100644 --- a/mm2src/coins/hd_confirm_address.rs +++ b/mm2src/coins/hd_wallet/confirm_address.rs @@ -2,7 +2,7 @@ use async_trait::async_trait; use bip32::DerivationPath; use crypto::hw_rpc_task::HwConnectStatuses; use crypto::trezor::trezor_rpc_task::{TrezorRequestStatuses, TrezorRpcTaskProcessor, TryIntoUserAction}; -use crypto::trezor::{ProcessTrezorResponse, TrezorError, TrezorProcessingError}; +use crypto::trezor::{ProcessTrezorResponse, TrezorError, TrezorMessageType, TrezorProcessingError}; use crypto::{CryptoCtx, CryptoCtxError, HardwareWalletArc, HwError, HwProcessingError}; use enum_derives::{EnumFromInner, EnumFromStringify}; use mm2_core::mm_ctx::MmArc; @@ -22,6 +22,7 @@ pub enum HDConfirmAddressError { expected: String, found: String, }, + NoAddressReceived, #[from_stringify("CryptoCtxError")] Internal(String), } @@ -50,7 +51,7 @@ impl From> for HDConfirmAddressError { } /// An `InProgress` status constructor. -pub trait ConfirmAddressStatus: Sized { +pub(crate) trait ConfirmAddressStatus: Sized { /// Returns an `InProgress` RPC status that will be used to ask the user /// to confirm an `address` on his HW device. fn confirm_addr_status(address: String) -> Self; @@ -60,19 +61,20 @@ pub trait ConfirmAddressStatus: Sized { #[async_trait] pub trait HDConfirmAddress: Sync { /// Asks the user to confirm if the given `expected_address` is the same as on the HW display. - async fn confirm_utxo_address( + async fn confirm_address( &self, - trezor_utxo_coin: String, + trezor_coin: String, derivation_path: DerivationPath, expected_address: String, ) -> MmResult<(), HDConfirmAddressError>; } -pub enum RpcTaskConfirmAddress { +pub(crate) enum RpcTaskConfirmAddress { Trezor { hw_ctx: HardwareWalletArc, task_handle: RpcTaskHandleShared, statuses: HwConnectStatuses, + trezor_message_type: TrezorMessageType, }, } @@ -83,7 +85,7 @@ where Task::InProgressStatus: ConfirmAddressStatus, Task::UserAction: TryIntoUserAction + Send, { - async fn confirm_utxo_address( + async fn confirm_address( &self, trezor_utxo_coin: String, derivation_path: DerivationPath, @@ -94,14 +96,16 @@ where hw_ctx, task_handle, statuses, + trezor_message_type, } => { - Self::confirm_utxo_address_with_trezor( + Self::confirm_address_with_trezor( hw_ctx, task_handle.clone(), statuses, trezor_utxo_coin, derivation_path, expected_address, + trezor_message_type, ) .await }, @@ -119,6 +123,7 @@ where ctx: &MmArc, task_handle: RpcTaskHandleShared, statuses: HwConnectStatuses, + trezor_message_type: TrezorMessageType, ) -> MmResult, HDConfirmAddressError> { let crypto_ctx = CryptoCtx::from_ctx(ctx)?; let hw_ctx = crypto_ctx @@ -128,16 +133,18 @@ where hw_ctx, task_handle, statuses, + trezor_message_type, }) } - async fn confirm_utxo_address_with_trezor( + async fn confirm_address_with_trezor( hw_ctx: &HardwareWalletArc, task_handle: RpcTaskHandleShared, connect_statuses: &HwConnectStatuses, trezor_coin: String, derivation_path: DerivationPath, expected_address: String, + trezor_message_type: &TrezorMessageType, ) -> MmResult<(), HDConfirmAddressError> { let confirm_statuses = TrezorRequestStatuses { on_button_request: Task::InProgressStatus::confirm_addr_status(expected_address.clone()), @@ -147,11 +154,21 @@ where let pubkey_processor = TrezorRpcTaskProcessor::new(task_handle, confirm_statuses); let pubkey_processor = Arc::new(pubkey_processor); let mut trezor_session = hw_ctx.trezor(pubkey_processor.clone()).await?; - let address = trezor_session - .get_utxo_address(derivation_path, trezor_coin, SHOW_ADDRESS_ON_DISPLAY) - .await? - .process(pubkey_processor.clone()) - .await?; + let address = match trezor_message_type { + TrezorMessageType::Bitcoin => { + trezor_session + .get_utxo_address(derivation_path, trezor_coin, SHOW_ADDRESS_ON_DISPLAY) + .await? + .process(pubkey_processor.clone()) + .await? + }, + TrezorMessageType::Ethereum => trezor_session + .get_eth_address(derivation_path, SHOW_ADDRESS_ON_DISPLAY) + .await? + .process(pubkey_processor.clone()) + .await? + .or_mm_err(|| HDConfirmAddressError::NoAddressReceived)?, + }; if address != expected_address { return MmError::err(HDConfirmAddressError::InvalidAddress { @@ -169,12 +186,12 @@ pub(crate) mod for_tests { use mocktopus::macros::mockable; #[derive(Default)] - pub struct MockableConfirmAddress; + pub(crate) struct MockableConfirmAddress; #[async_trait] #[mockable] impl HDConfirmAddress for MockableConfirmAddress { - async fn confirm_utxo_address( + async fn confirm_address( &self, _trezor_utxo_coin: String, _derivation_path: DerivationPath, diff --git a/mm2src/coins/hd_wallet/errors.rs b/mm2src/coins/hd_wallet/errors.rs new file mode 100644 index 0000000000..6667fd2021 --- /dev/null +++ b/mm2src/coins/hd_wallet/errors.rs @@ -0,0 +1,243 @@ +use super::{HDConfirmAddressError, HDWalletStorageError}; +use bip32::Error as Bip32Error; +use crypto::trezor::{TrezorError, TrezorProcessingError}; +use crypto::{Bip32DerPathError, Bip44Chain, CryptoCtxError, HwError, HwProcessingError, StandardHDPathError, XpubError}; +use rpc_task::RpcTaskError; + +#[derive(Debug, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum AddressDerivingError { + #[display(fmt = "Coin doesn't support the given BIP44 chain: {:?}", chain)] + InvalidBip44Chain { + chain: Bip44Chain, + }, + #[display(fmt = "BIP32 address deriving error: {}", _0)] + Bip32Error(String), + Internal(String), +} + +impl From for AddressDerivingError { + fn from(e: InvalidBip44ChainError) -> Self { AddressDerivingError::InvalidBip44Chain { chain: e.chain } } +} + +impl From for AddressDerivingError { + fn from(e: Bip32Error) -> Self { AddressDerivingError::Bip32Error(e.to_string()) } +} + +#[derive(Display)] +pub enum NewAddressDerivingError { + #[display(fmt = "Addresses limit reached. Max number of addresses: {}", max_addresses_number)] + AddressLimitReached { max_addresses_number: u32 }, + #[display(fmt = "Coin doesn't support the given BIP44 chain: {:?}", chain)] + InvalidBip44Chain { chain: Bip44Chain }, + #[display(fmt = "BIP32 address deriving error: {}", _0)] + Bip32Error(String), + #[display(fmt = "Wallet storage error: {}", _0)] + WalletStorageError(HDWalletStorageError), + #[display(fmt = "Internal error: {}", _0)] + Internal(String), +} + +impl From for NewAddressDerivingError { + fn from(e: Bip32Error) -> Self { NewAddressDerivingError::Bip32Error(e.to_string()) } +} + +impl From for NewAddressDerivingError { + fn from(e: AddressDerivingError) -> Self { + match e { + AddressDerivingError::InvalidBip44Chain { chain } => NewAddressDerivingError::InvalidBip44Chain { chain }, + AddressDerivingError::Bip32Error(bip32) => NewAddressDerivingError::Bip32Error(bip32), + AddressDerivingError::Internal(internal) => NewAddressDerivingError::Internal(internal), + } + } +} + +impl From for NewAddressDerivingError { + fn from(e: InvalidBip44ChainError) -> Self { NewAddressDerivingError::InvalidBip44Chain { chain: e.chain } } +} + +impl From for NewAddressDerivingError { + fn from(e: AccountUpdatingError) -> Self { + match e { + AccountUpdatingError::AddressLimitReached { max_addresses_number } => { + NewAddressDerivingError::AddressLimitReached { max_addresses_number } + }, + AccountUpdatingError::InvalidBip44Chain(e) => NewAddressDerivingError::from(e), + AccountUpdatingError::WalletStorageError(storage) => NewAddressDerivingError::WalletStorageError(storage), + } + } +} + +pub enum NewAddressDeriveConfirmError { + DeriveError(NewAddressDerivingError), + ConfirmError(HDConfirmAddressError), +} + +impl From for NewAddressDeriveConfirmError { + fn from(e: HDConfirmAddressError) -> Self { NewAddressDeriveConfirmError::ConfirmError(e) } +} + +impl From for NewAddressDeriveConfirmError { + fn from(e: NewAddressDerivingError) -> Self { NewAddressDeriveConfirmError::DeriveError(e) } +} + +impl From for NewAddressDeriveConfirmError { + fn from(e: AccountUpdatingError) -> Self { + NewAddressDeriveConfirmError::DeriveError(NewAddressDerivingError::from(e)) + } +} + +impl From for NewAddressDeriveConfirmError { + fn from(e: InvalidBip44ChainError) -> Self { + NewAddressDeriveConfirmError::DeriveError(NewAddressDerivingError::from(e)) + } +} + +#[derive(Display)] +pub enum NewAccountCreationError { + #[display(fmt = "Hardware Wallet context is not initialized")] + HwContextNotInitialized, + #[display(fmt = "HD wallet is unavailable")] + HDWalletUnavailable, + #[display( + fmt = "Coin doesn't support Trezor hardware wallet. Please consider adding the 'trezor_coin' field to the coins config" + )] + CoinDoesntSupportTrezor, + RpcTaskError(RpcTaskError), + HardwareWalletError(HwError), + #[display(fmt = "Accounts limit reached. Max number of accounts: {}", max_accounts_number)] + AccountLimitReached { + max_accounts_number: u32, + }, + #[display(fmt = "Error saving HD account to storage: {}", _0)] + ErrorSavingAccountToStorage(String), + #[display(fmt = "Internal error: {}", _0)] + Internal(String), +} + +impl From for NewAccountCreationError { + fn from(e: Bip32DerPathError) -> Self { + NewAccountCreationError::Internal(StandardHDPathError::from(e).to_string()) + } +} + +impl From for NewAccountCreationError { + fn from(e: HDWalletStorageError) -> Self { + match e { + HDWalletStorageError::ErrorSaving(e) | HDWalletStorageError::ErrorSerializing(e) => { + NewAccountCreationError::ErrorSavingAccountToStorage(e) + }, + HDWalletStorageError::HDWalletUnavailable => NewAccountCreationError::HDWalletUnavailable, + HDWalletStorageError::Internal(internal) => NewAccountCreationError::Internal(internal), + other => NewAccountCreationError::Internal(other.to_string()), + } + } +} + +/// Currently, we suppose that ETH/ERC20/QRC20 don't have [`Bip44Chain::Internal`] addresses. +#[derive(Display)] +#[display(fmt = "Coin doesn't support the given BIP44 chain: {:?}", chain)] +pub struct InvalidBip44ChainError { + pub chain: Bip44Chain, +} + +#[derive(Display)] +pub enum AccountUpdatingError { + AddressLimitReached { max_addresses_number: u32 }, + InvalidBip44Chain(InvalidBip44ChainError), + WalletStorageError(HDWalletStorageError), +} + +impl From for AccountUpdatingError { + fn from(e: InvalidBip44ChainError) -> Self { AccountUpdatingError::InvalidBip44Chain(e) } +} + +impl From for AccountUpdatingError { + fn from(e: HDWalletStorageError) -> Self { AccountUpdatingError::WalletStorageError(e) } +} + +pub enum HDWithdrawError { + UnexpectedFromAddress(String), + UnknownAccount { account_id: u32 }, + AddressDerivingError(AddressDerivingError), + InternalError(String), +} + +impl From for HDWithdrawError { + fn from(e: AddressDerivingError) -> Self { HDWithdrawError::AddressDerivingError(e) } +} + +#[derive(Clone)] +pub enum HDExtractPubkeyError { + HwContextNotInitialized, + CoinDoesntSupportTrezor, + RpcTaskError(RpcTaskError), + HardwareWalletError(HwError), + InvalidXpub(String), + Internal(String), +} + +impl From for HDExtractPubkeyError { + fn from(e: CryptoCtxError) -> Self { HDExtractPubkeyError::Internal(e.to_string()) } +} + +impl From for HDExtractPubkeyError { + fn from(e: TrezorError) -> Self { HDExtractPubkeyError::HardwareWalletError(HwError::from(e)) } +} + +impl From for HDExtractPubkeyError { + fn from(e: HwError) -> Self { HDExtractPubkeyError::HardwareWalletError(e) } +} + +impl From> for HDExtractPubkeyError { + fn from(e: TrezorProcessingError) -> Self { + match e { + TrezorProcessingError::TrezorError(trezor) => HDExtractPubkeyError::from(HwError::from(trezor)), + TrezorProcessingError::ProcessorError(rpc) => HDExtractPubkeyError::RpcTaskError(rpc), + } + } +} + +impl From> for HDExtractPubkeyError { + fn from(e: HwProcessingError) -> Self { + match e { + HwProcessingError::HwError(hw) => HDExtractPubkeyError::from(hw), + HwProcessingError::ProcessorError(rpc) => HDExtractPubkeyError::RpcTaskError(rpc), + HwProcessingError::InternalError(internal) => HDExtractPubkeyError::Internal(internal), + } + } +} + +impl From for HDExtractPubkeyError { + fn from(e: XpubError) -> Self { HDExtractPubkeyError::InvalidXpub(e.to_string()) } +} + +impl From for NewAccountCreationError { + fn from(e: HDExtractPubkeyError) -> Self { + match e { + HDExtractPubkeyError::HwContextNotInitialized => NewAccountCreationError::HwContextNotInitialized, + HDExtractPubkeyError::CoinDoesntSupportTrezor => NewAccountCreationError::CoinDoesntSupportTrezor, + HDExtractPubkeyError::RpcTaskError(rpc) => NewAccountCreationError::RpcTaskError(rpc), + HDExtractPubkeyError::HardwareWalletError(hw) => NewAccountCreationError::HardwareWalletError(hw), + HDExtractPubkeyError::InvalidXpub(xpub) => { + NewAccountCreationError::HardwareWalletError(HwError::InvalidXpub(xpub)) + }, + HDExtractPubkeyError::Internal(internal) => NewAccountCreationError::Internal(internal), + } + } +} + +#[derive(Display)] +pub enum TrezorCoinError { + Internal(String), +} + +impl From for HDExtractPubkeyError { + fn from(e: TrezorCoinError) -> Self { HDExtractPubkeyError::Internal(e.to_string()) } +} + +impl From for NewAddressDeriveConfirmError { + fn from(e: TrezorCoinError) -> Self { + NewAddressDeriveConfirmError::DeriveError(NewAddressDerivingError::Internal(e.to_string())) + } +} diff --git a/mm2src/coins/hd_wallet/mod.rs b/mm2src/coins/hd_wallet/mod.rs new file mode 100644 index 0000000000..b8db30733a --- /dev/null +++ b/mm2src/coins/hd_wallet/mod.rs @@ -0,0 +1,517 @@ +use async_trait::async_trait; +use common::log::warn; +use crypto::{Bip32DerPathOps, Bip32Error, Bip44Chain, ChildNumber, DerivationPath, HDPathToAccount, HDPathToCoin, + Secp256k1ExtendedPublicKey, StandardHDPath, StandardHDPathError}; +use futures::lock::{MappedMutexGuard as AsyncMappedMutexGuard, Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard}; +use mm2_err_handle::prelude::*; +use primitives::hash::H160; +use serde::Serialize; +use std::collections::{BTreeMap, HashMap}; +use std::fmt::Display; +use std::hash::Hash; +use std::str::FromStr; +use std::sync::Arc; + +mod account_ops; +pub use account_ops::HDAccountOps; + +mod address_ops; +pub use address_ops::HDAddressOps; + +mod coin_ops; +pub use coin_ops::{HDAddressId, HDWalletCoinOps}; + +mod confirm_address; +#[cfg(test)] +pub(crate) use confirm_address::for_tests::MockableConfirmAddress; +pub(crate) use confirm_address::{ConfirmAddressStatus, RpcTaskConfirmAddress}; +pub use confirm_address::{HDConfirmAddress, HDConfirmAddressError}; + +mod errors; +pub use errors::{AccountUpdatingError, AddressDerivingError, HDExtractPubkeyError, HDWithdrawError, + InvalidBip44ChainError, NewAccountCreationError, NewAddressDeriveConfirmError, + NewAddressDerivingError, TrezorCoinError}; + +mod pubkey; +pub use pubkey::{ExtractExtendedPubkey, HDXPubExtractor, RpcTaskXPubExtractor}; + +mod storage; +#[cfg(target_arch = "wasm32")] +pub(crate) use storage::HDWalletDb; +#[cfg(test)] pub(crate) use storage::HDWalletMockStorage; +pub use storage::{HDAccountStorageItem, HDAccountStorageOps, HDWalletCoinStorage, HDWalletId, HDWalletStorageError, + HDWalletStorageOps}; +pub(crate) use storage::{HDWalletStorageInternalOps, HDWalletStorageResult}; + +mod wallet_ops; +pub use wallet_ops::HDWalletOps; + +mod withdraw_ops; +pub use withdraw_ops::{HDCoinWithdrawOps, WithdrawFrom, WithdrawSenderAddress}; + +pub(crate) type AddressDerivingResult = MmResult; +pub(crate) type HDAccountsMap = BTreeMap; +pub(crate) type HDAccountsMutex = AsyncMutex>; +pub(crate) type HDAccountsMut<'a, HDAccount> = AsyncMutexGuard<'a, HDAccountsMap>; +pub(crate) type HDAccountMut<'a, HDAccount> = AsyncMappedMutexGuard<'a, HDAccountsMap, HDAccount>; +pub(crate) type HDWalletAddress = + <<::HDAccount as HDAccountOps>::HDAddress as HDAddressOps>::Address; +pub(crate) type HDWalletPubKey = + <<::HDAccount as HDAccountOps>::HDAddress as HDAddressOps>::Pubkey; +pub(crate) type HDCoinAddress = HDWalletAddress<::HDWallet>; +pub(crate) type HDCoinPubKey = HDWalletPubKey<::HDWallet>; +pub(crate) type HDWalletHDAddress = <::HDAccount as HDAccountOps>::HDAddress; +pub(crate) type HDCoinHDAddress = HDWalletHDAddress<::HDWallet>; +pub(crate) type HDWalletHDAccount = ::HDAccount; +pub(crate) type HDCoinHDAccount = HDWalletHDAccount<::HDWallet>; + +pub(crate) const DEFAULT_GAP_LIMIT: u32 = 20; +const DEFAULT_ACCOUNT_LIMIT: u32 = ChildNumber::HARDENED_FLAG; +const DEFAULT_ADDRESS_LIMIT: u32 = ChildNumber::HARDENED_FLAG; +const DEFAULT_RECEIVER_CHAIN: Bip44Chain = Bip44Chain::External; + +/// A generic HD address that can be used with any HD wallet. +#[derive(Clone)] +pub struct HDAddress { + pub address: Address, + pub pubkey: Pubkey, + pub derivation_path: DerivationPath, +} + +impl HDAddressOps for HDAddress +where + Address: Clone + Display + Eq + Hash + Send + Sync, + Pubkey: Clone, +{ + type Address = Address; + type Pubkey = Pubkey; + + fn address(&self) -> Self::Address { self.address.clone() } + + fn pubkey(&self) -> Self::Pubkey { self.pubkey.clone() } + + fn derivation_path(&self) -> &DerivationPath { &self.derivation_path } +} + +/// A generic HD address that can be used with any HD wallet. +#[derive(Clone, Debug)] +pub struct HDAddressesCache { + cache: Arc>>, +} + +impl Default for HDAddressesCache { + fn default() -> Self { + HDAddressesCache { + cache: Arc::new(AsyncMutex::new(HashMap::new())), + } + } +} + +impl HDAddressesCache { + pub fn with_capacity(capacity: usize) -> Self { + HDAddressesCache { + cache: Arc::new(AsyncMutex::new(HashMap::with_capacity(capacity))), + } + } + + pub async fn lock(&self) -> AsyncMutexGuard<'_, HashMap> { self.cache.lock().await } +} + +/// A generic HD account that can be used with any HD wallet. +#[derive(Clone, Debug)] +pub struct HDAccount +where + HDAddress: HDAddressOps + Send, +{ + pub account_id: u32, + /// [Extended public key](https://learnmeabitcoin.com/technical/extended-keys) that corresponds to the derivation path: + /// `m/purpose'/coin_type'/account'`. + pub extended_pubkey: Secp256k1ExtendedPublicKey, + /// [`HDWallet::derivation_path`] derived by [`HDAccount::account_id`]. + pub account_derivation_path: HDPathToAccount, + /// The number of addresses that we know have been used by the user. + /// This is used in order not to check the transaction history for each address, + /// but to request the balance of addresses whose index is less than `address_number`. + pub external_addresses_number: u32, + /// The number of internal addresses that we know have been used by the user. + /// This is used in order not to check the transaction history for each address, + /// but to request the balance of addresses whose index is less than `address_number`. + pub internal_addresses_number: u32, + /// The cache of derived addresses. + /// This is used at [`HDWalletCoinOps::derive_address`]. + pub derived_addresses: HDAddressesCache, +} + +impl HDAccountOps for HDAccount +where + HDAddress: HDAddressOps + Clone + Send, +{ + type HDAddress = HDAddress; + + fn new( + account_id: u32, + extended_pubkey: Secp256k1ExtendedPublicKey, + account_derivation_path: HDPathToAccount, + ) -> Self { + HDAccount { + account_id, + extended_pubkey, + account_derivation_path, + external_addresses_number: 0, + internal_addresses_number: 0, + derived_addresses: HDAddressesCache::default(), + } + } + + fn address_limit(&self) -> u32 { DEFAULT_ADDRESS_LIMIT } + + fn known_addresses_number(&self, chain: Bip44Chain) -> MmResult { + match chain { + Bip44Chain::External => Ok(self.external_addresses_number), + Bip44Chain::Internal => Ok(self.internal_addresses_number), + } + } + + fn set_known_addresses_number(&mut self, chain: Bip44Chain, num: u32) { + match chain { + Bip44Chain::External => { + self.external_addresses_number = num; + }, + Bip44Chain::Internal => { + self.internal_addresses_number = num; + }, + } + } + + fn account_derivation_path(&self) -> DerivationPath { self.account_derivation_path.to_derivation_path() } + + fn account_id(&self) -> u32 { self.account_id } + + fn is_address_activated(&self, chain: Bip44Chain, address_id: u32) -> MmResult { + let is_activated = address_id < self.known_addresses_number(chain)?; + Ok(is_activated) + } + + fn derived_addresses(&self) -> &HDAddressesCache { &self.derived_addresses } + + fn extended_pubkey(&self) -> &Secp256k1ExtendedPublicKey { &self.extended_pubkey } +} + +impl HDAccountStorageOps for HDAccount +where + HDAddress: HDAddressOps + Send, +{ + fn try_from_storage_item( + wallet_der_path: &HDPathToCoin, + account_info: &HDAccountStorageItem, + ) -> HDWalletStorageResult + where + Self: Sized, + { + const ACCOUNT_CHILD_HARDENED: bool = true; + + let account_child = ChildNumber::new(account_info.account_id, ACCOUNT_CHILD_HARDENED)?; + let account_derivation_path = wallet_der_path + .derive(account_child) + .map_to_mm(StandardHDPathError::from)?; + let extended_pubkey = Secp256k1ExtendedPublicKey::from_str(&account_info.account_xpub)?; + let capacity = + account_info.external_addresses_number + account_info.internal_addresses_number + DEFAULT_GAP_LIMIT; + Ok(HDAccount { + account_id: account_info.account_id, + extended_pubkey, + account_derivation_path, + external_addresses_number: account_info.external_addresses_number, + internal_addresses_number: account_info.internal_addresses_number, + derived_addresses: HDAddressesCache::with_capacity(capacity as usize), + }) + } + + fn to_storage_item(&self) -> HDAccountStorageItem { + HDAccountStorageItem { + account_id: self.account_id, + account_xpub: self.extended_pubkey.to_string(bip32::Prefix::XPUB), + external_addresses_number: self.external_addresses_number, + internal_addresses_number: self.internal_addresses_number, + } + } +} + +pub async fn load_hd_accounts_from_storage( + hd_wallet_storage: &HDWalletCoinStorage, + derivation_path: &HDPathToCoin, +) -> HDWalletStorageResult>> +where + HDAddress: HDAddressOps + Send, +{ + let accounts = hd_wallet_storage.load_all_accounts().await?; + let res: HDWalletStorageResult>> = accounts + .iter() + .map(|account_info| { + let account = HDAccount::try_from_storage_item(derivation_path, account_info)?; + Ok((account.account_id, account)) + }) + .collect(); + match res { + Ok(accounts) => Ok(accounts), + Err(e) if e.get_inner().is_deserializing_err() => { + warn!("Error loading HD accounts from the storage: '{}'. Clear accounts", e); + hd_wallet_storage.clear_accounts().await?; + Ok(HDAccountsMap::new()) + }, + Err(e) => Err(e), + } +} + +/// Represents a Hierarchical Deterministic (HD) wallet for UTXO coins. +/// This struct encapsulates all the necessary data for HD wallet operations +/// and is initialized whenever a utxo coin is activated in HD wallet mode. +#[derive(Debug)] +pub struct HDWallet +where + HDAccount: HDAccountOps + Clone + Send + Sync, +{ + /// A unique identifier for the HD wallet derived from the master public key. + /// Specifically, it's the RIPEMD160 hash of the SHA256 hash of the master pubkey. + /// This property aids in storing database items uniquely for each HD wallet. + pub hd_wallet_rmd160: H160, + /// Provides a means to access database operations for a specific user, HD wallet, and coin. + /// The storage wrapper associates with the `coin` and `hd_wallet_rmd160` to provide unique storage access. + pub hd_wallet_storage: HDWalletCoinStorage, + /// Derivation path of the coin. + /// This derivation path consists of `purpose` and `coin_type` only + /// where the full `BIP44` address has the following structure: + /// `m/purpose'/coin_type'/account'/change/address_index`. + pub derivation_path: HDPathToCoin, + /// Contains information about the accounts enabled for this HD wallet. + pub accounts: HDAccountsMutex, + // Todo: This should be removed in the future to enable simultaneous swaps from multiple addresses + /// The address that's specifically enabled for certain operations, e.g. swaps. + pub enabled_address: HDPathAccountToAddressId, + /// Defines the maximum number of consecutive addresses that can be generated + /// without any associated transactions. If an address outside this limit + /// receives transactions, they won't be identified. + pub gap_limit: u32, +} + +#[async_trait] +impl HDWalletOps for HDWallet +where + HDAccount: HDAccountOps + Clone + Send + Sync, +{ + type HDAccount = HDAccount; + + fn coin_type(&self) -> u32 { self.derivation_path.coin_type() } + + fn derivation_path(&self) -> &HDPathToCoin { &self.derivation_path } + + fn gap_limit(&self) -> u32 { self.gap_limit } + + fn account_limit(&self) -> u32 { DEFAULT_ACCOUNT_LIMIT } + + fn default_receiver_chain(&self) -> Bip44Chain { DEFAULT_RECEIVER_CHAIN } + + fn get_accounts_mutex(&self) -> &HDAccountsMutex { &self.accounts } + + async fn get_account(&self, account_id: u32) -> Option { + let accounts = self.get_accounts_mutex().lock().await; + accounts.get(&account_id).cloned() + } + + async fn get_account_mut(&self, account_id: u32) -> Option> { + let accounts = self.get_accounts_mutex().lock().await; + if !accounts.contains_key(&account_id) { + return None; + } + + Some(AsyncMutexGuard::map(accounts, |accounts| { + accounts + .get_mut(&account_id) + .expect("getting an element should never fail due to the checks above") + })) + } + + async fn get_accounts(&self) -> HDAccountsMap { self.get_accounts_mutex().lock().await.clone() } + + async fn get_accounts_mut(&self) -> HDAccountsMut<'_, Self::HDAccount> { self.get_accounts_mutex().lock().await } + + async fn remove_account_if_last(&self, account_id: u32) -> Option { + let mut x = self.get_accounts_mutex().lock().await; + // `BTreeMap::last_entry` is still unstable. + let (last_account_id, _) = x.iter().last()?; + if *last_account_id == account_id { + x.remove(&account_id) + } else { + None + } + } + + async fn get_enabled_address(&self) -> Option<::HDAddress> { + let enabled_address = self.enabled_address; + let account = self.get_account(enabled_address.account_id).await?; + let hd_address_id = HDAddressId { + chain: enabled_address.chain, + address_id: enabled_address.address_id, + }; + let derived = account.derived_addresses().lock().await; + + let address = derived.get(&hd_address_id); + address.cloned() + } +} + +/// Creates and registers a new HD account for a HDWallet. +/// +/// # Parameters +/// - `coin`: A coin that implements [`ExtractExtendedPubkey`]. +/// - `hd_wallet`: The specified HD wallet. +/// - `xpub_extractor`: Optional method for extracting the extended public key. +/// This is especially useful when dealing with hardware wallets. It can +/// allow for the extraction of the extended public key directly from the +/// wallet when needed. +/// - `account_id`: Optional account identifier. +/// +/// # Returns +/// A result containing a mutable reference to the created `HDAccount` if successful. +pub async fn create_new_account<'a, Coin, XPubExtractor, HDWallet, HDAccount>( + coin: &Coin, + hd_wallet: &'a HDWallet, + xpub_extractor: Option, + account_id: Option, +) -> MmResult>, NewAccountCreationError> +where + Coin: ExtractExtendedPubkey + Sync, + HDWallet: HDWalletOps + HDWalletStorageOps + Sync, + XPubExtractor: HDXPubExtractor + Send, + HDAccount: 'a + HDAccountOps + HDAccountStorageOps, +{ + const INIT_ACCOUNT_ID: u32 = 0; + let new_account_id = match account_id { + Some(account_id) => account_id, + None => { + let accounts = hd_wallet.get_accounts_mut().await; + let last_account_id = accounts.iter().last().map(|(account_id, _account)| *account_id); + last_account_id.map_or(INIT_ACCOUNT_ID, |last_id| { + (INIT_ACCOUNT_ID..=last_id) + .find(|id| !accounts.contains_key(id)) + .unwrap_or(last_id + 1) + }) + }, + }; + let max_accounts_number = hd_wallet.account_limit(); + if new_account_id >= max_accounts_number { + return MmError::err(NewAccountCreationError::AccountLimitReached { max_accounts_number }); + } + + let account_child_hardened = true; + let account_child = ChildNumber::new(new_account_id, account_child_hardened) + .map_to_mm(|e| NewAccountCreationError::Internal(e.to_string()))?; + + let account_derivation_path: HDPathToAccount = hd_wallet.derivation_path().derive(account_child)?; + let account_pubkey = coin + .extract_extended_pubkey(xpub_extractor, account_derivation_path.to_derivation_path()) + .await?; + + let new_account = HDAccount::new(new_account_id, account_pubkey, account_derivation_path); + + let accounts = hd_wallet.get_accounts_mut().await; + if accounts.contains_key(&new_account_id) { + let error = format!( + "Account '{}' has been activated while we proceed the 'create_new_account' function", + new_account_id + ); + return MmError::err(NewAccountCreationError::Internal(error)); + } + + hd_wallet.upload_new_account(new_account.to_storage_item()).await?; + + Ok(AsyncMutexGuard::map(accounts, |accounts| { + accounts + .entry(new_account_id) + // the `entry` method should return [`Entry::Vacant`] due to the checks above + .or_insert(new_account) + })) +} + +#[async_trait] +impl HDWalletStorageOps for HDWallet +where + HDAccount: HDAccountOps + HDAccountStorageOps + Clone + Send + Sync, +{ + fn hd_wallet_storage(&self) -> &HDWalletCoinStorage { &self.hd_wallet_storage } +} + +/// Unique identifier for an HD wallet address within the whole wallet context. +#[derive(Copy, Clone, Debug, Deserialize, Serialize)] +pub struct HDPathAccountToAddressId { + pub account_id: u32, + pub chain: Bip44Chain, + pub address_id: u32, +} + +impl Default for HDPathAccountToAddressId { + fn default() -> Self { + HDPathAccountToAddressId { + account_id: 0, + chain: Bip44Chain::External, + address_id: 0, + } + } +} + +impl From for HDPathAccountToAddressId { + fn from(der_path: StandardHDPath) -> Self { + HDPathAccountToAddressId { + account_id: der_path.account_id(), + chain: der_path.chain(), + address_id: der_path.address_id(), + } + } +} + +impl HDPathAccountToAddressId { + pub fn to_derivation_path(&self, path_to_coin: &HDPathToCoin) -> Result> { + let mut account_der_path = path_to_coin.to_derivation_path(); + account_der_path.push(ChildNumber::new(self.account_id, true)?); + account_der_path.push(self.chain.to_child_number()); + account_der_path.push(ChildNumber::new(self.address_id, false)?); + + Ok(account_der_path) + } +} + +pub(crate) mod inner_impl { + use super::*; + use coin_ops::HDWalletCoinOps; + + pub struct NewAddress + where + HDAddress: HDAddressOps, + { + pub hd_address: HDAddress, + pub new_known_addresses_number: u32, + } + + /// Generates a new address without updating a corresponding number of used `hd_account` addresses. + pub async fn generate_new_address_immutable( + coin: &Coin, + hd_account: &HDCoinHDAccount, + chain: Bip44Chain, + ) -> MmResult>, NewAddressDerivingError> + where + Coin: HDWalletCoinOps + ?Sized + Sync, + { + let known_addresses_number = hd_account.known_addresses_number(chain)?; + // Address IDs start from 0, so the `known_addresses_number = last_known_address_id + 1`. + let new_address_id = known_addresses_number; + let max_addresses_number = hd_account.address_limit(); + if new_address_id >= max_addresses_number { + return MmError::err(NewAddressDerivingError::AddressLimitReached { max_addresses_number }); + } + let address = coin.derive_address(hd_account, chain, new_address_id).await?; + Ok(NewAddress { + hd_address: address, + new_known_addresses_number: known_addresses_number + 1, + }) + } +} diff --git a/mm2src/coins/hd_pubkey.rs b/mm2src/coins/hd_wallet/pubkey.rs similarity index 51% rename from mm2src/coins/hd_pubkey.rs rename to mm2src/coins/hd_wallet/pubkey.rs index 667b9bc1f8..0c27030af9 100644 --- a/mm2src/coins/hd_pubkey.rs +++ b/mm2src/coins/hd_wallet/pubkey.rs @@ -1,106 +1,52 @@ -use std::sync::Arc; +use crate::CoinProtocol; -use crate::hd_wallet::NewAccountCreatingError; +use super::*; use async_trait::async_trait; use crypto::hw_rpc_task::HwConnectStatuses; use crypto::trezor::trezor_rpc_task::{TrezorRpcTaskProcessor, TryIntoUserAction}; use crypto::trezor::utxo::IGNORE_XPUB_MAGIC; -use crypto::trezor::{ProcessTrezorResponse, TrezorError, TrezorProcessingError}; -use crypto::{CryptoCtx, CryptoCtxError, DerivationPath, EcdsaCurve, HardwareWalletArc, HwError, HwProcessingError, - XPub, XPubConverter, XpubError}; +use crypto::trezor::ProcessTrezorResponse; +use crypto::trezor::TrezorMessageType; +use crypto::{CryptoCtx, DerivationPath, EcdsaCurve, HardwareWalletArc, XPub, XPubConverter}; use mm2_core::mm_ctx::MmArc; -use mm2_err_handle::prelude::*; -use rpc_task::{RpcTask, RpcTaskError, RpcTaskHandleShared}; +use rpc_task::{RpcTask, RpcTaskHandleShared}; +use std::sync::Arc; const SHOW_PUBKEY_ON_DISPLAY: bool = false; -#[derive(Clone)] -pub enum HDExtractPubkeyError { - HwContextNotInitialized, - CoinDoesntSupportTrezor, - RpcTaskError(RpcTaskError), - HardwareWalletError(HwError), - InvalidXpub(String), - Internal(String), -} - -impl From for HDExtractPubkeyError { - fn from(e: CryptoCtxError) -> Self { HDExtractPubkeyError::Internal(e.to_string()) } -} - -impl From for HDExtractPubkeyError { - fn from(e: TrezorError) -> Self { HDExtractPubkeyError::HardwareWalletError(HwError::from(e)) } -} - -impl From for HDExtractPubkeyError { - fn from(e: HwError) -> Self { HDExtractPubkeyError::HardwareWalletError(e) } -} - -impl From> for HDExtractPubkeyError { - fn from(e: TrezorProcessingError) -> Self { - match e { - TrezorProcessingError::TrezorError(trezor) => HDExtractPubkeyError::from(HwError::from(trezor)), - TrezorProcessingError::ProcessorError(rpc) => HDExtractPubkeyError::RpcTaskError(rpc), - } - } -} - -impl From> for HDExtractPubkeyError { - fn from(e: HwProcessingError) -> Self { - match e { - HwProcessingError::HwError(hw) => HDExtractPubkeyError::from(hw), - HwProcessingError::ProcessorError(rpc) => HDExtractPubkeyError::RpcTaskError(rpc), - HwProcessingError::InternalError(err) => HDExtractPubkeyError::Internal(err), - } - } -} - -impl From for HDExtractPubkeyError { - fn from(e: XpubError) -> Self { HDExtractPubkeyError::InvalidXpub(e.to_string()) } -} - -impl From for NewAccountCreatingError { - fn from(e: HDExtractPubkeyError) -> Self { - match e { - HDExtractPubkeyError::HwContextNotInitialized => NewAccountCreatingError::HwContextNotInitialized, - HDExtractPubkeyError::CoinDoesntSupportTrezor => NewAccountCreatingError::CoinDoesntSupportTrezor, - HDExtractPubkeyError::RpcTaskError(rpc) => NewAccountCreatingError::RpcTaskError(rpc), - HDExtractPubkeyError::HardwareWalletError(hw) => NewAccountCreatingError::HardwareWalletError(hw), - HDExtractPubkeyError::InvalidXpub(xpub) => { - NewAccountCreatingError::HardwareWalletError(HwError::InvalidXpub(xpub)) - }, - HDExtractPubkeyError::Internal(internal) => NewAccountCreatingError::Internal(internal), - } - } -} - +/// This trait should be implemented for coins +/// to support extracting extended public keys from any depth. +/// The extraction can be from either an internal or external wallet. #[async_trait] pub trait ExtractExtendedPubkey { type ExtendedPublicKey; async fn extract_extended_pubkey( &self, - xpub_extractor: &XPubExtractor, + xpub_extractor: Option, derivation_path: DerivationPath, ) -> MmResult where - XPubExtractor: HDXPubExtractor; + XPubExtractor: HDXPubExtractor + Send; } +/// A trait for extracting an extended public key from an external source. #[async_trait] pub trait HDXPubExtractor: Sync { - async fn extract_utxo_xpub( + async fn extract_xpub( &self, - trezor_utxo_coin: String, + trezor_coin: String, derivation_path: DerivationPath, ) -> MmResult; } +/// The task for extracting an extended public key from an external source. pub enum RpcTaskXPubExtractor { Trezor { hw_ctx: HardwareWalletArc, task_handle: RpcTaskHandleShared, statuses: HwConnectStatuses, + trezor_message_type: TrezorMessageType, }, } @@ -110,9 +56,9 @@ where Task: RpcTask, Task::UserAction: TryIntoUserAction + Send, { - async fn extract_utxo_xpub( + async fn extract_xpub( &self, - trezor_utxo_coin: String, + trezor_coin: String, derivation_path: DerivationPath, ) -> MmResult { match self { @@ -120,15 +66,21 @@ where hw_ctx, task_handle, statuses, - } => { - Self::extract_utxo_xpub_from_trezor( - hw_ctx, - task_handle.clone(), - statuses, - trezor_utxo_coin, - derivation_path, - ) - .await + trezor_message_type, + } => match trezor_message_type { + TrezorMessageType::Bitcoin => { + Self::extract_utxo_xpub_from_trezor( + hw_ctx, + task_handle.clone(), + statuses, + trezor_coin, + derivation_path, + ) + .await + }, + TrezorMessageType::Ethereum => { + Self::extract_eth_xpub_from_trezor(hw_ctx, task_handle.clone(), statuses, derivation_path).await + }, }, } } @@ -139,31 +91,31 @@ where Task: RpcTask, Task::UserAction: TryIntoUserAction + Send, { - pub fn new( + pub fn new_trezor_extractor( ctx: &MmArc, task_handle: RpcTaskHandleShared, statuses: HwConnectStatuses, + coin_protocol: CoinProtocol, ) -> MmResult, HDExtractPubkeyError> { let crypto_ctx = CryptoCtx::from_ctx(ctx)?; let hw_ctx = crypto_ctx .hw_ctx() .or_mm_err(|| HDExtractPubkeyError::HwContextNotInitialized)?; + + let trezor_message_type = match coin_protocol { + CoinProtocol::UTXO => TrezorMessageType::Bitcoin, + CoinProtocol::QTUM => TrezorMessageType::Bitcoin, + CoinProtocol::ETH | CoinProtocol::ERC20 { .. } => TrezorMessageType::Ethereum, + _ => return Err(MmError::new(HDExtractPubkeyError::CoinDoesntSupportTrezor)), + }; Ok(RpcTaskXPubExtractor::Trezor { hw_ctx, task_handle, statuses, + trezor_message_type, }) } - /// Constructs an Xpub extractor without checking if the MarketMaker is initialized with a hardware wallet. - pub fn new_unchecked( - ctx: &MmArc, - task_handle: RpcTaskHandleShared, - statuses: HwConnectStatuses, - ) -> XPubExtractorUnchecked> { - XPubExtractorUnchecked(Self::new(ctx, task_handle, statuses)) - } - async fn extract_utxo_xpub_from_trezor( hw_ctx: &HardwareWalletArc, task_handle: RpcTaskHandleShared, @@ -185,12 +137,28 @@ where .await? .process(pubkey_processor.clone()) .await?; - // Despite we pass `IGNORE_XPUB_MAGIC` to the [`TrezorSession::get_public_key`] method, // Trezor sometimes returns pubkeys with magic prefixes like `dgub` prefix for DOGE coin. // So we need to replace the magic prefix manually. XPubConverter::replace_magic_prefix(xpub).mm_err(HDExtractPubkeyError::from) } + + async fn extract_eth_xpub_from_trezor( + hw_ctx: &HardwareWalletArc, + task_handle: RpcTaskHandleShared, + statuses: &HwConnectStatuses, + derivation_path: DerivationPath, + ) -> MmResult { + let pubkey_processor = TrezorRpcTaskProcessor::new(task_handle, statuses.to_trezor_request_statuses()); + let pubkey_processor = Arc::new(pubkey_processor); + let mut trezor_session = hw_ctx.trezor(pubkey_processor.clone()).await?; + trezor_session + .get_eth_public_key(&derivation_path, SHOW_PUBKEY_ON_DISPLAY) + .await? + .process(pubkey_processor) + .await + .mm_err(HDExtractPubkeyError::from) + } } /// This is a wrapper over `XPubExtractor`. The main goal of this structure is to allow construction of an Xpub extractor @@ -203,15 +171,15 @@ impl HDXPubExtractor for XPubExtractorUnchecked where XPubExtractor: HDXPubExtractor + Send + Sync, { - async fn extract_utxo_xpub( + async fn extract_xpub( &self, - trezor_utxo_coin: String, + trezor_coin: String, derivation_path: DerivationPath, ) -> MmResult { self.0 .as_ref() .map_err(Clone::clone)? - .extract_utxo_xpub(trezor_utxo_coin, derivation_path) + .extract_xpub(trezor_coin, derivation_path) .await } } diff --git a/mm2src/coins/hd_wallet_storage/mock_storage.rs b/mm2src/coins/hd_wallet/storage/mock_storage.rs similarity index 90% rename from mm2src/coins/hd_wallet_storage/mock_storage.rs rename to mm2src/coins/hd_wallet/storage/mock_storage.rs index 2fbbc19f4c..8086e58be8 100644 --- a/mm2src/coins/hd_wallet_storage/mock_storage.rs +++ b/mm2src/coins/hd_wallet/storage/mock_storage.rs @@ -1,9 +1,9 @@ -use crate::hd_wallet_storage::{HDAccountStorageItem, HDWalletId, HDWalletStorageInternalOps, HDWalletStorageResult}; +use crate::hd_wallet::{HDAccountStorageItem, HDWalletId, HDWalletStorageInternalOps, HDWalletStorageResult}; use async_trait::async_trait; use mm2_core::mm_ctx::MmArc; #[cfg(test)] use mocktopus::macros::*; -pub struct HDWalletMockStorage; +pub(crate) struct HDWalletMockStorage; #[async_trait] #[cfg_attr(test, mockable)] diff --git a/mm2src/coins/hd_wallet_storage/mod.rs b/mm2src/coins/hd_wallet/storage/mod.rs similarity index 89% rename from mm2src/coins/hd_wallet_storage/mod.rs rename to mm2src/coins/hd_wallet/storage/mod.rs index 2c52cf3895..ced30d5bb8 100644 --- a/mm2src/coins/hd_wallet_storage/mod.rs +++ b/mm2src/coins/hd_wallet/storage/mod.rs @@ -1,6 +1,5 @@ -use crate::hd_wallet::HDWalletCoinOps; use async_trait::async_trait; -use crypto::{CryptoCtx, CryptoCtxError, XPub}; +use crypto::{CryptoCtx, CryptoCtxError, HDPathToCoin, XPub}; use derive_more::Display; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; @@ -16,19 +15,18 @@ use std::ops::Deref; #[cfg(any(test, target_arch = "wasm32"))] mod mock_storage; #[cfg(any(test, target_arch = "wasm32"))] -pub use mock_storage::HDWalletMockStorage; +pub(crate) use mock_storage::HDWalletMockStorage; cfg_wasm32! { use wasm_storage::HDWalletIndexedDbStorage as HDWalletStorageInstance; - - pub use wasm_storage::{HDWalletDb, HDWalletDbLocked}; + pub(crate) use wasm_storage::HDWalletDb; } cfg_native! { use sqlite_storage::HDWalletSqliteStorage as HDWalletStorageInstance; } -pub type HDWalletStorageResult = MmResult; +pub(crate) type HDWalletStorageResult = MmResult; type HDWalletStorageBoxed = Box; #[derive(Debug, Display)] @@ -85,7 +83,7 @@ pub struct HDAccountStorageItem { #[async_trait] #[cfg_attr(test, mockable)] -pub trait HDWalletStorageInternalOps { +pub(crate) trait HDWalletStorageInternalOps { async fn init(ctx: &MmArc) -> HDWalletStorageResult where Self: Sized; @@ -121,69 +119,81 @@ pub trait HDWalletStorageInternalOps { async fn clear_accounts(&self, wallet_id: HDWalletId) -> HDWalletStorageResult<()>; } +/// `HDWalletStorageOps` is a trait that allows us to interact with the storage implementation of the HD wallet. #[async_trait] -pub trait HDWalletCoinWithStorageOps: HDWalletCoinOps { - fn hd_wallet_storage<'a>(&self, hd_wallet: &'a Self::HDWallet) -> &'a HDWalletCoinStorage; +pub trait HDWalletStorageOps { + /// Getter for the HD wallet storage. + fn hd_wallet_storage(&self) -> &HDWalletCoinStorage; - async fn load_all_accounts(&self, hd_wallet: &Self::HDWallet) -> HDWalletStorageResult> { - let storage = self.hd_wallet_storage(hd_wallet); + /// Loads all accounts from the HD wallet storage. + async fn load_all_accounts(&self) -> HDWalletStorageResult> { + let storage = self.hd_wallet_storage(); storage.load_all_accounts().await } - async fn load_account( - &self, - hd_wallet: &Self::HDWallet, - account_id: u32, - ) -> HDWalletStorageResult> { - let storage = self.hd_wallet_storage(hd_wallet); + /// Loads a specific account from the HD wallet storage. + async fn load_account(&self, account_id: u32) -> HDWalletStorageResult> { + let storage = self.hd_wallet_storage(); storage.load_account(account_id).await } + /// Updates the number of external addresses for a specific account. async fn update_external_addresses_number( &self, - hd_wallet: &Self::HDWallet, account_id: u32, new_external_addresses_number: u32, ) -> HDWalletStorageResult<()> { - let storage = self.hd_wallet_storage(hd_wallet); + let storage = self.hd_wallet_storage(); storage .update_external_addresses_number(account_id, new_external_addresses_number) .await } + /// Updates the number of internal addresses for a specific account. async fn update_internal_addresses_number( &self, - hd_wallet: &Self::HDWallet, account_id: u32, new_internal_addresses_number: u32, ) -> HDWalletStorageResult<()> { - let storage = self.hd_wallet_storage(hd_wallet); + let storage = self.hd_wallet_storage(); storage .update_internal_addresses_number(account_id, new_internal_addresses_number) .await } - async fn upload_new_account( - &self, - hd_wallet: &Self::HDWallet, - account_info: HDAccountStorageItem, - ) -> HDWalletStorageResult<()> { - let storage = self.hd_wallet_storage(hd_wallet); + /// Saves new account details to the HD wallet storage. + async fn upload_new_account(&self, account_info: HDAccountStorageItem) -> HDWalletStorageResult<()> { + let storage = self.hd_wallet_storage(); storage.upload_new_account(account_info).await } - async fn clear_accounts(&self, hd_wallet: &Self::HDWallet) -> HDWalletStorageResult<()> { - let storage = self.hd_wallet_storage(hd_wallet); + /// Deletes all accounts from the HD wallet storage. + async fn clear_accounts(&self) -> HDWalletStorageResult<()> { + let storage = self.hd_wallet_storage(); storage.clear_accounts().await } } +/// `HDAccountStorageOps` is a trait that allows us to convert `HDAccountStorageItem` to whatever implements this trait and vice versa. +pub trait HDAccountStorageOps { + /// Converts `HDAccountStorageItem` to whatever implements this trait. + fn try_from_storage_item( + wallet_der_path: &HDPathToCoin, + account_info: &HDAccountStorageItem, + ) -> HDWalletStorageResult + where + Self: Sized; + + /// Converts whatever implements this trait to `HDAccountStorageItem`. + fn to_storage_item(&self) -> HDAccountStorageItem; +} + /// The wrapper over the [`HDWalletStorage::inner`] database implementation. /// It's associated with a specific mm2 user, HD wallet and coin. pub struct HDWalletCoinStorage { coin: String, /// RIPEMD160(SHA256(x)) where x is a pubkey extracted from a Hardware Wallet device or passphrase. - /// This property allows us to store DB items that are unique to each Hardware Wallet device. + /// This property allows us to store DB items that are unique to each Hardware Wallet device or HD wallet. hd_wallet_rmd160: H160, inner: HDWalletStorageBoxed, } @@ -222,7 +232,6 @@ impl HDWalletCoinStorage { }) } - #[cfg(any(test, target_arch = "wasm32"))] pub async fn init_with_rmd160( ctx: &MmArc, coin: String, @@ -291,14 +300,14 @@ mod tests { use primitives::hash::H160; cfg_wasm32! { - use crate::hd_wallet_storage::wasm_storage::get_all_storage_items; + use wasm_storage::get_all_storage_items; use wasm_bindgen_test::*; wasm_bindgen_test_configure!(run_in_browser); } cfg_native! { - use crate::hd_wallet_storage::sqlite_storage::get_all_storage_items; + use sqlite_storage::get_all_storage_items; use common::block_on; } diff --git a/mm2src/coins/hd_wallet_storage/sqlite_storage.rs b/mm2src/coins/hd_wallet/storage/sqlite_storage.rs similarity index 97% rename from mm2src/coins/hd_wallet_storage/sqlite_storage.rs rename to mm2src/coins/hd_wallet/storage/sqlite_storage.rs index 7d015b3d29..898f4c8823 100644 --- a/mm2src/coins/hd_wallet_storage/sqlite_storage.rs +++ b/mm2src/coins/hd_wallet/storage/sqlite_storage.rs @@ -1,7 +1,7 @@ #![allow(deprecated)] // TODO: remove this once rusqlite is >= 0.29 -use crate::hd_wallet_storage::{HDAccountStorageItem, HDWalletId, HDWalletStorageError, HDWalletStorageInternalOps, - HDWalletStorageResult}; +use crate::hd_wallet::{HDAccountStorageItem, HDWalletId, HDWalletStorageError, HDWalletStorageInternalOps, + HDWalletStorageResult}; use async_trait::async_trait; use common::async_blocking; use db_common::owned_named_params; @@ -91,7 +91,7 @@ impl HDWalletId { } #[derive(Clone)] -pub struct HDWalletSqliteStorage { +pub(super) struct HDWalletSqliteStorage { conn: SqliteConnWeak, } @@ -275,7 +275,7 @@ enum UpdatingProperty { /// This function is used in `hd_wallet_storage::tests`. #[cfg(test)] -pub(super) async fn get_all_storage_items(ctx: &MmArc) -> Vec { +pub(crate) async fn get_all_storage_items(ctx: &MmArc) -> Vec { const SELECT_ALL_ACCOUNTS: &str = "SELECT account_id, account_xpub, external_addresses_number, internal_addresses_number FROM hd_account"; diff --git a/mm2src/coins/hd_wallet_storage/wasm_storage.rs b/mm2src/coins/hd_wallet/storage/wasm_storage.rs similarity index 97% rename from mm2src/coins/hd_wallet_storage/wasm_storage.rs rename to mm2src/coins/hd_wallet/storage/wasm_storage.rs index d25363a854..4654474236 100644 --- a/mm2src/coins/hd_wallet_storage/wasm_storage.rs +++ b/mm2src/coins/hd_wallet/storage/wasm_storage.rs @@ -1,5 +1,5 @@ -use crate::hd_wallet_storage::{HDAccountStorageItem, HDWalletId, HDWalletStorageError, HDWalletStorageInternalOps, - HDWalletStorageResult}; +use crate::hd_wallet::{HDAccountStorageItem, HDWalletId, HDWalletStorageError, HDWalletStorageInternalOps, + HDWalletStorageResult}; use crate::CoinsContext; use async_trait::async_trait; use crypto::XPub; @@ -21,7 +21,7 @@ const WALLET_ID_INDEX: &str = "wallet_id"; /// * account_id - HD account id const WALLET_ACCOUNT_ID_INDEX: &str = "wallet_account_id"; -pub type HDWalletDbLocked<'a> = DbLocked<'a, HDWalletDb>; +type HDWalletDbLocked<'a> = DbLocked<'a, HDWalletDb>; impl From for HDWalletStorageError { fn from(e: DbTransactionError) -> Self { @@ -77,7 +77,7 @@ impl From for HDWalletStorageError { /// and one unique multi-index `wallet_account_id` that consists of these four indexes in a row. /// See [`HDAccountTable::on_update_needed`]. #[derive(Deserialize, Serialize)] -pub struct HDAccountTable { +struct HDAccountTable { /// [`HDWalletId::coin`]. /// Non-unique index that is used to fetch/remove items from the storage. coin: String, @@ -135,7 +135,7 @@ impl From for HDAccountStorageItem { } } -pub struct HDWalletDb { +pub(crate) struct HDWalletDb { pub(crate) inner: IndexedDb, } @@ -154,7 +154,7 @@ impl DbInstance for HDWalletDb { } /// The wrapper over the [`CoinsContext::hd_wallet_db`] weak pointer. -pub struct HDWalletIndexedDbStorage { +pub(super) struct HDWalletIndexedDbStorage { db: WeakDb, } diff --git a/mm2src/coins/hd_wallet/wallet_ops.rs b/mm2src/coins/hd_wallet/wallet_ops.rs new file mode 100644 index 0000000000..65b1de223e --- /dev/null +++ b/mm2src/coins/hd_wallet/wallet_ops.rs @@ -0,0 +1,53 @@ +use super::{HDAccountMut, HDAccountOps, HDAccountsMap, HDAccountsMut, HDAccountsMutex}; +use async_trait::async_trait; +use crypto::{Bip44Chain, HDPathToCoin}; + +/// `HDWalletOps`: Operations that should be implemented for Structs or any type that represents HD wallets. +#[async_trait] +pub trait HDWalletOps { + /// Any type that represents a Hierarchical Deterministic (HD) wallet account. + type HDAccount: HDAccountOps + Clone + Send + Sync; + + /// Returns the coin type associated with this HD Wallet. + /// + /// This method should be implemented to fetch the coin type as specified in the wallet's BIP44 derivation path. + /// For example, in the derivation path `m/44'/0'/0'/0`, the coin type would be the third level `0'` + /// (representing Bitcoin). + fn coin_type(&self) -> u32; + + /// Returns the derivation path associated with this HD Wallet. This is the path used to derive the accounts. + fn derivation_path(&self) -> &HDPathToCoin; + + /// Fetches the gap limit associated with this HD Wallet. + /// Gap limit is the maximum number of consecutive unused addresses in an account + /// that should be checked before considering the wallet as having no more funds. + fn gap_limit(&self) -> u32; + + /// Returns the limit on the number of accounts that can be added to the wallet. + fn account_limit(&self) -> u32; + + /// Returns the default BIP44 chain for receiver addresses. + fn default_receiver_chain(&self) -> Bip44Chain; + + /// Returns a mutex that can be used to access the accounts. + fn get_accounts_mutex(&self) -> &HDAccountsMutex; + + /// Fetches an account based on its ID. This method will return `None` if the account is not activated. + async fn get_account(&self, account_id: u32) -> Option; + + /// Similar to `get_account`, but provides a mutable reference. + async fn get_account_mut(&self, account_id: u32) -> Option>; + + /// Fetches all accounts in the wallet. + async fn get_accounts(&self) -> HDAccountsMap; + + /// Similar to `get_accounts`, but provides a mutable reference to the accounts. + async fn get_accounts_mut(&self) -> HDAccountsMut<'_, Self::HDAccount>; + + /// Attempts to remove an account only if it's the last in the set. + /// This method will return the removed account if successful or `None` otherwise. + async fn remove_account_if_last(&self, account_id: u32) -> Option; + + /// Returns an address that's currently enabled for single-address operations, such as swaps. + async fn get_enabled_address(&self) -> Option<::HDAddress>; +} diff --git a/mm2src/coins/hd_wallet/withdraw_ops.rs b/mm2src/coins/hd_wallet/withdraw_ops.rs new file mode 100644 index 0000000000..7eeafbcd12 --- /dev/null +++ b/mm2src/coins/hd_wallet/withdraw_ops.rs @@ -0,0 +1,92 @@ +use super::{HDPathAccountToAddressId, HDWalletOps, HDWithdrawError}; +use crate::hd_wallet::{HDAccountOps, HDAddressOps, HDCoinAddress, HDCoinPubKey, HDWalletCoinOps}; +use async_trait::async_trait; +use bip32::DerivationPath; +use crypto::{StandardHDPath, StandardHDPathError}; +use mm2_err_handle::prelude::*; +use std::str::FromStr; + +/// Represents the source of the funds for a withdrawal operation. +#[derive(Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum WithdrawFrom { + /// The address id of the sender address which is specified by the account id, chain, and address id. + AddressId(HDPathAccountToAddressId), + /// The derivation path of the sender address in the BIP-44 format. + /// + /// IMPORTANT: Don't use `Bip44DerivationPath` or `RpcDerivationPath` because if there is an error in the path, + /// `serde::Deserialize` returns "data did not match any variant of untagged enum WithdrawFrom". + /// It's better to show the user an informative error. + DerivationPath { derivation_path: String }, +} + +impl WithdrawFrom { + #[allow(clippy::result_large_err)] + pub fn to_address_path(&self, expected_coin_type: u32) -> MmResult { + match self { + WithdrawFrom::AddressId(address_id) => Ok(*address_id), + WithdrawFrom::DerivationPath { derivation_path } => { + let derivation_path = StandardHDPath::from_str(derivation_path) + .map_to_mm(StandardHDPathError::from) + .mm_err(|e| HDWithdrawError::UnexpectedFromAddress(e.to_string()))?; + let coin_type = derivation_path.coin_type(); + if coin_type != expected_coin_type { + let error = format!( + "Derivation path '{}' must have '{}' coin type", + derivation_path, expected_coin_type + ); + return MmError::err(HDWithdrawError::UnexpectedFromAddress(error)); + } + Ok(HDPathAccountToAddressId::from(derivation_path)) + }, + } + } +} + +/// Contains the details of the sender address for a withdraw operation. +pub struct WithdrawSenderAddress { + pub(crate) address: Address, + pub(crate) pubkey: Pubkey, + pub(crate) derivation_path: Option, +} + +/// `HDCoinWithdrawOps`: Operations that should be implemented for coins to support withdraw from HD wallets. +#[async_trait] +pub trait HDCoinWithdrawOps: HDWalletCoinOps { + /// Fetches the sender address for a withdraw operation. + /// This is the address from which the funds will be withdrawn. + async fn get_withdraw_hd_sender( + &self, + hd_wallet: &Self::HDWallet, + from: &WithdrawFrom, + ) -> MmResult, HDCoinPubKey>, HDWithdrawError> { + let HDPathAccountToAddressId { + account_id, + chain, + address_id, + } = from.to_address_path(hd_wallet.coin_type())?; + + let hd_account = hd_wallet + .get_account(account_id) + .await + .or_mm_err(|| HDWithdrawError::UnknownAccount { account_id })?; + + let is_address_activated = hd_account + .is_address_activated(chain, address_id) + // If [`HDWalletCoinOps::derive_address`] succeeds, [`HDAccountOps::is_address_activated`] shouldn't fails with an `InvalidBip44ChainError`. + .mm_err(|e| HDWithdrawError::InternalError(e.to_string()))?; + + let hd_address = self.derive_address(&hd_account, chain, address_id).await?; + let address = hd_address.address(); + if !is_address_activated { + let error = format!("'{}' address is not activated", address); + return MmError::err(HDWithdrawError::UnexpectedFromAddress(error)); + } + + Ok(WithdrawSenderAddress { + address, + pubkey: hd_address.pubkey(), + derivation_path: Some(hd_address.derivation_path().clone()), + }) + } +} diff --git a/mm2src/coins/lightning.rs b/mm2src/coins/lightning.rs index 4698a595ac..f6ccf0363e 100644 --- a/mm2src/coins/lightning.rs +++ b/mm2src/coins/lightning.rs @@ -539,26 +539,24 @@ impl LightningCoin { Ok(PaymentInstructions::Lightning(invoice)) } - fn spend_swap_payment(&self, spend_payment_args: SpendPaymentArgs<'_>) -> TransactionFut { - let payment_hash = try_tx_fus!(payment_hash_from_slice(spend_payment_args.other_payment_tx)); + async fn spend_swap_payment(&self, spend_payment_args: SpendPaymentArgs<'_>) -> TransactionResult { + let payment_hash = try_tx_s!(payment_hash_from_slice(spend_payment_args.other_payment_tx)); + let mut preimage = [b' '; 32]; preimage.copy_from_slice(spend_payment_args.secret); + drop_mutability!(preimage); - let coin = self.clone(); - let fut = async move { - let payment_preimage = PaymentPreimage(preimage); - coin.channel_manager.claim_funds(payment_preimage); - coin.db - .update_payment_preimage_in_db(payment_hash, payment_preimage) - .await - .error_log_with_msg(&format!( - "Unable to update payment {} information in DB with preimage: {}!", - hex::encode(payment_hash.0), - hex::encode(preimage) - )); - Ok(TransactionEnum::LightningPayment(payment_hash)) - }; - Box::new(fut.boxed().compat()) + let payment_preimage = PaymentPreimage(preimage); + self.channel_manager.claim_funds(payment_preimage); + self.db + .update_payment_preimage_in_db(payment_hash, payment_preimage) + .await + .error_log_with_msg(&format!( + "Unable to update payment {} information in DB with preimage: {}!", + hex::encode(payment_hash.0), + hex::encode(preimage) + )); + Ok(TransactionEnum::LightningPayment(payment_hash)) } fn validate_swap_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { @@ -652,13 +650,19 @@ impl SwapOps for LightningCoin { } #[inline] - fn send_maker_spends_taker_payment(&self, maker_spends_payment_args: SpendPaymentArgs<'_>) -> TransactionFut { - self.spend_swap_payment(maker_spends_payment_args) + async fn send_maker_spends_taker_payment( + &self, + maker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { + self.spend_swap_payment(maker_spends_payment_args).await } #[inline] - fn send_taker_spends_maker_payment(&self, taker_spends_payment_args: SpendPaymentArgs<'_>) -> TransactionFut { - self.spend_swap_payment(taker_spends_payment_args) + async fn send_taker_spends_maker_payment( + &self, + taker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { + self.spend_swap_payment(taker_spends_payment_args).await } async fn send_taker_refunds_payment( @@ -1034,7 +1038,7 @@ impl MarketCoinOps for LightningCoin { fn my_address(&self) -> MmResult { Ok(self.my_node_id()) } - fn get_public_key(&self) -> Result> { Ok(self.my_node_id()) } + async fn get_public_key(&self) -> Result> { Ok(self.my_node_id()) } fn sign_message_hash(&self, message: &str) -> Option<[u8; 32]> { let mut _message_prefix = self.conf.sign_message_prefix.clone()?; @@ -1256,6 +1260,8 @@ impl MarketCoinOps for LightningCoin { // Todo: Equals to min_tx_amount for now (1 satoshi), should change this later // Todo: doesn't take routing fees into account too, There is no way to know the route to the other side of the swap when placing the order, need to find a workaround for this fn min_trading_vol(&self) -> MmNumber { self.min_tx_amount().into() } + + fn is_trezor(&self) -> bool { self.platform.coin.is_trezor() } } #[derive(Deserialize, Serialize)] diff --git a/mm2src/coins/lightning/ln_events.rs b/mm2src/coins/lightning/ln_events.rs index aab33e9088..3a761cc2b3 100644 --- a/mm2src/coins/lightning/ln_events.rs +++ b/mm2src/coins/lightning/ln_events.rs @@ -189,7 +189,7 @@ pub enum SignFundingTransactionError { } // Generates the raw funding transaction with one output equal to the channel value. -fn sign_funding_transaction( +async fn sign_funding_transaction( uuid: Uuid, output_script_pubkey: &Script, platform: Arc, @@ -213,6 +213,7 @@ fn sign_funding_transaction( .as_ref() .derivation_method .single_addr_or_err() + .await .map_err(|e| SignFundingTransactionError::Internal(e.to_string()))?; let key_pair = coin .as_ref() @@ -221,7 +222,7 @@ fn sign_funding_transaction( .map_err(|e| SignFundingTransactionError::Internal(e.to_string()))?; let prev_script = coin - .script_for_address(my_address) + .script_for_address(&my_address) .map_err(|e| SignFundingTransactionError::Internal(e.to_string()))?; let signed = sign_tx( unsigned, @@ -300,30 +301,32 @@ impl LightningEventHandler { "Handling FundingGenerationReady event for channel with uuid: {} with: {}", uuid, counterparty_node_id ); - let funding_tx = match sign_funding_transaction(uuid, &output_script, self.platform.clone()) { - Ok(tx) => tx, - Err(e) => { - error!( - "Error generating funding transaction for channel with uuid {}: {}", - uuid, - e.to_string() - ); - return; - }, - }; - let funding_txid = funding_tx.txid(); - // Give the funding transaction back to LDK for opening the channel. - if let Err(e) = - self.channel_manager - .funding_transaction_generated(&temporary_channel_id, &counterparty_node_id, funding_tx) - { - error!("{:?}", e); - return; - } + + let channel_manager = self.channel_manager.clone(); let platform = self.platform.clone(); let db = self.db.clone(); let fut = async move { + let funding_tx = match sign_funding_transaction(uuid, &output_script, platform.clone()).await { + Ok(tx) => tx, + Err(e) => { + error!( + "Error generating funding transaction for channel with uuid {}: {}", + uuid, + e.to_string() + ); + return; + }, + }; + let funding_txid = funding_tx.txid(); + // Give the funding transaction back to LDK for opening the channel. + if let Err(e) = + channel_manager.funding_transaction_generated(&temporary_channel_id, &counterparty_node_id, funding_tx) + { + error!("{:?}", e); + return; + } + let best_block_height = platform.best_block_height(); db.add_funding_tx_to_db( uuid, @@ -518,20 +521,19 @@ impl LightningEventHandler { return; } - // Todo: add support for Hardware wallets for funding transactions and spending spendable outputs (channel closing transactions) - let my_address = match self.platform.coin.as_ref().derivation_method.single_addr_or_err() { - Ok(addr) => addr.clone(), - Err(e) => { - error!("{}", e); - return; - }, - }; - let platform = self.platform.clone(); let db = self.db.clone(); let keys_manager = self.keys_manager.clone(); let fut = async move { + // Todo: add support for HD and Hardware wallets for funding transactions and spending spendable outputs (channel closing transactions) + let my_address = match platform.coin.as_ref().derivation_method.single_addr_or_err().await { + Ok(addr) => addr.clone(), + Err(e) => { + error!("{}", e); + return; + }, + }; let change_destination_script = match Builder::build_p2wpkh(my_address.hash()) { Ok(script) => script.to_bytes().take().into(), Err(err) => { diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 6ba9b7ac69..8123681d78 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -49,8 +49,9 @@ use common::executor::{abortable_queue::{AbortableQueue, WeakSpawner}, AbortSettings, AbortedError, SpawnAbortable, SpawnFuture}; use common::log::{warn, LogOnError}; use common::{calc_total_pages, now_sec, ten, HttpStatusCode}; -use crypto::{derive_secp256k1_secret, Bip32Error, CryptoCtx, CryptoCtxError, DerivationPath, GlobalHDAccountArc, - HwRpcError, KeyPairPolicy, Secp256k1Secret, StandardHDCoinAddress, StandardHDPathToCoin, WithHwRpcError}; +use crypto::{derive_secp256k1_secret, Bip32Error, Bip44Chain, CryptoCtx, CryptoCtxError, DerivationPath, + GlobalHDAccountArc, HDPathToCoin, HwRpcError, KeyPairPolicy, RpcDerivationPath, + Secp256k1ExtendedPublicKey, Secp256k1Secret, WithHwRpcError}; use derive_more::Display; use enum_derives::{EnumFromStringify, EnumFromTrait}; use ethereum_types::H256; @@ -74,15 +75,15 @@ use serde_json::{self as json, Value as Json}; use std::cmp::Ordering; use std::collections::hash_map::{HashMap, RawEntryMut}; use std::collections::HashSet; -use std::fmt; use std::future::Future as Future03; use std::num::{NonZeroUsize, TryFromIntError}; -use std::ops::{Add, Deref}; +use std::ops::{Add, AddAssign, Deref}; use std::str::FromStr; use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering as AtomicOrdering; use std::sync::Arc; use std::time::Duration; +use std::{fmt, iter}; use utxo_signer::with_key_pair::UtxoSignWithKeyPairError; use zcash_primitives::transaction::Transaction as ZTransaction; @@ -99,7 +100,7 @@ cfg_native! { cfg_wasm32! { use ethereum_types::{H264 as EthH264, H520 as EthH520}; - use hd_wallet_storage::HDWalletDb; + use hd_wallet::HDWalletDb; use mm2_db::indexed_db::{ConstructibleDb, DbLocked, SharedDb}; use tx_history_storage::wasm::{clear_tx_history, load_tx_history, save_tx_history, TxHistoryDb}; pub type TxHistoryDbLocked<'a> = DbLocked<'a, TxHistoryDb>; @@ -204,11 +205,13 @@ macro_rules! ok_or_continue_after_sleep { } pub mod coin_balance; +use coin_balance::{AddressBalanceStatus, HDAddressBalance, HDWalletBalanceOps}; + pub mod lp_price; pub mod watcher_common; pub mod coin_errors; -use coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentFut}; +use coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentResult}; #[doc(hidden)] #[cfg(test)] @@ -220,13 +223,11 @@ use eth::{eth_coin_from_conf_and_request, get_eth_address, EthCoin, EthGasDetail GetEthAddressError, SignedEthTx}; use ethereum_types::U256; -pub mod hd_confirm_address; -pub mod hd_pubkey; - pub mod hd_wallet; -use hd_wallet::{HDAccountAddressId, HDAddress}; +use hd_wallet::{AccountUpdatingError, AddressDerivingError, HDAccountOps, HDAddressId, HDAddressOps, HDCoinAddress, + HDCoinHDAccount, HDExtractPubkeyError, HDPathAccountToAddressId, HDWalletAddress, HDWalletCoinOps, + HDWalletOps, HDWithdrawError, HDXPubExtractor, WithdrawFrom, WithdrawSenderAddress}; -pub mod hd_wallet_storage; #[cfg(not(target_arch = "wasm32"))] pub mod lightning; #[cfg_attr(target_arch = "wasm32", allow(dead_code, unused_imports))] pub mod my_tx_history_v2; @@ -284,19 +285,16 @@ use utxo::qtum::{self, qtum_coin_with_policy, Qrc20AddressError, QtumCoin, QtumD use utxo::rpc_clients::UtxoRpcError; use utxo::slp::SlpToken; use utxo::slp::{slp_addr_from_pubkey_str, SlpFeeDetails}; -use utxo::utxo_common::big_decimal_from_sat_unsigned; +use utxo::utxo_common::{big_decimal_from_sat_unsigned, payment_script, WaitForOutputSpendErr}; use utxo::utxo_standard::{utxo_standard_coin_with_policy, UtxoStandardCoin}; -use utxo::UtxoActivationParams; -use utxo::{BlockchainNetwork, GenerateTxError, UtxoFeeDetails, UtxoTx}; +use utxo::{swap_proto_v2_scripts, BlockchainNetwork, GenerateTxError, UtxoActivationParams, UtxoFeeDetails, UtxoTx}; pub mod nft; use nft::nft_errors::GetNftInfoError; use script::Script; pub mod z_coin; -use crate::coin_errors::ValidatePaymentResult; -use crate::utxo::swap_proto_v2_scripts; -use crate::utxo::utxo_common::{payment_script, WaitForOutputSpendErr}; +use crate::coin_balance::{BalanceObjectOps, HDWalletBalanceObject}; use z_coin::{ZCoin, ZcoinProtocolInfo}; pub type TransactionFut = Box + Send>; @@ -502,7 +500,7 @@ pub struct SignRawTransactionRequest { pub struct MyAddressReq { coin: String, #[serde(default)] - path_to_address: StandardHDCoinAddress, + path_to_address: HDPathAccountToAddressId, } #[derive(Debug, Serialize)] @@ -549,7 +547,7 @@ impl Serialize for PrivKeyPolicyNotAllowed { } } -#[derive(Clone, Debug, Display, PartialEq, Serialize)] +#[derive(Clone, Debug, Deserialize, Display, PartialEq, Serialize)] pub enum UnexpectedDerivationMethod { #[display(fmt = "Expected 'SingleAddress' derivation method")] ExpectedSingleAddress, @@ -1046,9 +1044,15 @@ pub trait SwapOps { fn send_taker_payment(&self, taker_payment_args: SendPaymentArgs<'_>) -> TransactionFut; - fn send_maker_spends_taker_payment(&self, maker_spends_payment_args: SpendPaymentArgs<'_>) -> TransactionFut; + async fn send_maker_spends_taker_payment( + &self, + maker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult; - fn send_taker_spends_maker_payment(&self, taker_spends_payment_args: SpendPaymentArgs<'_>) -> TransactionFut; + async fn send_taker_spends_maker_payment( + &self, + taker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult; async fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult; @@ -1438,6 +1442,7 @@ pub trait ToBytes { } /// Defines associated types specific to each coin (Pubkey, Address, etc.) +#[async_trait] pub trait ParseCoinAssocTypes { type Address: Send + Sync + fmt::Display; type AddressParseError: fmt::Debug + Send + fmt::Display; @@ -1450,7 +1455,7 @@ pub trait ParseCoinAssocTypes { type Sig: ToBytes + Send + Sync; type SigParseError: fmt::Debug + Send + fmt::Display; - fn my_addr(&self) -> &Self::Address; + async fn my_addr(&self) -> Self::Address; fn parse_address(&self, address: &str) -> Result; @@ -1847,7 +1852,7 @@ pub trait MarketCoinOps { fn my_address(&self) -> MmResult; - fn get_public_key(&self) -> Result>; + async fn get_public_key(&self) -> Result>; fn sign_message_hash(&self, _message: &str) -> Option<[u8; 32]>; @@ -1902,6 +1907,8 @@ pub trait MarketCoinOps { fn min_trading_vol(&self) -> MmNumber; fn is_privacy(&self) -> bool { false } + + fn is_trezor(&self) -> bool; } #[derive(Clone, Debug, Deserialize, PartialEq)] @@ -1929,22 +1936,6 @@ pub enum WithdrawFee { }, } -pub struct WithdrawSenderAddress { - address: Address, - pubkey: Pubkey, - derivation_path: Option, -} - -impl From> for WithdrawSenderAddress { - fn from(addr: HDAddress) -> Self { - WithdrawSenderAddress { - address: addr.address, - pubkey: addr.pubkey, - derivation_path: Some(addr.derivation_path), - } - } -} - /// Rename to `GetWithdrawSenderAddresses` when withdraw supports multiple `from` addresses. #[async_trait] pub trait GetWithdrawSenderAddress { @@ -1957,19 +1948,6 @@ pub trait GetWithdrawSenderAddress { ) -> MmResult, WithdrawError>; } -#[derive(Clone, Deserialize, Serialize)] -#[serde(untagged)] -pub enum WithdrawFrom { - AddressId(HDAccountAddressId), - /// Don't use `Bip44DerivationPath` or `RpcDerivationPath` because if there is an error in the path, - /// `serde::Deserialize` returns "data did not match any variant of untagged enum WithdrawFrom". - /// It's better to show the user an informative error. - DerivationPath { - derivation_path: String, - }, - HDWalletAddress(StandardHDCoinAddress), -} - #[derive(Clone, Deserialize)] pub struct WithdrawRequest { coin: String, @@ -2264,12 +2242,38 @@ pub struct TradeFee { pub paid_from_trading_vol: bool, } +/// A type alias for a HashMap where the key is a String representing the coin/token ticker, +/// and the value is a `CoinBalance` struct representing the balance of that coin/token. +/// This is used to represent the balance of a wallet or account for multiple coins/tokens. +pub type CoinBalanceMap = HashMap; + +impl BalanceObjectOps for CoinBalanceMap { + fn new() -> Self { HashMap::new() } + + fn add(&mut self, other: Self) { + for (ticker, balance) in other { + let total_balance = self.entry(ticker).or_insert_with(CoinBalance::default); + *total_balance += balance; + } + } + + fn get_total_for_ticker(&self, ticker: &str) -> Option { self.get(ticker).map(|b| b.get_total()) } +} + #[derive(Clone, Debug, Default, PartialEq, PartialOrd, Serialize)] pub struct CoinBalance { pub spendable: BigDecimal, pub unspendable: BigDecimal, } +impl BalanceObjectOps for CoinBalance { + fn new() -> Self { CoinBalance::default() } + + fn add(&mut self, other: Self) { *self += other; } + + fn get_total_for_ticker(&self, _ticker: &str) -> Option { Some(self.get_total()) } +} + impl CoinBalance { pub fn new(spendable: BigDecimal) -> CoinBalance { CoinBalance { @@ -2294,6 +2298,13 @@ impl Add for CoinBalance { } } +impl AddAssign for CoinBalance { + fn add_assign(&mut self, rhs: Self) { + self.spendable += rhs.spendable; + self.unspendable += rhs.unspendable; + } +} + /// The approximation is needed to cover the dynamic miner fee changing during a swap. #[derive(Clone, Copy, Debug)] pub enum FeeApproxStage { @@ -2443,6 +2454,23 @@ pub enum GetNonZeroBalance { BalanceIsZero, } +impl From for BalanceError { + fn from(e: AddressDerivingError) -> Self { BalanceError::Internal(e.to_string()) } +} + +impl From for BalanceError { + fn from(e: AccountUpdatingError) -> Self { + let error = e.to_string(); + match e { + AccountUpdatingError::AddressLimitReached { .. } | AccountUpdatingError::InvalidBip44Chain(_) => { + // Account updating is expected to be called after `address_id` and `chain` validation. + BalanceError::Internal(format!("Unexpected internal error: {}", error)) + }, + AccountUpdatingError::WalletStorageError(_) => BalanceError::WalletStorageError(error), + } + } +} + impl From for GetNonZeroBalance { fn from(e: BalanceError) -> Self { GetNonZeroBalance::MyBalanceError(e) } } @@ -2814,6 +2842,17 @@ impl HttpStatusCode for WithdrawError { } } +impl From for WithdrawError { + fn from(e: AddressDerivingError) -> Self { + match e { + AddressDerivingError::InvalidBip44Chain { .. } | AddressDerivingError::Bip32Error(_) => { + WithdrawError::UnexpectedFromAddress(e.to_string()) + }, + AddressDerivingError::Internal(internal) => WithdrawError::InternalError(internal), + } + } +} + impl From for WithdrawError { fn from(e: BalanceError) -> Self { match e { @@ -2833,6 +2872,17 @@ impl From for WithdrawError { } } +impl From for WithdrawError { + fn from(e: HDWithdrawError) -> Self { + match e { + HDWithdrawError::UnexpectedFromAddress(e) => WithdrawError::UnexpectedFromAddress(e), + HDWithdrawError::UnknownAccount { account_id } => WithdrawError::UnknownAccount { account_id }, + HDWithdrawError::AddressDerivingError(e) => e.into(), + HDWithdrawError::InternalError(e) => WithdrawError::InternalError(e), + } + } +} + impl From for WithdrawError { fn from(e: UtxoSignWithKeyPairError) -> Self { let error = format!("Error signing: {}", e); @@ -2869,7 +2919,7 @@ impl From for WithdrawError { impl From for WithdrawError { fn from(e: Bip32Error) -> Self { let error = format!("Error deriving key: {}", e); - WithdrawError::InternalError(error) + WithdrawError::UnexpectedFromAddress(error) } } @@ -3623,19 +3673,49 @@ impl Default for PrivKeyActivationPolicy { fn default() -> Self { PrivKeyActivationPolicy::ContextPrivKey } } +impl PrivKeyActivationPolicy { + pub fn is_hw_policy(&self) -> bool { matches!(self, PrivKeyActivationPolicy::Trezor) } +} + +/// Enum representing various private key management policies. +/// +/// This enum defines the various ways in which private keys can be managed +/// or sourced within the system, whether it's from a local software-based HD Wallet, +/// a hardware device like Trezor, or even external sources like Metamask. #[derive(Clone, Debug)] pub enum PrivKeyPolicy { + /// The legacy private key policy. + /// + /// This policy corresponds to a one-to-one mapping of private keys to addresses. + /// In this scheme, only a single key and corresponding address is activated per coin, + /// without any hierarchical deterministic derivation. Iguana(T), + /// The HD Wallet private key policy. + /// + /// This variant uses a BIP44 derivation path up to the coin level + /// and contains the necessary information to manage and derive + /// keys using an HD Wallet scheme. HDWallet { - /// Derivation path of the coin. - /// This derivation path consists of `purpose` and `coin_type` only - /// where the full `BIP44` address has the following structure: + /// Derivation path up to coin. + /// + /// Represents the first two segments of the BIP44 derivation path: `purpose` and `coin_type`. + /// A full BIP44 address is structured as: /// `m/purpose'/coin_type'/account'/change/address_index`. - derivation_path: StandardHDPathToCoin, + path_to_coin: HDPathToCoin, + /// The key that's currently activated and in use for this HD Wallet policy. activated_key: T, + /// Extended private key based on the secp256k1 elliptic curve cryptography scheme. bip39_secp_priv_key: ExtendedPrivateKey, }, + /// The Trezor hardware wallet private key policy. + /// + /// Details about how the keys are managed with the Trezor device + /// are abstracted away and are not directly managed by this policy. Trezor, + /// The Metamask private key policy, specific to the WASM target architecture. + /// + /// This variant encapsulates details about how keys are managed when interfacing + /// with the Metamask extension, especially within web-based contexts. #[cfg(target_arch = "wasm32")] Metamask(EthMetamaskPolicy), } @@ -3695,17 +3775,22 @@ impl PrivKeyPolicy { }) } - fn derivation_path(&self) -> Option<&StandardHDPathToCoin> { + fn path_to_coin(&self) -> Option<&HDPathToCoin> { match self { - PrivKeyPolicy::HDWallet { derivation_path, .. } => Some(derivation_path), - PrivKeyPolicy::Iguana(_) | PrivKeyPolicy::Trezor => None, + PrivKeyPolicy::HDWallet { + path_to_coin: derivation_path, + .. + } => Some(derivation_path), + PrivKeyPolicy::Trezor => None, + PrivKeyPolicy::Iguana(_) => None, #[cfg(target_arch = "wasm32")] PrivKeyPolicy::Metamask(_) => None, } } - fn derivation_path_or_err(&self) -> Result<&StandardHDPathToCoin, MmError> { - self.derivation_path().or_mm_err(|| { + // Todo: this can be removed after the HDWallet is fully implemented for all protocols + fn path_to_coin_or_err(&self) -> Result<&HDPathToCoin, MmError> { + self.path_to_coin().or_mm_err(|| { PrivKeyPolicyNotAllowed::UnsupportedMethod( "`derivation_path_or_err` is supported only for `PrivKeyPolicy::HDWallet`".to_string(), ) @@ -3714,13 +3799,56 @@ impl PrivKeyPolicy { fn hd_wallet_derived_priv_key_or_err( &self, - path_to_address: &StandardHDCoinAddress, + derivation_path: &DerivationPath, ) -> Result> { let bip39_secp_priv_key = self.bip39_secp_priv_key_or_err()?; - let derivation_path = self.derivation_path_or_err()?; - derive_secp256k1_secret(bip39_secp_priv_key.clone(), derivation_path, path_to_address) + derive_secp256k1_secret(bip39_secp_priv_key.clone(), derivation_path) .mm_err(|e| PrivKeyPolicyNotAllowed::InternalError(e.to_string())) } + + fn is_trezor(&self) -> bool { matches!(self, PrivKeyPolicy::Trezor) } +} + +/// 'CoinWithPrivKeyPolicy' trait is used to get the private key policy of a coin. +pub trait CoinWithPrivKeyPolicy { + /// The type of the key pair used by the coin. + type KeyPair; + + /// Returns the private key policy of the coin. + fn priv_key_policy(&self) -> &PrivKeyPolicy; +} + +/// A common function to get the extended public key for a certain coin and derivation path. +pub async fn extract_extended_pubkey_impl( + coin: &Coin, + xpub_extractor: Option, + derivation_path: DerivationPath, +) -> MmResult +where + XPubExtractor: HDXPubExtractor + Send, + Coin: HDWalletCoinOps + CoinWithPrivKeyPolicy, +{ + match xpub_extractor { + Some(xpub_extractor) => { + let trezor_coin = coin.trezor_coin()?; + let xpub = xpub_extractor.extract_xpub(trezor_coin, derivation_path).await?; + Secp256k1ExtendedPublicKey::from_str(&xpub).map_to_mm(|e| HDExtractPubkeyError::InvalidXpub(e.to_string())) + }, + None => { + let mut priv_key = coin + .priv_key_policy() + .bip39_secp_priv_key_or_err() + .mm_err(|e| HDExtractPubkeyError::Internal(e.to_string()))? + .clone(); + for child in derivation_path { + priv_key = priv_key + .derive_child(child) + .map_to_mm(|e| HDExtractPubkeyError::Internal(e.to_string()))?; + } + drop_mutability!(priv_key); + Ok(priv_key.public_key()) + }, + } } #[derive(Clone)] @@ -3745,22 +3873,55 @@ impl PrivKeyBuildPolicy { } } +/// Serializable struct for compatibility with the discontinued DerivationMethod struct +#[derive(Clone, Debug, Serialize)] +#[serde(tag = "type", content = "data")] +pub enum DerivationMethodResponse { + /// Legacy iguana's privkey derivation, used by default + Iguana, + /// HD wallet derivation path, String is temporary here + HDWallet(String), +} + +/// Enum representing methods for deriving cryptographic addresses. +/// +/// This enum distinguishes between two primary strategies for address generation: +/// 1. A static, single address approach. +/// 2. A hierarchical deterministic (HD) wallet that can derive multiple addresses. #[derive(Debug)] -pub enum DerivationMethod { +pub enum DerivationMethod +where + HDWallet: HDWalletOps, + HDWalletAddress: Into
, +{ + /// Represents the use of a single, static address for transactions and operations. SingleAddress(Address), + /// Represents the use of an HD wallet for deriving multiple addresses. + /// + /// The encapsulated HD wallet should be capable of operations like + /// getting the globally enabled address, and more, as defined by the + /// [`HDWalletOps`] trait. HDWallet(HDWallet), } -impl DerivationMethod { - pub fn single_addr(&self) -> Option<&Address> { +impl DerivationMethod +where + Address: Clone, + HDWallet: HDWalletOps, + HDWalletAddress: Into
, +{ + pub async fn single_addr(&self) -> Option
{ match self { - DerivationMethod::SingleAddress(my_address) => Some(my_address), - DerivationMethod::HDWallet(_) => None, + DerivationMethod::SingleAddress(my_address) => Some(my_address.clone()), + DerivationMethod::HDWallet(hd_wallet) => { + hd_wallet.get_enabled_address().await.map(|addr| addr.address().into()) + }, } } - pub fn single_addr_or_err(&self) -> MmResult<&Address, UnexpectedDerivationMethod> { + pub async fn single_addr_or_err(&self) -> MmResult { self.single_addr() + .await .or_mm_err(|| UnexpectedDerivationMethod::ExpectedSingleAddress) } @@ -3779,19 +3940,93 @@ impl DerivationMethod { /// # Panic /// /// Panic if the address mode is [`DerivationMethod::HDWallet`]. - pub fn unwrap_single_addr(&self) -> &Address { self.single_addr_or_err().unwrap() } + pub async fn unwrap_single_addr(&self) -> Address { self.single_addr_or_err().await.unwrap() } + + pub async fn to_response(&self) -> MmResult { + match self { + DerivationMethod::SingleAddress(_) => Ok(DerivationMethodResponse::Iguana), + DerivationMethod::HDWallet(hd_wallet) => { + let enabled_address = hd_wallet + .get_enabled_address() + .await + .or_mm_err(|| UnexpectedDerivationMethod::ExpectedHDWallet)?; + Ok(DerivationMethodResponse::HDWallet( + enabled_address.derivation_path().to_string(), + )) + }, + } + } } +/// A trait representing coins with specific address derivation methods. +/// +/// This trait is designed for coins that have a defined mechanism for address derivation, +/// be it a single address approach or a hierarchical deterministic (HD) wallet strategy. +/// Coins implementing this trait should be clear about their chosen derivation method and +/// offer utility functions to interact with that method. +/// +/// Implementors of this trait will typically be coins or tokens that are either used within +/// a traditional single address scheme or leverage the power and flexibility of HD wallets. #[async_trait] -pub trait CoinWithDerivationMethod { - type Address; - type HDWallet; - - fn derivation_method(&self) -> &DerivationMethod; +pub trait CoinWithDerivationMethod: HDWalletCoinOps { + /// Returns the address derivation method associated with the coin. + /// + /// Implementors should return the specific `DerivationMethod` that the coin utilizes, + /// either `SingleAddress` for a static address approach or `HDWallet` for an HD wallet strategy. + fn derivation_method(&self) -> &DerivationMethod, Self::HDWallet>; + /// Checks if the coin uses the HD wallet strategy for address derivation. + /// + /// This is a utility function that returns `true` if the coin's derivation method is `HDWallet` and + /// `false` otherwise. + /// + /// # Returns + /// + /// - `true` if the coin uses an HD wallet for address derivation. + /// - `false` if it uses any other method. fn has_hd_wallet_derivation_method(&self) -> bool { matches!(self.derivation_method(), DerivationMethod::HDWallet(_)) } + + /// Retrieves all addresses associated with the coin. + async fn all_addresses(&self) -> MmResult>, AddressDerivingError> { + const ADDRESSES_CAPACITY: usize = 60; + + match self.derivation_method() { + DerivationMethod::SingleAddress(ref my_address) => Ok(iter::once(my_address.clone()).collect()), + DerivationMethod::HDWallet(ref hd_wallet) => { + let hd_accounts = hd_wallet.get_accounts().await; + + // We pre-allocate a suitable capacity for the HashSet to try to avoid re-allocations. + // If the capacity is exceeded, the HashSet will automatically resize itself by re-allocating, + // but this will not happen in most use cases where addresses will be below the capacity. + let mut all_addresses = HashSet::with_capacity(ADDRESSES_CAPACITY); + for (_, hd_account) in hd_accounts { + let external_addresses = self.derive_known_addresses(&hd_account, Bip44Chain::External).await?; + let internal_addresses = self.derive_known_addresses(&hd_account, Bip44Chain::Internal).await?; + + let addresses_it = external_addresses + .into_iter() + .chain(internal_addresses) + .map(|hd_address| hd_address.address()); + all_addresses.extend(addresses_it); + } + + Ok(all_addresses) + }, + } + } +} + +/// The `IguanaBalanceOps` trait provides an interface for fetching the balance of a coin and its tokens. +/// This trait should be implemented by coins that use the iguana derivation method. +#[async_trait] +pub trait IguanaBalanceOps { + /// The object that holds the balance/s of the coin. + type BalanceObject: BalanceObjectOps; + + /// Fetches the balance of the coin and its tokens if the coin uses an iguana derivation method. + async fn iguana_balances(&self) -> BalanceResult; } #[allow(clippy::upper_case_acronyms)] @@ -4960,6 +5195,83 @@ fn coins_conf_check(ctx: &MmArc, coins_en: &Json, ticker: &str, req: Option<&Jso Ok(()) } +/// Checks addresses that either had empty transaction history last time we checked or has not been checked before. +/// The checking stops at the moment when we find `gap_limit` consecutive empty addresses. +pub async fn scan_for_new_addresses_impl( + coin: &T, + hd_wallet: &T::HDWallet, + hd_account: &mut HDCoinHDAccount, + address_scanner: &T::HDAddressScanner, + chain: Bip44Chain, + gap_limit: u32, +) -> BalanceResult>>> +where + T: HDWalletBalanceOps + Sync, +{ + let mut balances = Vec::with_capacity(gap_limit as usize); + + // Get the first unknown address id. + let mut checking_address_id = hd_account + .known_addresses_number(chain) + // A UTXO coin should support both [`Bip44Chain::External`] and [`Bip44Chain::Internal`]. + .mm_err(|e| BalanceError::Internal(e.to_string()))?; + + let mut unused_addresses_counter = 0; + let max_addresses_number = hd_account.address_limit(); + while checking_address_id < max_addresses_number && unused_addresses_counter <= gap_limit { + let hd_address = coin.derive_address(hd_account, chain, checking_address_id).await?; + let checking_address = hd_address.address(); + let checking_address_der_path = hd_address.derivation_path(); + + match coin.is_address_used(&checking_address, address_scanner).await? { + // We found a non-empty address, so we have to fill up the balance list + // with zeros starting from `last_non_empty_address_id = checking_address_id - unused_addresses_counter`. + AddressBalanceStatus::Used(non_empty_balance) => { + let last_non_empty_address_id = checking_address_id - unused_addresses_counter; + + // First, derive all empty addresses and put it into `balances` with default balance. + let address_ids = (last_non_empty_address_id..checking_address_id) + .into_iter() + .map(|address_id| HDAddressId { chain, address_id }); + let empty_addresses = + coin.derive_addresses(hd_account, address_ids) + .await? + .into_iter() + .map(|empty_address| HDAddressBalance { + address: coin.address_formatter()(&empty_address.address()), + derivation_path: RpcDerivationPath(empty_address.derivation_path().clone()), + chain, + balance: HDWalletBalanceObject::::new(), + }); + balances.extend(empty_addresses); + + // Then push this non-empty address. + balances.push(HDAddressBalance { + address: coin.address_formatter()(&checking_address), + derivation_path: RpcDerivationPath(checking_address_der_path.clone()), + chain, + balance: non_empty_balance, + }); + // Reset the counter of unused addresses to zero since we found a non-empty address. + unused_addresses_counter = 0; + }, + AddressBalanceStatus::NotUsed => unused_addresses_counter += 1, + } + + checking_address_id += 1; + } + + coin.set_known_addresses_number( + hd_wallet, + hd_account, + chain, + checking_address_id - unused_addresses_counter, + ) + .await?; + + Ok(balances) +} + #[cfg(test)] mod tests { use super::*; @@ -5017,3 +5329,60 @@ mod tests { assert!(matches!(Some(coin), _found)); } } + +#[cfg(all(feature = "for-tests", not(target_arch = "wasm32")))] +pub mod for_tests { + use crate::rpc_command::init_withdraw::WithdrawStatusRequest; + use crate::rpc_command::init_withdraw::{init_withdraw, withdraw_status}; + use crate::{TransactionDetails, WithdrawError, WithdrawFee, WithdrawFrom, WithdrawRequest}; + use common::executor::Timer; + use common::{now_ms, wait_until_ms}; + use mm2_core::mm_ctx::MmArc; + use mm2_err_handle::prelude::MmResult; + use mm2_number::BigDecimal; + use rpc_task::RpcTaskStatus; + use std::str::FromStr; + + /// Helper to call init_withdraw and wait for completion + pub async fn test_withdraw_init_loop( + ctx: MmArc, + ticker: &str, + to: &str, + amount: &str, + from_derivation_path: Option<&str>, + fee: Option, + ) -> MmResult { + let withdraw_req = WithdrawRequest { + amount: BigDecimal::from_str(amount).unwrap(), + from: from_derivation_path.map(|from_derivation_path| WithdrawFrom::DerivationPath { + derivation_path: from_derivation_path.to_owned(), + }), + to: to.to_owned(), + coin: ticker.to_owned(), + max: false, + fee, + memo: None, + }; + let init = init_withdraw(ctx.clone(), withdraw_req).await.unwrap(); + let timeout = wait_until_ms(150000); + loop { + if now_ms() > timeout { + panic!("{} init_withdraw timed out", ticker); + } + let status = withdraw_status(ctx.clone(), WithdrawStatusRequest { + task_id: init.task_id, + forget_if_finished: true, + }) + .await; + if let Ok(status) = status { + match status { + RpcTaskStatus::Ok(tx_details) => break Ok(tx_details), + RpcTaskStatus::Error(e) => break Err(e), + _ => Timer::sleep(1.).await, + } + } else { + panic!("{} could not get withdraw_status", ticker) + } + } + } +} diff --git a/mm2src/coins/my_tx_history_v2.rs b/mm2src/coins/my_tx_history_v2.rs index 00f25b2701..517eb1ca47 100644 --- a/mm2src/coins/my_tx_history_v2.rs +++ b/mm2src/coins/my_tx_history_v2.rs @@ -4,8 +4,9 @@ use crate::tx_history_storage::{CreateTxHistoryStorageError, FilteringAddresses, TxHistoryStorageBuilder, WalletId}; use crate::utxo::utxo_common::big_decimal_from_sat_unsigned; use crate::MyAddressError; -use crate::{coin_conf, lp_coinfind_or_err, BlockHeightAndTime, CoinFindError, HDAccountAddressId, HistorySyncState, - MmCoin, MmCoinEnum, Transaction, TransactionDetails, TransactionType, TxFeeDetails, UtxoRpcError}; +use crate::{coin_conf, lp_coinfind_or_err, BlockHeightAndTime, CoinFindError, HDPathAccountToAddressId, + HistorySyncState, MmCoin, MmCoinEnum, Transaction, TransactionDetails, TransactionType, TxFeeDetails, + UtxoRpcError}; use async_trait::async_trait; use bitcrypto::sha256; use common::{calc_total_pages, ten, HttpStatusCode, PagingOptionsEnum, StatusCode}; @@ -266,7 +267,7 @@ impl<'a, Addr: Clone + DisplayAddress + Eq + std::hash::Hash, Tx: Transaction> T pub enum MyTxHistoryTarget { Iguana, AccountId { account_id: u32 }, - AddressId(HDAccountAddressId), + AddressId(HDPathAccountToAddressId), AddressDerivationPath(StandardHDPath), } diff --git a/mm2src/coins/nft.rs b/mm2src/coins/nft.rs index 8e73e2b272..508016af54 100644 --- a/mm2src/coins/nft.rs +++ b/mm2src/coins/nft.rs @@ -17,6 +17,7 @@ use nft_structs::{Chain, ContractType, ConvertChain, Nft, NftFromMoralis, NftLis use crate::eth::{eth_addr_to_hex, get_eth_address, withdraw_erc1155, withdraw_erc721, EthCoin, EthCoinType, EthTxFeeDetails}; +use crate::hd_wallet::HDPathAccountToAddressId; use crate::nft::nft_errors::{ClearNftDbError, MetaFromUrlError, ProtectFromSpamError, TransferConfirmationsError, UpdateSpamPhishingError}; use crate::nft::nft_structs::{build_nft_with_empty_meta, BuildNftFields, ClearNftDbReq, NftCommon, NftCtx, NftInfo, @@ -24,7 +25,6 @@ use crate::nft::nft_structs::{build_nft_with_empty_meta, BuildNftFields, ClearNf SpamContractReq, SpamContractRes, TransferMeta, TransferStatus, UriMeta}; use crate::nft::storage::{NftListStorageOps, NftTransferHistoryStorageOps}; use common::parse_rfc3339_to_timestamp; -use crypto::StandardHDCoinAddress; use ethereum_types::{Address, H256}; use futures::compat::Future01CompatExt; use futures::future::try_join_all; @@ -610,7 +610,7 @@ async fn get_moralis_nft_list( let mut res_list = Vec::new(); let ticker = chain.to_ticker(); let conf = coin_conf(ctx, ticker); - let my_address = get_eth_address(ctx, &conf, ticker, &StandardHDCoinAddress::default()).await?; + let my_address = get_eth_address(ctx, &conf, ticker, &HDPathAccountToAddressId::default()).await?; let uri_without_cursor = construct_moralis_uri_for_nft(url, &my_address.wallet_address, chain)?; // The cursor returned in the previous response (used for getting the next page). @@ -710,7 +710,7 @@ async fn get_moralis_nft_transfers( let mut res_list = Vec::new(); let ticker = chain.to_ticker(); let conf = coin_conf(ctx, ticker); - let my_address = get_eth_address(ctx, &conf, ticker, &StandardHDCoinAddress::default()).await?; + let my_address = get_eth_address(ctx, &conf, ticker, &HDPathAccountToAddressId::default()).await?; let mut uri_without_cursor = url.clone(); uri_without_cursor.set_path(MORALIS_API_ENDPOINT); @@ -972,7 +972,7 @@ async fn update_nft_list( let transfers = storage.get_transfers_from_block(*chain, scan_from_block).await?; let req = MyAddressReq { coin: chain.to_ticker().to_string(), - path_to_address: StandardHDCoinAddress::default(), + path_to_address: HDPathAccountToAddressId::default(), }; let my_address = get_my_address(ctx.clone(), req).await?.wallet_address.to_lowercase(); for transfer in transfers.into_iter() { diff --git a/mm2src/coins/nft/nft_errors.rs b/mm2src/coins/nft/nft_errors.rs index 8a6fb90f19..5e35b138ff 100644 --- a/mm2src/coins/nft/nft_errors.rs +++ b/mm2src/coins/nft/nft_errors.rs @@ -2,7 +2,7 @@ use crate::eth::GetEthAddressError; #[cfg(target_arch = "wasm32")] use crate::nft::storage::wasm::WasmNftCacheError; use crate::nft::storage::NftStorageError; -use crate::{CoinFindError, GetMyAddressError, NumConversError, WithdrawError}; +use crate::{CoinFindError, GetMyAddressError, NumConversError, UnexpectedDerivationMethod, WithdrawError}; use common::{HttpStatusCode, ParseRfc3339Err}; #[cfg(not(target_arch = "wasm32"))] use db_common::sqlite::rusqlite::Error as SqlError; @@ -52,6 +52,10 @@ impl From for WithdrawError { fn from(e: GetNftInfoError) -> Self { WithdrawError::GetNftInfoError(e) } } +impl From for GetNftInfoError { + fn from(e: UnexpectedDerivationMethod) -> Self { GetNftInfoError::Internal(e.to_string()) } +} + impl From for GetNftInfoError { fn from(e: SlurpError) -> Self { let error_str = e.to_string(); diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index 806fe3c0e2..d014e027e5 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -521,8 +521,8 @@ impl Qrc20Coin { &self, contract_outputs: Vec, ) -> Result> { - let my_address = self.utxo.derivation_method.single_addr_or_err()?; - let (unspents, _) = self.get_unspent_ordered_list(my_address).await?; + let my_address = self.utxo.derivation_method.single_addr_or_err().await?; + let (unspents, _) = self.get_unspent_ordered_list(&my_address).await?; let mut gas_fee = 0; let mut outputs = Vec::with_capacity(contract_outputs.len()); @@ -532,17 +532,18 @@ impl Qrc20Coin { } let (unsigned, data) = UtxoTxBuilder::new(self) + .await .add_available_inputs(unspents) .add_outputs(outputs) .with_gas_fee(gas_fee) .build() .await?; - let my_address = self.utxo.derivation_method.single_addr_or_err()?; + let my_address = self.utxo.derivation_method.single_addr_or_err().await?; let key_pair = self.utxo.priv_key_policy.activated_key_or_err()?; let prev_script = self - .script_for_address(my_address) + .script_for_address(&my_address) .map_err(|e| Qrc20GenTxError::InvalidAddress(e.to_string()))?; let signed = sign_tx( unsigned, @@ -811,35 +812,31 @@ impl SwapOps for Qrc20Coin { } #[inline] - fn send_maker_spends_taker_payment(&self, maker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { + async fn send_maker_spends_taker_payment( + &self, + maker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { let payment_tx: UtxoTx = - try_tx_fus!(deserialize(maker_spends_payment_args.other_payment_tx).map_err(|e| ERRL!("{:?}", e))); - let swap_contract_address = try_tx_fus!(maker_spends_payment_args.swap_contract_address.try_to_address()); + try_tx_s!(deserialize(maker_spends_payment_args.other_payment_tx).map_err(|e| ERRL!("{:?}", e))); + let swap_contract_address = try_tx_s!(maker_spends_payment_args.swap_contract_address.try_to_address()); let secret = maker_spends_payment_args.secret.to_vec(); - let selfi = self.clone(); - let fut = async move { - selfi - .spend_hash_time_locked_payment(payment_tx, swap_contract_address, secret) - .await - }; - Box::new(fut.boxed().compat()) + self.spend_hash_time_locked_payment(payment_tx, swap_contract_address, secret) + .await } #[inline] - fn send_taker_spends_maker_payment(&self, taker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { + async fn send_taker_spends_maker_payment( + &self, + taker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { let payment_tx: UtxoTx = - try_tx_fus!(deserialize(taker_spends_payment_args.other_payment_tx).map_err(|e| ERRL!("{:?}", e))); + try_tx_s!(deserialize(taker_spends_payment_args.other_payment_tx).map_err(|e| ERRL!("{:?}", e))); let secret = taker_spends_payment_args.secret.to_vec(); - let swap_contract_address = try_tx_fus!(taker_spends_payment_args.swap_contract_address.try_to_address()); + let swap_contract_address = try_tx_s!(taker_spends_payment_args.swap_contract_address.try_to_address()); - let selfi = self.clone(); - let fut = async move { - selfi - .spend_hash_time_locked_payment(payment_tx, swap_contract_address, secret) - .await - }; - Box::new(fut.boxed().compat()) + self.spend_hash_time_locked_payment(payment_tx, swap_contract_address, secret) + .await } #[inline] @@ -1188,7 +1185,7 @@ impl MarketCoinOps for Qrc20Coin { fn my_address(&self) -> MmResult { utxo_common::my_address(self) } - fn get_public_key(&self) -> Result> { + async fn get_public_key(&self) -> Result> { let pubkey = utxo_common::my_public_key(self.as_ref())?; Ok(pubkey.to_string()) } @@ -1212,6 +1209,7 @@ impl MarketCoinOps for Qrc20Coin { let fut = async move { let my_address = coin .my_addr_as_contract_addr() + .await .mm_err(|e| BalanceError::Internal(e.to_string()))?; let params = [Token::Address(my_address)]; let contract_address = coin.contract_address; @@ -1235,7 +1233,6 @@ impl MarketCoinOps for Qrc20Coin { }; Box::new(fut.boxed().compat()) } - fn base_coin_balance(&self) -> BalanceFut { // use standard UTXO my_balance implementation that returns Qtum balance instead of QRC20 Box::new(utxo_common::my_balance(self.clone()).map(|CoinBalance { spendable, .. }| spendable)) @@ -1312,6 +1309,8 @@ impl MarketCoinOps for Qrc20Coin { let pow = self.utxo.decimals as u32; MmNumber::from(1) / MmNumber::from(10u64.pow(pow)) } + + fn is_trezor(&self) -> bool { self.as_ref().priv_key_policy.is_trezor() } } #[async_trait] @@ -1581,8 +1580,8 @@ async fn qrc20_withdraw(coin: Qrc20Coin, req: WithdrawRequest) -> WithdrawResult .await .mm_err(|gen_tx_error| gen_tx_error.into_withdraw_error(coin.platform.clone(), coin.utxo.decimals))?; - let my_address = coin.utxo.derivation_method.single_addr_or_err()?; - let received_by_me = if to_addr == *my_address { + let my_address = coin.utxo.derivation_method.single_addr_or_err().await?; + let received_by_me = if to_addr == my_address { qrc20_amount.clone() } else { 0.into() diff --git a/mm2src/coins/qrc20/history.rs b/mm2src/coins/qrc20/history.rs index b3ae9e7655..d49905b53d 100644 --- a/mm2src/coins/qrc20/history.rs +++ b/mm2src/coins/qrc20/history.rs @@ -209,22 +209,24 @@ impl Qrc20Coin { let mut details = TxTransferMap::new(); for receipt in receipts { - let log_details = - try_s!(self.transfer_details_from_receipt(&qtum_tx, &qtum_details, receipt, miner_fee.clone())); + let log_details = try_s!( + self.transfer_details_from_receipt(&qtum_tx, &qtum_details, receipt, miner_fee.clone()) + .await + ); details.extend(log_details.into_iter()) } Ok(details) } - fn transfer_details_from_receipt( + async fn transfer_details_from_receipt( &self, qtum_tx: &UtxoTx, qtum_details: &TransactionDetails, receipt: TxReceipt, miner_fee: BigDecimal, ) -> Result { - let my_address = try_s!(self.utxo.derivation_method.single_addr_or_err()); + let my_address = try_s!(self.utxo.derivation_method.single_addr_or_err().await); let tx_hash: H256Json = try_s!(H256Json::from_str(&qtum_details.tx_hash)); if qtum_tx.outputs.len() <= (receipt.output_index as usize) { return ERR!( @@ -280,17 +282,17 @@ impl Qrc20Coin { }; // https://github.com/qtumproject/qtum-electrum/blob/v4.0.2/electrum/wallet.py#L2102 - if from != *my_address && to != *my_address { + if from != my_address && to != my_address { // address mismatch continue; } - let spent_by_me = if from == *my_address { + let spent_by_me = if from == my_address { total_amount.clone() } else { 0.into() }; - let received_by_me = if to == *my_address { + let received_by_me = if to == my_address { total_amount.clone() } else { 0.into() @@ -604,16 +606,16 @@ impl TransferHistoryBuilder { } pub async fn build(self) -> Result, MmError> { - let params = self.build_params()?; + let params = self.build_params().await?; self.coin.utxo.rpc_client.build(params).await } pub async fn build_tx_idents(self) -> Result, MmError> { - let params = self.build_params()?; + let params = self.build_params().await?; self.coin.utxo.rpc_client.build_tx_idents(params).await } - fn build_params(&self) -> Result> { + async fn build_params(&self) -> Result> { let address = match self.address { Some(addr) => addr, None => { @@ -622,9 +624,9 @@ impl TransferHistoryBuilder { .utxo .derivation_method .single_addr_or_err() + .await .mm_err(|e| UtxoRpcError::Internal(e.to_string()))?; - qtum::contract_addr_from_utxo_addr(my_address.clone()) - .mm_err(|e| UtxoRpcError::Internal(e.to_string()))? + qtum::contract_addr_from_utxo_addr(my_address).mm_err(|e| UtxoRpcError::Internal(e.to_string()))? }, }; diff --git a/mm2src/coins/qrc20/qrc20_tests.rs b/mm2src/coins/qrc20/qrc20_tests.rs index a925d96a6a..9df455a188 100644 --- a/mm2src/coins/qrc20/qrc20_tests.rs +++ b/mm2src/coins/qrc20/qrc20_tests.rs @@ -74,8 +74,10 @@ fn test_withdraw_to_p2sh_address_should_fail() { let p2sh_address = AddressBuilder::new( UtxoAddressFormat::Standard, - coin.as_ref().derivation_method.unwrap_single_addr().hash().clone(), - *coin.as_ref().derivation_method.unwrap_single_addr().checksum_type(), + block_on(coin.as_ref().derivation_method.unwrap_single_addr()) + .hash() + .clone(), + *block_on(coin.as_ref().derivation_method.unwrap_single_addr()).checksum_type(), coin.as_ref().conf.address_prefixes.clone(), coin.as_ref().conf.bech32_hrp.clone(), ) @@ -159,7 +161,7 @@ fn test_validate_maker_payment() { let (_ctx, coin) = qrc20_coin_for_test(priv_key, None); assert_eq!( - *coin.utxo.derivation_method.unwrap_single_addr(), + block_on(coin.utxo.derivation_method.unwrap_single_addr()), Address::from_legacyaddress( "qUX9FGHubczidVjWPCUWuwCUJWpkAtGCgf", &coin.as_ref().conf.address_prefixes @@ -256,7 +258,7 @@ fn test_wait_for_confirmations_excepted() { let (_ctx, coin) = qrc20_coin_for_test(priv_key, None); assert_eq!( - *coin.utxo.derivation_method.unwrap_single_addr(), + block_on(coin.utxo.derivation_method.unwrap_single_addr()), Address::from_legacyaddress( "qUX9FGHubczidVjWPCUWuwCUJWpkAtGCgf", &coin.as_ref().conf.address_prefixes diff --git a/mm2src/coins/qrc20/swap.rs b/mm2src/coins/qrc20/swap.rs index a560d37057..a6162f6421 100644 --- a/mm2src/coins/qrc20/swap.rs +++ b/mm2src/coins/qrc20/swap.rs @@ -136,7 +136,7 @@ impl Qrc20Coin { let expected_call_bytes = { let expected_value = wei_from_big_decimal(&amount, self.utxo.decimals)?; - let my_address = self.utxo.derivation_method.single_addr_or_err()?.clone(); + let my_address = self.utxo.derivation_method.single_addr_or_err().await?; let expected_receiver = qtum::contract_addr_from_utxo_addr(my_address) .mm_err(|err| ValidatePaymentError::InternalError(err.to_string()))?; self.erc20_payment_call_bytes( @@ -264,7 +264,7 @@ impl Qrc20Coin { } // Else try to find a 'senderRefund' contract call. - let my_address = try_s!(self.utxo.derivation_method.single_addr_or_err()).clone(); + let my_address = try_s!(self.utxo.derivation_method.single_addr_or_err().await); let sender = try_s!(qtum::contract_addr_from_utxo_addr(my_address)); let refund_txs = try_s!(self.sender_refund_transactions(sender, search_from_block).await); let found = refund_txs.into_iter().find(|tx| { @@ -288,7 +288,7 @@ impl Qrc20Coin { return Ok(None); }; - let my_address = try_s!(self.utxo.derivation_method.single_addr_or_err()).clone(); + let my_address = try_s!(self.utxo.derivation_method.single_addr_or_err().await); let sender = try_s!(qtum::contract_addr_from_utxo_addr(my_address)); let erc20_payment_txs = try_s!(self.erc20_payment_transactions(sender, search_from_block).await); let found = erc20_payment_txs @@ -461,6 +461,7 @@ impl Qrc20Coin { .utxo .derivation_method .single_addr_or_err() + .await .mm_err(|e| UtxoRpcError::Internal(e.to_string()))?; let tokens = self .utxo diff --git a/mm2src/coins/rpc_command/account_balance.rs b/mm2src/coins/rpc_command/account_balance.rs index 4c29383d1e..c0cb1bf09b 100644 --- a/mm2src/coins/rpc_command/account_balance.rs +++ b/mm2src/coins/rpc_command/account_balance.rs @@ -1,13 +1,11 @@ use crate::coin_balance::HDAddressBalance; -use crate::hd_wallet::HDWalletCoinOps; use crate::rpc_command::hd_account_balance_rpc_error::HDAccountBalanceRpcError; -use crate::{lp_coinfind_or_err, CoinBalance, CoinWithDerivationMethod, MmCoinEnum}; +use crate::{lp_coinfind_or_err, CoinBalance, CoinBalanceMap, CoinWithDerivationMethod, MmCoinEnum}; use async_trait::async_trait; use common::PagingOptionsEnum; use crypto::{Bip44Chain, RpcDerivationPath}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; -use std::fmt; #[derive(Deserialize)] pub struct HDAccountBalanceRequest { @@ -27,11 +25,12 @@ pub struct AccountBalanceParams { } #[derive(Debug, PartialEq, Serialize)] -pub struct HDAccountBalanceResponse { +pub struct HDAccountBalanceResponse { pub account_index: u32, pub derivation_path: RpcDerivationPath, - pub addresses: Vec, - pub page_balance: CoinBalance, + pub addresses: Vec>, + // Todo: Add option to get total balance of all addresses in addition to page_balance + pub page_balance: BalanceObject, pub limit: usize, pub skipped: u32, pub total: u32, @@ -39,38 +38,57 @@ pub struct HDAccountBalanceResponse { pub paging_options: PagingOptionsEnum, } +/// Enum for the response of the `account_balance` RPC command. +#[derive(Debug, PartialEq, Serialize)] +#[serde(untagged)] +pub enum HDAccountBalanceResponseEnum { + Single(HDAccountBalanceResponse), + Map(HDAccountBalanceResponse), +} + +/// Trait for the `account_balance` RPC command. #[async_trait] pub trait AccountBalanceRpcOps { + type BalanceObject; + async fn account_balance_rpc( &self, params: AccountBalanceParams, - ) -> MmResult; + ) -> MmResult, HDAccountBalanceRpcError>; } +/// `account_balance` RPC command implementation. pub async fn account_balance( ctx: MmArc, req: HDAccountBalanceRequest, -) -> MmResult { +) -> MmResult { match lp_coinfind_or_err(&ctx, &req.coin).await? { - MmCoinEnum::UtxoCoin(utxo) => utxo.account_balance_rpc(req.params).await, - MmCoinEnum::QtumCoin(qtum) => qtum.account_balance_rpc(req.params).await, + MmCoinEnum::UtxoCoin(utxo) => Ok(HDAccountBalanceResponseEnum::Single( + utxo.account_balance_rpc(req.params).await?, + )), + MmCoinEnum::QtumCoin(qtum) => Ok(HDAccountBalanceResponseEnum::Single( + qtum.account_balance_rpc(req.params).await?, + )), + MmCoinEnum::EthCoin(eth) => Ok(HDAccountBalanceResponseEnum::Map( + eth.account_balance_rpc(req.params).await?, + )), _ => MmError::err(HDAccountBalanceRpcError::CoinIsActivatedNotWithHDWallet), } } pub mod common_impl { use super::*; - use crate::coin_balance::HDWalletBalanceOps; + use crate::coin_balance::{BalanceObjectOps, HDWalletBalanceObject, HDWalletBalanceOps}; use crate::hd_wallet::{HDAccountOps, HDWalletOps}; use common::calc_total_pages; + /// Common implementation for the `account_balance` RPC command. pub async fn account_balance_rpc( coin: &Coin, params: AccountBalanceParams, - ) -> MmResult + ) -> MmResult>, HDAccountBalanceRpcError> where - Coin: HDWalletBalanceOps + CoinWithDerivationMethod::HDWallet> + Sync, - ::Address: fmt::Display + Clone, + Coin: HDWalletBalanceOps + CoinWithDerivationMethod + Sync, { let account_id = params.account_index; let hd_account = coin @@ -90,9 +108,13 @@ pub mod common_impl { let addresses = coin .known_addresses_balances_with_ids(&hd_account, params.chain, from_address_id..to_address_id) .await?; - let page_balance = addresses.iter().fold(CoinBalance::default(), |total, addr_balance| { - total + addr_balance.balance.clone() - }); + + let page_balance = addresses + .iter() + .fold(HDWalletBalanceObject::::new(), |mut total, addr_balance| { + total.add(addr_balance.balance.clone()); + total + }); let result = HDAccountBalanceResponse { account_index: account_id, diff --git a/mm2src/coins/rpc_command/get_new_address.rs b/mm2src/coins/rpc_command/get_new_address.rs index ee8d8ad73d..9bba74cf75 100644 --- a/mm2src/coins/rpc_command/get_new_address.rs +++ b/mm2src/coins/rpc_command/get_new_address.rs @@ -1,11 +1,13 @@ use crate::coin_balance::HDAddressBalance; -use crate::hd_confirm_address::{ConfirmAddressStatus, HDConfirmAddress, HDConfirmAddressError, RpcTaskConfirmAddress}; -use crate::hd_wallet::{AddressDerivingError, InvalidBip44ChainError, NewAddressDeriveConfirmError, - NewAddressDerivingError}; -use crate::{lp_coinfind_or_err, BalanceError, CoinFindError, CoinsContext, MmCoinEnum, UnexpectedDerivationMethod}; +use crate::hd_wallet::{AddressDerivingError, ConfirmAddressStatus, HDConfirmAddress, HDConfirmAddressError, + InvalidBip44ChainError, NewAddressDeriveConfirmError, NewAddressDerivingError, + RpcTaskConfirmAddress}; +use crate::{lp_coinfind_or_err, BalanceError, CoinBalance, CoinBalanceMap, CoinFindError, CoinsContext, MmCoinEnum, + UnexpectedDerivationMethod}; use async_trait::async_trait; use common::{HttpStatusCode, SuccessResponse}; use crypto::hw_rpc_task::{HwConnectStatuses, HwRpcTaskAwaitingStatus, HwRpcTaskUserAction, HwRpcTaskUserActionRequest}; +use crypto::trezor::TrezorMessageType; use crypto::{from_hw_error, Bip44Chain, HwError, HwRpcError, WithHwRpcError}; use derive_more::Display; use enum_derives::EnumFromTrait; @@ -24,7 +26,7 @@ pub type GetNewAddressTaskManager = RpcTaskManager; pub type GetNewAddressTaskManagerShared = RpcTaskManagerShared; pub type GetNewAddressTaskHandleShared = RpcTaskHandleShared; pub type GetNewAddressRpcTaskStatus = RpcTaskStatus< - GetNewAddressResponse, + GetNewAddressResponseEnum, GetNewAddressRpcError, GetNewAddressInProgressStatus, GetNewAddressAwaitingStatus, @@ -110,7 +112,7 @@ impl From for GetNewAddressRpcError { GetNewAddressRpcError::AddressLimitReached { max_addresses_number } }, NewAddressDerivingError::InvalidBip44Chain { chain } => GetNewAddressRpcError::InvalidBip44Chain { chain }, - NewAddressDerivingError::Bip32Error(bip32) => GetNewAddressRpcError::Internal(bip32.to_string()), + NewAddressDerivingError::Bip32Error(bip32) => GetNewAddressRpcError::Internal(bip32), NewAddressDerivingError::WalletStorageError(storage) => { GetNewAddressRpcError::WalletStorageError(storage.to_string()) }, @@ -123,7 +125,7 @@ impl From for GetNewAddressRpcError { fn from(e: AddressDerivingError) -> Self { match e { AddressDerivingError::InvalidBip44Chain { chain } => GetNewAddressRpcError::InvalidBip44Chain { chain }, - AddressDerivingError::Bip32Error(bip32) => GetNewAddressRpcError::ErrorDerivingAddress(bip32.to_string()), + AddressDerivingError::Bip32Error(bip32) => GetNewAddressRpcError::ErrorDerivingAddress(bip32), AddressDerivingError::Internal(internal) => GetNewAddressRpcError::Internal(internal), } } @@ -147,6 +149,9 @@ impl From for GetNewAddressRpcError { HDConfirmAddressError::InvalidAddress { expected, found } => GetNewAddressRpcError::Internal(format!( "Confirmation address mismatched: expected '{expected}, found '{found}''" )), + HDConfirmAddressError::NoAddressReceived => { + GetNewAddressRpcError::Internal("No address received".to_string()) + }, HDConfirmAddressError::Internal(internal) => GetNewAddressRpcError::Internal(internal), } } @@ -211,9 +216,18 @@ pub struct GetNewAddressParams { pub(crate) gap_limit: Option, } +/// Generic response for the `get_new_address` RPC command. +#[derive(Clone, Debug, Serialize)] +pub struct GetNewAddressResponse { + new_address: HDAddressBalance, +} + +/// Enum for the response of the `get_new_address` RPC command. #[derive(Clone, Debug, Serialize)] -pub struct GetNewAddressResponse { - new_address: HDAddressBalance, +#[serde(untagged)] +pub enum GetNewAddressResponseEnum { + Single(GetNewAddressResponse), + Map(GetNewAddressResponse), } #[derive(Clone, Serialize)] @@ -236,21 +250,24 @@ impl ConfirmAddressStatus for GetNewAddressInProgressStatus { } } +/// A trait for the `get_new_address` RPC commands. #[async_trait] pub trait GetNewAddressRpcOps { + type BalanceObject; + /// Generates a new address. /// TODO remove once GUI integrates `task::get_new_address::init`. async fn get_new_address_rpc_without_conf( &self, params: GetNewAddressParams, - ) -> MmResult; + ) -> MmResult, GetNewAddressRpcError>; /// Generates and asks the user to confirm a new address. async fn get_new_address_rpc( &self, params: GetNewAddressParams, confirm_address: &ConfirmAddress, - ) -> MmResult + ) -> MmResult, GetNewAddressRpcError> where ConfirmAddress: HDConfirmAddress; } @@ -263,7 +280,7 @@ pub struct InitGetNewAddressTask { } impl RpcTaskTypes for InitGetNewAddressTask { - type Item = GetNewAddressResponse; + type Item = GetNewAddressResponseEnum; type Error = GetNewAddressRpcError; type InProgressStatus = GetNewAddressInProgressStatus; type AwaitingStatus = GetNewAddressAwaitingStatus; @@ -283,7 +300,8 @@ impl RpcTask for InitGetNewAddressTask { coin: &Coin, params: GetNewAddressParams, task_handle: GetNewAddressTaskHandleShared, - ) -> MmResult + trezor_message_type: TrezorMessageType, + ) -> MmResult::BalanceObject>, GetNewAddressRpcError> where Coin: GetNewAddressRpcOps + Send + Sync, { @@ -297,32 +315,62 @@ impl RpcTask for InitGetNewAddressTask { on_ready: GetNewAddressInProgressStatus::RequestingAccountBalance, }; let confirm_address: RpcTaskConfirmAddress = - RpcTaskConfirmAddress::new(ctx, task_handle, hw_statuses)?; + RpcTaskConfirmAddress::new(ctx, task_handle, hw_statuses, trezor_message_type)?; coin.get_new_address_rpc(params, &confirm_address).await } match self.coin { - MmCoinEnum::UtxoCoin(ref utxo) => { - get_new_address_helper(&self.ctx, utxo, self.req.params.clone(), task_handle).await - }, - MmCoinEnum::QtumCoin(ref qtum) => { - get_new_address_helper(&self.ctx, qtum, self.req.params.clone(), task_handle).await - }, + MmCoinEnum::UtxoCoin(ref utxo) => Ok(GetNewAddressResponseEnum::Single( + get_new_address_helper( + &self.ctx, + utxo, + self.req.params.clone(), + task_handle, + TrezorMessageType::Bitcoin, + ) + .await?, + )), + MmCoinEnum::QtumCoin(ref qtum) => Ok(GetNewAddressResponseEnum::Single( + get_new_address_helper( + &self.ctx, + qtum, + self.req.params.clone(), + task_handle, + TrezorMessageType::Bitcoin, + ) + .await?, + )), + MmCoinEnum::EthCoin(ref eth) => Ok(GetNewAddressResponseEnum::Map( + get_new_address_helper( + &self.ctx, + eth, + self.req.params.clone(), + task_handle, + TrezorMessageType::Ethereum, + ) + .await?, + )), _ => MmError::err(GetNewAddressRpcError::CoinIsActivatedNotWithHDWallet), } } } /// Generates a new address. -/// TODO remove once GUI integrates `task::get_new_address::init`. pub async fn get_new_address( ctx: MmArc, req: GetNewAddressRequest, -) -> MmResult { +) -> MmResult { let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; match coin { - MmCoinEnum::UtxoCoin(utxo) => utxo.get_new_address_rpc_without_conf(req.params).await, - MmCoinEnum::QtumCoin(qtum) => qtum.get_new_address_rpc_without_conf(req.params).await, + MmCoinEnum::UtxoCoin(utxo) => Ok(GetNewAddressResponseEnum::Single( + utxo.get_new_address_rpc_without_conf(req.params).await?, + )), + MmCoinEnum::QtumCoin(qtum) => Ok(GetNewAddressResponseEnum::Single( + qtum.get_new_address_rpc_without_conf(req.params).await?, + )), + MmCoinEnum::EthCoin(eth) => Ok(GetNewAddressResponseEnum::Map( + eth.get_new_address_rpc_without_conf(req.params).await?, + )), _ => MmError::err(GetNewAddressRpcError::CoinIsActivatedNotWithHDWallet), } } @@ -383,24 +431,24 @@ pub async fn cancel_get_new_address( pub(crate) mod common_impl { use super::*; - use crate::coin_balance::{HDAddressBalanceScanner, HDWalletBalanceOps}; - use crate::hd_wallet::{HDAccountOps, HDWalletCoinOps, HDWalletOps}; - use crate::utxo::UtxoCommonOps; - use crate::{CoinWithDerivationMethod, HDAddress}; + use crate::coin_balance::{HDAddressBalanceScanner, HDWalletBalanceObject, HDWalletBalanceOps}; + use crate::hd_wallet::{HDAccountOps, HDAddressOps, HDCoinAddress, HDCoinHDAccount, HDWalletOps}; + use crate::CoinWithDerivationMethod; use crypto::RpcDerivationPath; use std::collections::HashSet; use std::fmt; + use std::fmt::Display; + use std::hash::Hash; use std::ops::DerefMut; /// TODO remove once GUI integrates `task::get_new_address::init`. pub async fn get_new_address_rpc_without_conf( coin: &Coin, params: GetNewAddressParams, - ) -> MmResult + ) -> MmResult>, GetNewAddressRpcError> where - Coin: - HDWalletBalanceOps + CoinWithDerivationMethod::HDWallet> + Sync + Send, - ::Address: fmt::Display, + Coin: HDWalletBalanceOps + CoinWithDerivationMethod + Sync + Send, + HDCoinAddress: fmt::Display, { let hd_wallet = coin.derivation_method().hd_wallet_or_err()?; @@ -414,21 +462,18 @@ pub(crate) mod common_impl { let gap_limit = params.gap_limit.unwrap_or_else(|| hd_wallet.gap_limit()); // Check if we can generate new address. - check_if_can_get_new_address(coin, hd_wallet, &hd_account, chain, gap_limit).await?; + check_if_can_get_new_address(coin, &hd_account, chain, gap_limit).await?; - let HDAddress { - address, - derivation_path, - .. - } = coin + let hd_address = coin .generate_new_address(hd_wallet, hd_account.deref_mut(), chain) .await?; + let address = hd_address.address(); let balance = coin.known_address_balance(&address).await?; Ok(GetNewAddressResponse { new_address: HDAddressBalance { address: address.to_string(), - derivation_path: RpcDerivationPath(derivation_path), + derivation_path: RpcDerivationPath(hd_address.derivation_path().clone()), chain, balance, }, @@ -439,15 +484,11 @@ pub(crate) mod common_impl { coin: &Coin, params: GetNewAddressParams, confirm_address: &ConfirmAddress, - ) -> MmResult + ) -> MmResult>, GetNewAddressRpcError> where ConfirmAddress: HDConfirmAddress, - Coin: UtxoCommonOps - + HDWalletBalanceOps - + CoinWithDerivationMethod::HDWallet> - + Send - + Sync, - ::Address: fmt::Display + Into + std::hash::Hash + std::cmp::Eq, + Coin: HDWalletBalanceOps + CoinWithDerivationMethod + Send + Sync, + HDCoinAddress: Display + Eq + Hash, { let hd_wallet = coin.derivation_method().hd_wallet_or_err()?; @@ -461,28 +502,22 @@ pub(crate) mod common_impl { let gap_limit = params.gap_limit.unwrap_or_else(|| hd_wallet.gap_limit()); // Check if we can generate new address. - check_if_can_get_new_address(coin, hd_wallet, &hd_account, chain, gap_limit).await?; + check_if_can_get_new_address(coin, &hd_account, chain, gap_limit).await?; - let HDAddress { - address, - derivation_path, - .. - } = coin + let hd_address = coin .generate_and_confirm_new_address(hd_wallet, &mut hd_account, chain, confirm_address) .await?; - + let address = hd_address.address(); let balance = coin.known_address_balance(&address).await?; - let address_as_string = address.to_string(); - - coin.prepare_addresses_for_balance_stream_if_enabled(HashSet::from([address])) + coin.prepare_addresses_for_balance_stream_if_enabled(HashSet::from([address.to_string()])) .await .map_err(|e| GetNewAddressRpcError::FailedScripthashSubscription(e.to_string()))?; Ok(GetNewAddressResponse { new_address: HDAddressBalance { - address: address_as_string, - derivation_path: RpcDerivationPath(derivation_path), + address: address.to_string(), + derivation_path: RpcDerivationPath(hd_address.derivation_path().clone()), chain, balance, }, @@ -491,21 +526,20 @@ pub(crate) mod common_impl { async fn check_if_can_get_new_address( coin: &Coin, - hd_wallet: &Coin::HDWallet, - hd_account: &Coin::HDAccount, + hd_account: &HDCoinHDAccount, chain: Bip44Chain, gap_limit: u32, ) -> MmResult<(), GetNewAddressRpcError> where Coin: HDWalletBalanceOps + Sync, - ::Address: fmt::Display, + HDCoinAddress: fmt::Display, { let known_addresses_number = hd_account.known_addresses_number(chain)?; if known_addresses_number == 0 || gap_limit > known_addresses_number { return Ok(()); } - let max_addresses_number = hd_wallet.address_limit(); + let max_addresses_number = hd_account.address_limit(); if known_addresses_number >= max_addresses_number { return MmError::err(GetNewAddressRpcError::AddressLimitReached { max_addresses_number }); } @@ -517,7 +551,7 @@ pub(crate) mod common_impl { let last_address_id = known_addresses_number - 1; for address_id in (0..=last_address_id).rev() { - let HDAddress { address, .. } = coin.derive_address(hd_account, chain, address_id).await?; + let address = coin.derive_address(hd_account, chain, address_id).await?.address(); if address_scanner.is_address_used(&address).await? { return Ok(()); } diff --git a/mm2src/coins/rpc_command/hd_account_balance_rpc_error.rs b/mm2src/coins/rpc_command/hd_account_balance_rpc_error.rs index dfb369f90e..2e3592cc2c 100644 --- a/mm2src/coins/rpc_command/hd_account_balance_rpc_error.rs +++ b/mm2src/coins/rpc_command/hd_account_balance_rpc_error.rs @@ -88,9 +88,7 @@ impl From for HDAccountBalanceRpcError { fn from(e: AddressDerivingError) -> Self { match e { AddressDerivingError::InvalidBip44Chain { chain } => HDAccountBalanceRpcError::InvalidBip44Chain { chain }, - AddressDerivingError::Bip32Error(bip32) => { - HDAccountBalanceRpcError::ErrorDerivingAddress(bip32.to_string()) - }, + AddressDerivingError::Bip32Error(bip32) => HDAccountBalanceRpcError::ErrorDerivingAddress(bip32), AddressDerivingError::Internal(internal) => HDAccountBalanceRpcError::Internal(internal), } } diff --git a/mm2src/coins/rpc_command/init_account_balance.rs b/mm2src/coins/rpc_command/init_account_balance.rs index 46df549783..94745f65e5 100644 --- a/mm2src/coins/rpc_command/init_account_balance.rs +++ b/mm2src/coins/rpc_command/init_account_balance.rs @@ -1,4 +1,4 @@ -use crate::coin_balance::HDAccountBalance; +use crate::coin_balance::{BalanceObjectOps, HDAccountBalance, HDAccountBalanceEnum}; use crate::rpc_command::hd_account_balance_rpc_error::HDAccountBalanceRpcError; use crate::{lp_coinfind_or_err, CoinsContext, MmCoinEnum}; use async_trait::async_trait; @@ -15,7 +15,7 @@ pub type AccountBalanceTaskManager = RpcTaskManager; pub type AccountBalanceTaskManagerShared = RpcTaskManagerShared; pub type InitAccountBalanceTaskHandleShared = RpcTaskHandleShared; pub type AccountBalanceRpcTaskStatus = RpcTaskStatus< - HDAccountBalance, + HDAccountBalanceEnum, HDAccountBalanceRpcError, AccountBalanceInProgressStatus, AccountBalanceAwaitingStatus, @@ -40,10 +40,12 @@ pub struct InitAccountBalanceParams { #[async_trait] pub trait InitAccountBalanceRpcOps { + type BalanceObject; + async fn init_account_balance_rpc( &self, params: InitAccountBalanceParams, - ) -> MmResult; + ) -> MmResult, HDAccountBalanceRpcError>; } pub struct InitAccountBalanceTask { @@ -52,7 +54,7 @@ pub struct InitAccountBalanceTask { } impl RpcTaskTypes for InitAccountBalanceTask { - type Item = HDAccountBalance; + type Item = HDAccountBalanceEnum; type Error = HDAccountBalanceRpcError; type InProgressStatus = AccountBalanceInProgressStatus; type AwaitingStatus = AccountBalanceAwaitingStatus; @@ -71,8 +73,15 @@ impl RpcTask for InitAccountBalanceTask { _task_handle: InitAccountBalanceTaskHandleShared, ) -> Result> { match self.coin { - MmCoinEnum::UtxoCoin(ref utxo) => utxo.init_account_balance_rpc(self.req.params.clone()).await, - MmCoinEnum::QtumCoin(ref qtum) => qtum.init_account_balance_rpc(self.req.params.clone()).await, + MmCoinEnum::UtxoCoin(ref utxo) => Ok(HDAccountBalanceEnum::Single( + utxo.init_account_balance_rpc(self.req.params.clone()).await?, + )), + MmCoinEnum::QtumCoin(ref qtum) => Ok(HDAccountBalanceEnum::Single( + qtum.init_account_balance_rpc(self.req.params.clone()).await?, + )), + MmCoinEnum::EthCoin(ref eth) => Ok(HDAccountBalanceEnum::Map( + eth.init_account_balance_rpc(self.req.params.clone()).await?, + )), _ => MmError::err(HDAccountBalanceRpcError::CoinIsActivatedNotWithHDWallet), } } @@ -119,19 +128,19 @@ pub async fn cancel_account_balance( pub mod common_impl { use super::*; - use crate::coin_balance::HDWalletBalanceOps; - use crate::hd_wallet::{HDAccountOps, HDWalletCoinOps, HDWalletOps}; - use crate::{CoinBalance, CoinWithDerivationMethod}; + use crate::coin_balance::{HDWalletBalanceObject, HDWalletBalanceOps}; + use crate::hd_wallet::{HDAccountOps, HDCoinAddress, HDWalletOps}; + use crate::CoinWithDerivationMethod; use crypto::RpcDerivationPath; use std::fmt; pub async fn init_account_balance_rpc( coin: &Coin, params: InitAccountBalanceParams, - ) -> MmResult + ) -> MmResult>, HDAccountBalanceRpcError> where - Coin: HDWalletBalanceOps + CoinWithDerivationMethod::HDWallet> + Sync, - ::Address: fmt::Display + Clone, + Coin: HDWalletBalanceOps + CoinWithDerivationMethod + Sync, + HDCoinAddress: fmt::Display + Clone, { let account_id = params.account_index; let hd_account = coin @@ -142,10 +151,12 @@ pub mod common_impl { .or_mm_err(|| HDAccountBalanceRpcError::UnknownAccount { account_id })?; let addresses = coin.all_known_addresses_balances(&hd_account).await?; + let total_balance = addresses .iter() - .fold(CoinBalance::default(), |total_balance, address_balance| { - total_balance + address_balance.balance.clone() + .fold(HDWalletBalanceObject::::new(), |mut total, addr_balance| { + total.add(addr_balance.balance.clone()); + total }); Ok(HDAccountBalance { diff --git a/mm2src/coins/rpc_command/init_create_account.rs b/mm2src/coins/rpc_command/init_create_account.rs index 6e8b47047d..cf0c819f6c 100644 --- a/mm2src/coins/rpc_command/init_create_account.rs +++ b/mm2src/coins/rpc_command/init_create_account.rs @@ -1,8 +1,7 @@ -use crate::coin_balance::HDAccountBalance; -use crate::hd_pubkey::{HDExtractPubkeyError, HDXPubExtractor, RpcTaskXPubExtractor}; -use crate::hd_wallet::NewAccountCreatingError; -use crate::{lp_coinfind_or_err, BalanceError, CoinBalance, CoinFindError, CoinWithDerivationMethod, CoinsContext, - MmCoinEnum, UnexpectedDerivationMethod}; +use crate::coin_balance::{BalanceObjectOps, HDAccountBalance, HDAccountBalanceEnum}; +use crate::hd_wallet::{HDExtractPubkeyError, HDXPubExtractor, NewAccountCreationError, RpcTaskXPubExtractor}; +use crate::{lp_coinfind_or_err, BalanceError, CoinFindError, CoinProtocol, CoinWithDerivationMethod, CoinsContext, + MarketCoinOps, MmCoinEnum, UnexpectedDerivationMethod}; use async_trait::async_trait; use common::{true_f, HttpStatusCode, SuccessResponse}; use crypto::hw_rpc_task::{HwConnectStatuses, HwRpcTaskAwaitingStatus, HwRpcTaskUserAction, HwRpcTaskUserActionRequest}; @@ -25,8 +24,12 @@ pub type CreateAccountAwaitingStatus = HwRpcTaskAwaitingStatus; pub type CreateAccountTaskManager = RpcTaskManager; pub type CreateAccountTaskManagerShared = RpcTaskManagerShared; pub type CreateAccountTaskHandleShared = RpcTaskHandleShared; -pub type CreateAccountRpcTaskStatus = - RpcTaskStatus; +pub type CreateAccountRpcTaskStatus = RpcTaskStatus< + HDAccountBalanceEnum, + CreateAccountRpcError, + CreateAccountInProgressStatus, + CreateAccountAwaitingStatus, +>; type CreateAccountXPubExtractor = RpcTaskXPubExtractor; @@ -79,24 +82,24 @@ impl From for CreateAccountRpcError { } } -impl From for CreateAccountRpcError { - fn from(e: NewAccountCreatingError) -> Self { +impl From for CreateAccountRpcError { + fn from(e: NewAccountCreationError) -> Self { match e { - NewAccountCreatingError::HwContextNotInitialized => CreateAccountRpcError::HwContextNotInitialized, - NewAccountCreatingError::HDWalletUnavailable => CreateAccountRpcError::CoinIsActivatedNotWithHDWallet, - NewAccountCreatingError::CoinDoesntSupportTrezor => { + NewAccountCreationError::HwContextNotInitialized => CreateAccountRpcError::HwContextNotInitialized, + NewAccountCreationError::HDWalletUnavailable => CreateAccountRpcError::CoinIsActivatedNotWithHDWallet, + NewAccountCreationError::CoinDoesntSupportTrezor => { CreateAccountRpcError::Internal("Coin must support Trezor at this point".to_string()) }, - NewAccountCreatingError::RpcTaskError(rpc) => CreateAccountRpcError::from(rpc), - NewAccountCreatingError::HardwareWalletError(hw) => CreateAccountRpcError::from(hw), - NewAccountCreatingError::AccountLimitReached { max_accounts_number } => { + NewAccountCreationError::RpcTaskError(rpc) => CreateAccountRpcError::from(rpc), + NewAccountCreationError::HardwareWalletError(hw) => CreateAccountRpcError::from(hw), + NewAccountCreationError::AccountLimitReached { max_accounts_number } => { CreateAccountRpcError::AccountLimitReached { max_accounts_number } }, - NewAccountCreatingError::ErrorSavingAccountToStorage(e) => { + NewAccountCreationError::ErrorSavingAccountToStorage(e) => { let error = format!("Error uploading HD account info to the storage: {}", e); CreateAccountRpcError::WalletStorageError(error) }, - NewAccountCreatingError::Internal(internal) => CreateAccountRpcError::Internal(internal), + NewAccountCreationError::Internal(internal) => CreateAccountRpcError::Internal(internal), } } } @@ -115,7 +118,7 @@ impl From for CreateAccountRpcError { } impl From for CreateAccountRpcError { - fn from(e: HDExtractPubkeyError) -> Self { CreateAccountRpcError::from(NewAccountCreatingError::from(e)) } + fn from(e: HDExtractPubkeyError) -> Self { CreateAccountRpcError::from(NewAccountCreationError::from(e)) } } impl From for CreateAccountRpcError { @@ -170,6 +173,7 @@ pub struct CreateNewAccountParams { // The max number of empty addresses in a row. // If transactions were sent to an address outside the `gap_limit`, they will not be identified. gap_limit: Option, + account_id: Option, } #[derive(Clone, Serialize)] @@ -199,14 +203,16 @@ impl CreateAccountState { #[async_trait] pub trait InitCreateAccountRpcOps { + type BalanceObject; + async fn init_create_account_rpc( &self, params: CreateNewAccountParams, state: CreateAccountState, - xpub_extractor: &XPubExtractor, - ) -> MmResult + xpub_extractor: Option, + ) -> MmResult, CreateAccountRpcError> where - XPubExtractor: HDXPubExtractor; + XPubExtractor: HDXPubExtractor + Send; async fn revert_creating_account(&self, account_id: u32); } @@ -221,7 +227,7 @@ pub struct InitCreateAccountTask { } impl RpcTaskTypes for InitCreateAccountTask { - type Item = HDAccountBalance; + type Item = HDAccountBalanceEnum; type Error = CreateAccountRpcError; type InProgressStatus = CreateAccountInProgressStatus; type AwaitingStatus = CreateAccountAwaitingStatus; @@ -238,6 +244,7 @@ impl RpcTask for InitCreateAccountTask { match self.coin { MmCoinEnum::UtxoCoin(utxo) => utxo.revert_creating_account(account_id).await, MmCoinEnum::QtumCoin(qtum) => qtum.revert_creating_account(account_id).await, + MmCoinEnum::EthCoin(eth) => eth.revert_creating_account(account_id).await, _ => (), } }; @@ -250,44 +257,71 @@ impl RpcTask for InitCreateAccountTask { params: CreateNewAccountParams, state: CreateAccountState, task_handle: CreateAccountTaskHandleShared, - ) -> MmResult + is_trezor: bool, + coin_protocol: CoinProtocol, + ) -> MmResult::BalanceObject>, CreateAccountRpcError> where Coin: InitCreateAccountRpcOps + Send + Sync, { - let hw_statuses = HwConnectStatuses { - on_connect: CreateAccountInProgressStatus::WaitingForTrezorToConnect, - on_connected: CreateAccountInProgressStatus::Preparing, - on_connection_failed: CreateAccountInProgressStatus::Finishing, - on_button_request: CreateAccountInProgressStatus::FollowHwDeviceInstructions, - on_pin_request: CreateAccountAwaitingStatus::EnterTrezorPin, - on_passphrase_request: CreateAccountAwaitingStatus::EnterTrezorPassphrase, - on_ready: CreateAccountInProgressStatus::RequestingAccountBalance, + let xpub_extractor = if is_trezor { + let hw_statuses = HwConnectStatuses { + on_connect: CreateAccountInProgressStatus::WaitingForTrezorToConnect, + on_connected: CreateAccountInProgressStatus::Preparing, + on_connection_failed: CreateAccountInProgressStatus::Finishing, + on_button_request: CreateAccountInProgressStatus::FollowHwDeviceInstructions, + on_pin_request: CreateAccountAwaitingStatus::EnterTrezorPin, + on_passphrase_request: CreateAccountAwaitingStatus::EnterTrezorPassphrase, + on_ready: CreateAccountInProgressStatus::RequestingAccountBalance, + }; + Some(CreateAccountXPubExtractor::new_trezor_extractor( + ctx, + task_handle, + hw_statuses, + coin_protocol, + )?) + } else { + None }; - let xpub_extractor = CreateAccountXPubExtractor::new(ctx, task_handle, hw_statuses)?; - coin.init_create_account_rpc(params, state, &xpub_extractor).await + coin.init_create_account_rpc(params, state, xpub_extractor).await } match self.coin { - MmCoinEnum::UtxoCoin(ref utxo) => { + MmCoinEnum::UtxoCoin(ref utxo) => Ok(HDAccountBalanceEnum::Single( create_new_account_helper( &self.ctx, utxo, self.req.params.clone(), self.task_state.clone(), task_handle, + utxo.is_trezor(), + CoinProtocol::UTXO, ) - .await - }, - MmCoinEnum::QtumCoin(ref qtum) => { + .await?, + )), + MmCoinEnum::QtumCoin(ref qtum) => Ok(HDAccountBalanceEnum::Single( create_new_account_helper( &self.ctx, qtum, self.req.params.clone(), self.task_state.clone(), task_handle, + qtum.is_trezor(), + CoinProtocol::QTUM, ) - .await - }, + .await?, + )), + MmCoinEnum::EthCoin(ref eth) => Ok(HDAccountBalanceEnum::Map( + create_new_account_helper( + &self.ctx, + eth, + self.req.params.clone(), + self.task_state.clone(), + task_handle, + eth.is_trezor(), + CoinProtocol::ETH, + ) + .await?, + )), _ => MmError::err(CreateAccountRpcError::CoinIsActivatedNotWithHDWallet), } } @@ -352,23 +386,29 @@ pub async fn cancel_create_new_account( pub(crate) mod common_impl { use super::*; - use crate::coin_balance::HDWalletBalanceOps; - use crate::hd_wallet::{HDAccountOps, HDWalletCoinOps, HDWalletOps}; + use crate::coin_balance::{HDWalletBalanceObject, HDWalletBalanceOps}; + use crate::hd_wallet::{create_new_account, ExtractExtendedPubkey, HDAccountOps, HDAccountStorageOps, + HDCoinHDAccount, HDWalletOps}; + use crypto::Secp256k1ExtendedPublicKey; pub async fn init_create_new_account_rpc<'a, Coin, XPubExtractor>( coin: &Coin, params: CreateNewAccountParams, state: CreateAccountState, - xpub_extractor: &XPubExtractor, - ) -> MmResult + xpub_extractor: Option, + ) -> MmResult>, CreateAccountRpcError> where - Coin: - HDWalletBalanceOps + CoinWithDerivationMethod::HDWallet> + Send + Sync, - XPubExtractor: HDXPubExtractor, + Coin: ExtractExtendedPubkey + + HDWalletBalanceOps + + CoinWithDerivationMethod + + Send + + Sync, + XPubExtractor: HDXPubExtractor + Send, + HDCoinHDAccount: HDAccountStorageOps, { let hd_wallet = coin.derivation_method().hd_wallet_or_err()?; - let mut new_account = coin.create_new_account(hd_wallet, xpub_extractor).await?; + let mut new_account = create_new_account(coin, hd_wallet, xpub_extractor, params.account_id).await?; let account_index = new_account.account_id(); let account_derivation_path = new_account.account_derivation_path(); @@ -386,8 +426,9 @@ pub(crate) mod common_impl { let total_balance = addresses .iter() - .fold(CoinBalance::default(), |total_balance, address_balance| { - total_balance + address_balance.balance.clone() + .fold(HDWalletBalanceObject::::new(), |mut total, addr_balance| { + total.add(addr_balance.balance.clone()); + total }); Ok(HDAccountBalance { @@ -400,10 +441,59 @@ pub(crate) mod common_impl { pub async fn revert_creating_account(coin: &Coin, account_id: u32) where - Coin: HDWalletBalanceOps + CoinWithDerivationMethod::HDWallet> + Sync, + Coin: HDWalletBalanceOps + CoinWithDerivationMethod + Sync, { if let Some(hd_wallet) = coin.derivation_method().hd_wallet() { hd_wallet.remove_account_if_last(account_id).await; } } } + +#[cfg(all(feature = "for-tests", not(target_arch = "wasm32")))] +pub mod for_tests { + use super::{init_create_new_account, init_create_new_account_status, CreateAccountRpcError}; + use crate::coin_balance::HDAccountBalanceEnum; + use common::executor::Timer; + use common::{now_ms, wait_until_ms}; + use mm2_core::mm_ctx::MmArc; + use mm2_err_handle::prelude::MmResult; + use rpc_task::rpc_common::RpcTaskStatusRequest; + use rpc_task::RpcTaskStatus; + use serde_json::{self, json}; + + /// Helper to call init_create_new_account fn and wait for completion + pub async fn test_create_new_account_init_loop( + ctx: MmArc, + ticker: &str, + account_id: Option, + ) -> MmResult { + let req = serde_json::from_value(json!({ + "coin": ticker, + "params": { + "account_id": account_id + } + })) + .unwrap(); + let init = init_create_new_account(ctx.clone(), req).await.unwrap(); + let timeout = wait_until_ms(150000); + loop { + if now_ms() > timeout { + panic!("{} init_withdraw timed out", ticker); + } + let status = init_create_new_account_status(ctx.clone(), RpcTaskStatusRequest { + task_id: init.task_id, + forget_if_finished: true, + }) + .await; + if let Ok(status) = status { + match status { + RpcTaskStatus::Ok(account_balance) => break Ok(account_balance), + RpcTaskStatus::Error(e) => break Err(e), + _ => Timer::sleep(1.).await, + } + } else { + panic!("{} could not get withdraw_status", ticker) + } + } + } +} diff --git a/mm2src/coins/rpc_command/init_scan_for_new_addresses.rs b/mm2src/coins/rpc_command/init_scan_for_new_addresses.rs index b90866d6b2..4eabda46e9 100644 --- a/mm2src/coins/rpc_command/init_scan_for_new_addresses.rs +++ b/mm2src/coins/rpc_command/init_scan_for_new_addresses.rs @@ -1,7 +1,6 @@ use crate::coin_balance::HDAddressBalance; use crate::rpc_command::hd_account_balance_rpc_error::HDAccountBalanceRpcError; -use crate::utxo::utxo_common; -use crate::{lp_coinfind_or_err, CoinsContext, MmCoinEnum}; +use crate::{lp_coinfind_or_err, CoinBalance, CoinBalanceMap, CoinsContext, MmCoinEnum}; use async_trait::async_trait; use common::{SerdeInfallible, SuccessResponse}; use crypto::RpcDerivationPath; @@ -17,17 +16,26 @@ pub type ScanAddressesTaskManager = RpcTaskManager; pub type ScanAddressesTaskManagerShared = RpcTaskManagerShared; pub type ScanAddressesTaskHandleShared = RpcTaskHandleShared; pub type ScanAddressesRpcTaskStatus = RpcTaskStatus< - ScanAddressesResponse, + ScanAddressesResponseEnum, HDAccountBalanceRpcError, ScanAddressesInProgressStatus, ScanAddressesAwaitingStatus, >; +/// Generic response for the `scan_for_new_addresses` RPC commands. #[derive(Clone, Debug, PartialEq, Serialize)] -pub struct ScanAddressesResponse { +pub struct ScanAddressesResponse { pub account_index: u32, pub derivation_path: RpcDerivationPath, - pub new_addresses: Vec, + pub new_addresses: Vec>, +} + +/// Enum for the response of the `scan_for_new_addresses` RPC commands. +#[derive(Clone, Debug, PartialEq, Serialize)] +#[serde(untagged)] +pub enum ScanAddressesResponseEnum { + Single(ScanAddressesResponse), + Map(ScanAddressesResponse), } #[derive(Deserialize)] @@ -50,12 +58,15 @@ pub enum ScanAddressesInProgressStatus { InProgress, } +/// Trait for the `scan_for_new_addresses` RPC commands. #[async_trait] pub trait InitScanAddressesRpcOps { + type BalanceObject; + async fn init_scan_for_new_addresses_rpc( &self, params: ScanAddressesParams, - ) -> MmResult; + ) -> MmResult, HDAccountBalanceRpcError>; } pub struct InitScanAddressesTask { @@ -64,7 +75,7 @@ pub struct InitScanAddressesTask { } impl RpcTaskTypes for InitScanAddressesTask { - type Item = ScanAddressesResponse; + type Item = ScanAddressesResponseEnum; type Error = HDAccountBalanceRpcError; type InProgressStatus = ScanAddressesInProgressStatus; type AwaitingStatus = ScanAddressesAwaitingStatus; @@ -81,8 +92,15 @@ impl RpcTask for InitScanAddressesTask { async fn run(&mut self, _task_handle: ScanAddressesTaskHandleShared) -> Result> { match self.coin { - MmCoinEnum::UtxoCoin(ref utxo) => utxo.init_scan_for_new_addresses_rpc(self.req.params.clone()).await, - MmCoinEnum::QtumCoin(ref qtum) => qtum.init_scan_for_new_addresses_rpc(self.req.params.clone()).await, + MmCoinEnum::UtxoCoin(ref utxo) => Ok(ScanAddressesResponseEnum::Single( + utxo.init_scan_for_new_addresses_rpc(self.req.params.clone()).await?, + )), + MmCoinEnum::QtumCoin(ref qtum) => Ok(ScanAddressesResponseEnum::Single( + qtum.init_scan_for_new_addresses_rpc(self.req.params.clone()).await?, + )), + MmCoinEnum::EthCoin(ref eth) => Ok(ScanAddressesResponseEnum::Map( + eth.init_scan_for_new_addresses_rpc(self.req.params.clone()).await?, + )), _ => MmError::err(HDAccountBalanceRpcError::CoinIsActivatedNotWithHDWallet), } } @@ -129,9 +147,8 @@ pub async fn cancel_scan_for_new_addresses( pub mod common_impl { use super::*; - use crate::coin_balance::HDWalletBalanceOps; - use crate::hd_wallet::{HDAccountOps, HDWalletCoinOps, HDWalletOps}; - use crate::utxo::UtxoCommonOps; + use crate::coin_balance::{HDWalletBalanceObject, HDWalletBalanceOps}; + use crate::hd_wallet::{HDAccountOps, HDWalletOps}; use crate::CoinWithDerivationMethod; use std::collections::HashSet; use std::ops::DerefMut; @@ -139,13 +156,9 @@ pub mod common_impl { pub async fn scan_for_new_addresses_rpc( coin: &Coin, params: ScanAddressesParams, - ) -> MmResult + ) -> MmResult>, HDAccountBalanceRpcError> where - Coin: UtxoCommonOps - + CoinWithDerivationMethod::HDWallet> - + HDWalletBalanceOps - + Sync, - HashSet<::Address>: From>, + Coin: CoinWithDerivationMethod + HDWalletBalanceOps + Sync, { let hd_wallet = coin.derivation_method().hd_wallet_or_err()?; @@ -164,12 +177,10 @@ pub mod common_impl { let addresses: HashSet<_> = new_addresses .iter() - .map(|address_balance| { - utxo_common::address_from_str_unchecked(coin.as_ref(), &address_balance.address).expect("Valid address") - }) + .map(|address_balance| address_balance.address.clone()) .collect(); - coin.prepare_addresses_for_balance_stream_if_enabled(addresses.into()) + coin.prepare_addresses_for_balance_stream_if_enabled(addresses) .await .map_err(|e| HDAccountBalanceRpcError::FailedScripthashSubscription(e.to_string()))?; diff --git a/mm2src/coins/rpc_command/init_withdraw.rs b/mm2src/coins/rpc_command/init_withdraw.rs index 2483511925..43b86cf19a 100644 --- a/mm2src/coins/rpc_command/init_withdraw.rs +++ b/mm2src/coins/rpc_command/init_withdraw.rs @@ -133,6 +133,7 @@ impl RpcTask for WithdrawTask { MmCoinEnum::UtxoCoin(ref standard_utxo) => standard_utxo.init_withdraw(ctx, request, task_handle).await, MmCoinEnum::QtumCoin(ref qtum) => qtum.init_withdraw(ctx, request, task_handle).await, MmCoinEnum::ZCoin(ref z) => z.init_withdraw(ctx, request, task_handle).await, + MmCoinEnum::EthCoin(ref eth) => eth.init_withdraw(ctx, request, task_handle).await, _ => MmError::err(WithdrawError::CoinDoesntSupportInitWithdraw { coin: self.coin.ticker().to_owned(), }), diff --git a/mm2src/coins/rpc_command/lightning/open_channel.rs b/mm2src/coins/rpc_command/lightning/open_channel.rs index f0e7b48bd7..fdb5b9caa9 100644 --- a/mm2src/coins/rpc_command/lightning/open_channel.rs +++ b/mm2src/coins/rpc_command/lightning/open_channel.rs @@ -146,8 +146,8 @@ pub async fn open_channel(ctx: MmArc, req: OpenChannelRequest) -> OpenChannelRes let platform_coin = ln_coin.platform_coin().clone(); let decimals = platform_coin.as_ref().decimals; - let my_address = platform_coin.as_ref().derivation_method.single_addr_or_err()?; - let (unspents, _) = platform_coin.get_unspent_ordered_list(my_address).await?; + let my_address = platform_coin.as_ref().derivation_method.single_addr_or_err().await?; + let (unspents, _) = platform_coin.get_unspent_ordered_list(&my_address).await?; let (value, fee_policy) = match req.amount.clone() { ChannelOpenAmount::Max => ( unspents.iter().fold(0, |sum, unspent| sum + unspent.value), @@ -168,6 +168,7 @@ pub async fn open_channel(ctx: MmArc, req: OpenChannelRequest) -> OpenChannelRes let outputs = vec![TransactionOutput { value, script_pubkey }]; let mut tx_builder = UtxoTxBuilder::new(&platform_coin) + .await .add_available_inputs(unspents) .add_outputs(outputs) .with_fee_policy(fee_policy); diff --git a/mm2src/coins/solana.rs b/mm2src/coins/solana.rs index e209c02172..b897503006 100644 --- a/mm2src/coins/solana.rs +++ b/mm2src/coins/solana.rs @@ -1,5 +1,6 @@ use super::{CoinBalance, HistorySyncState, MarketCoinOps, MmCoin, SwapOps, TradeFee, TransactionEnum, WatcherOps}; use crate::coin_errors::{MyAddressError, ValidatePaymentResult}; +use crate::hd_wallet::HDPathAccountToAddressId; use crate::solana::solana_common::{lamports_to_sol, PrepareTransferData, SufficientBalanceError}; use crate::solana::spl::SplTokenInfo; use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinFutSpawner, ConfirmPaymentInput, DexFee, @@ -20,7 +21,7 @@ use base58::ToBase58; use bincode::{deserialize, serialize}; use common::executor::{abortable_queue::AbortableQueue, AbortableSystem, AbortedError}; use common::{async_blocking, now_sec}; -use crypto::{StandardHDCoinAddress, StandardHDPathToCoin}; +use crypto::HDPathToCoin; use derive_more::Display; use futures::{FutureExt, TryFutureExt}; use futures01::Future; @@ -141,7 +142,7 @@ pub struct SolanaActivationParams { confirmation_commitment: CommitmentLevel, client_url: String, #[serde(default)] - path_to_address: StandardHDCoinAddress, + path_to_address: HDPathAccountToAddressId, } #[derive(Debug, Display)] @@ -189,8 +190,9 @@ pub async fn solana_coin_with_policy( let priv_key = match priv_key_policy { PrivKeyBuildPolicy::IguanaPrivKey(priv_key) => priv_key, PrivKeyBuildPolicy::GlobalHDAccount(global_hd) => { - let derivation_path: StandardHDPathToCoin = try_s!(json::from_value(conf["derivation_path"].clone())); - try_s!(global_hd.derive_secp256k1_secret(&derivation_path, ¶ms.path_to_address)) + let path_to_coin: HDPathToCoin = try_s!(json::from_value(conf["derivation_path"].clone())); + let derivation_path = try_s!(params.path_to_address.to_derivation_path(&path_to_coin)); + try_s!(global_hd.derive_secp256k1_secret(&derivation_path)) }, PrivKeyBuildPolicy::Trezor => return ERR!("{}", PrivKeyPolicyNotAllowed::HardwareWalletNotSupported), }; @@ -385,7 +387,7 @@ impl MarketCoinOps for SolanaCoin { fn my_address(&self) -> MmResult { Ok(self.my_address.clone()) } - fn get_public_key(&self) -> Result> { unimplemented!() } + async fn get_public_key(&self) -> Result> { unimplemented!() } fn sign_message_hash(&self, _message: &str) -> Option<[u8; 32]> { unimplemented!() } @@ -473,6 +475,8 @@ impl MarketCoinOps for SolanaCoin { fn min_tx_amount(&self) -> BigDecimal { BigDecimal::from(0) } fn min_trading_vol(&self) -> MmNumber { MmNumber::from("0.00777") } + + fn is_trezor(&self) -> bool { unimplemented!() } } #[async_trait] @@ -483,11 +487,17 @@ impl SwapOps for SolanaCoin { fn send_taker_payment(&self, _taker_payment_args: SendPaymentArgs) -> TransactionFut { unimplemented!() } - fn send_maker_spends_taker_payment(&self, _maker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { + async fn send_maker_spends_taker_payment( + &self, + _maker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { unimplemented!() } - fn send_taker_spends_maker_payment(&self, _taker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { + async fn send_taker_spends_maker_payment( + &self, + _taker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { unimplemented!() } diff --git a/mm2src/coins/solana/spl.rs b/mm2src/coins/solana/spl.rs index 253b1187c0..bfecd9351f 100644 --- a/mm2src/coins/solana/spl.rs +++ b/mm2src/coins/solana/spl.rs @@ -237,7 +237,7 @@ impl MarketCoinOps for SplToken { fn my_address(&self) -> MmResult { Ok(self.platform_coin.my_address.clone()) } - fn get_public_key(&self) -> Result> { unimplemented!() } + async fn get_public_key(&self) -> Result> { unimplemented!() } fn sign_message_hash(&self, _message: &str) -> Option<[u8; 32]> { unimplemented!() } @@ -294,6 +294,8 @@ impl MarketCoinOps for SplToken { fn min_tx_amount(&self) -> BigDecimal { BigDecimal::from(0) } fn min_trading_vol(&self) -> MmNumber { MmNumber::from("0.00777") } + + fn is_trezor(&self) -> bool { self.platform_coin.is_trezor() } } #[async_trait] @@ -304,11 +306,17 @@ impl SwapOps for SplToken { fn send_taker_payment(&self, _taker_payment_args: SendPaymentArgs) -> TransactionFut { unimplemented!() } - fn send_maker_spends_taker_payment(&self, _maker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { + async fn send_maker_spends_taker_payment( + &self, + _maker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { unimplemented!() } - fn send_taker_spends_maker_payment(&self, _taker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { + async fn send_taker_spends_maker_payment( + &self, + _taker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { unimplemented!() } diff --git a/mm2src/coins/tendermint/tendermint_balance_events.rs b/mm2src/coins/tendermint/tendermint_balance_events.rs index 3b876b550b..1a55dfa768 100644 --- a/mm2src/coins/tendermint/tendermint_balance_events.rs +++ b/mm2src/coins/tendermint/tendermint_balance_events.rs @@ -7,7 +7,7 @@ use jsonrpc_core::MethodCall; use jsonrpc_core::{Id as RpcId, Params as RpcParams, Value as RpcValue, Version as RpcVersion}; use mm2_core::mm_ctx::MmArc; use mm2_event_stream::{behaviour::{EventBehaviour, EventInitStatus}, - Event, EventStreamConfiguration}; + ErrorEventName, Event, EventName, EventStreamConfiguration}; use mm2_number::BigDecimal; use std::collections::{HashMap, HashSet}; @@ -16,8 +16,9 @@ use crate::{tendermint::TendermintCommons, utxo::utxo_common::big_decimal_from_s #[async_trait] impl EventBehaviour for TendermintCoin { - const EVENT_NAME: &'static str = "COIN_BALANCE"; - const ERROR_EVENT_NAME: &'static str = "COIN_BALANCE_ERROR"; + fn event_name() -> EventName { EventName::CoinBalance } + + fn error_event_name() -> ErrorEventName { ErrorEventName::CoinBalanceError } async fn handle(self, _interval: f64, tx: oneshot::Sender) { fn generate_subscription_query(query_filter: String) -> String { @@ -125,7 +126,7 @@ impl EventBehaviour for TendermintCoin { let e = serde_json::to_value(e).expect("Serialization should't fail."); ctx.stream_channel_controller .broadcast(Event::new( - format!("{}:{}", Self::ERROR_EVENT_NAME, ticker), + format!("{}:{}", Self::error_event_name(), ticker), e.to_string(), )) .await; @@ -160,7 +161,7 @@ impl EventBehaviour for TendermintCoin { if !balance_updates.is_empty() { ctx.stream_channel_controller .broadcast(Event::new( - Self::EVENT_NAME.to_string(), + Self::event_name().to_string(), json!(balance_updates).to_string(), )) .await; @@ -171,18 +172,21 @@ impl EventBehaviour for TendermintCoin { } async fn spawn_if_active(self, config: &EventStreamConfiguration) -> EventInitStatus { - if let Some(event) = config.get_event(Self::EVENT_NAME) { + if let Some(event) = config.get_event(&Self::event_name()) { log::info!( "{} event is activated for {}. `stream_interval_seconds`({}) has no effect on this.", - Self::EVENT_NAME, + Self::event_name(), self.ticker(), event.stream_interval_seconds ); let (tx, rx): (Sender, Receiver) = oneshot::channel(); let fut = self.clone().handle(event.stream_interval_seconds, tx); - let settings = - AbortSettings::info_on_abort(format!("{} event is stopped for {}.", Self::EVENT_NAME, self.ticker())); + let settings = AbortSettings::info_on_abort(format!( + "{} event is stopped for {}.", + Self::event_name(), + self.ticker() + )); self.spawner().spawn_with_settings(fut, settings); rx.await.unwrap_or_else(|e| { diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index bee0848a13..cee6fc0f75 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -5,6 +5,7 @@ use super::ibc::transfer_v1::MsgTransfer; use super::ibc::IBC_GAS_LIMIT_DEFAULT; use super::rpc::*; use crate::coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentResult}; +use crate::hd_wallet::HDPathAccountToAddressId; use crate::rpc_command::tendermint::{IBCChainRegistriesResponse, IBCChainRegistriesResult, IBCChainsRequestError, IBCTransferChannel, IBCTransferChannelTag, IBCTransferChannelsRequest, IBCTransferChannelsRequestError, IBCTransferChannelsResponse, @@ -27,7 +28,7 @@ use crate::{big_decimal_from_sat_unsigned, BalanceError, BalanceFut, BigDecimal, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, ValidateWatcherSpendInput, VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, - WatcherValidateTakerFeeInput, WithdrawError, WithdrawFee, WithdrawFrom, WithdrawFut, WithdrawRequest}; + WatcherValidateTakerFeeInput, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest}; use async_std::prelude::FutureExt as AsyncStdFutureExt; use async_trait::async_trait; use bitcrypto::{dhash160, sha256}; @@ -51,7 +52,7 @@ use cosmrs::tendermint::PublicKey; use cosmrs::tx::{self, Fee, Msg, Raw, SignDoc, SignerInfo}; use cosmrs::{AccountId, Any, Coin, Denom, ErrorReport}; use crypto::privkey::key_pair_from_secret; -use crypto::{Secp256k1Secret, StandardHDCoinAddress, StandardHDPathToCoin}; +use crypto::{HDPathToCoin, Secp256k1Secret}; use derive_more::Display; use futures::future::try_join_all; use futures::lock::Mutex as AsyncMutex; @@ -114,7 +115,7 @@ pub trait TendermintCommons { async fn get_block_timestamp(&self, block: i64) -> MmResult, TendermintCoinRpcError>; - async fn all_balances(&self) -> MmResult; + async fn get_all_balances(&self) -> MmResult; async fn rpc_client(&self) -> MmResult; } @@ -150,7 +151,7 @@ pub struct TendermintConf { /// This derivation path consists of `purpose` and `coin_type` only /// where the full `BIP44` address has the following structure: /// `m/purpose'/coin_type'/account'/change/address_index`. - derivation_path: Option, + derivation_path: Option, } impl TendermintConf { @@ -248,13 +249,13 @@ impl Deref for TendermintCoin { fn deref(&self) -> &Self::Target { &self.0 } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct TendermintInitError { pub ticker: String, pub kind: TendermintInitErrorKind, } -#[derive(Display, Debug)] +#[derive(Display, Debug, Clone)] pub enum TendermintInitErrorKind { Internal(String), InvalidPrivKey(String), @@ -263,6 +264,7 @@ pub enum TendermintInitErrorKind { RpcClientInitError(String), InvalidChainId(String), InvalidDenom(String), + InvalidPathToAddress(String), #[display(fmt = "'derivation_path' field is not found in config")] DerivationPathIsNotSet, #[display(fmt = "'account' field is not found in config")] @@ -449,7 +451,7 @@ impl TendermintCommons for TendermintCoin { Ok(u64::try_from(timestamp.seconds).ok()) } - async fn all_balances(&self) -> MmResult { + async fn get_all_balances(&self) -> MmResult { let platform_balance_denom = self .account_balance_for_denom(&self.account_id, self.denom.to_string()) .await?; @@ -571,20 +573,16 @@ impl TendermintCoin { AccountId::from_str(&req.to).map_to_mm(|e| WithdrawError::InvalidAddress(e.to_string()))?; let (account_id, priv_key) = match req.from { - Some(WithdrawFrom::HDWalletAddress(ref path_to_address)) => { + Some(from) => { + let path_to_coin = coin.priv_key_policy.path_to_coin_or_err()?; + let path_to_address = from.to_address_path(path_to_coin.coin_type())?; let priv_key = coin .priv_key_policy - .hd_wallet_derived_priv_key_or_err(path_to_address)?; + .hd_wallet_derived_priv_key_or_err(&path_to_address.to_derivation_path(path_to_coin)?)?; let account_id = account_id_from_privkey(priv_key.as_slice(), &coin.account_prefix) .map_err(|e| WithdrawError::InternalError(e.to_string()))?; (account_id, priv_key) }, - Some(WithdrawFrom::AddressId(_)) | Some(WithdrawFrom::DerivationPath { .. }) => { - return MmError::err(WithdrawError::UnexpectedFromAddress( - "Withdraw from 'AddressId' or 'DerivationPath' is not supported yet for Tendermint!" - .to_string(), - )) - }, None => (coin.account_id.clone(), *coin.priv_key_policy.activated_key_or_err()?), }; @@ -1958,20 +1956,16 @@ impl MmCoin for TendermintCoin { } let (account_id, priv_key) = match req.from { - Some(WithdrawFrom::HDWalletAddress(ref path_to_address)) => { + Some(from) => { + let path_to_coin = coin.priv_key_policy.path_to_coin_or_err()?; + let path_to_address = from.to_address_path(path_to_coin.coin_type())?; let priv_key = coin .priv_key_policy - .hd_wallet_derived_priv_key_or_err(path_to_address)?; + .hd_wallet_derived_priv_key_or_err(&path_to_address.to_derivation_path(path_to_coin)?)?; let account_id = account_id_from_privkey(priv_key.as_slice(), &coin.account_prefix) .map_err(|e| WithdrawError::InternalError(e.to_string()))?; (account_id, priv_key) }, - Some(WithdrawFrom::AddressId(_)) | Some(WithdrawFrom::DerivationPath { .. }) => { - return MmError::err(WithdrawError::UnexpectedFromAddress( - "Withdraw from 'AddressId' or 'DerivationPath' is not supported yet for Tendermint!" - .to_string(), - )) - }, None => (coin.account_id.clone(), *coin.priv_key_policy.activated_key_or_err()?), }; @@ -2255,7 +2249,7 @@ impl MarketCoinOps for TendermintCoin { fn my_address(&self) -> MmResult { Ok(self.account_id.to_string()) } - fn get_public_key(&self) -> Result> { + async fn get_public_key(&self) -> Result> { let key = SigningKey::from_slice(self.priv_key_policy.activated_key_or_err()?.as_slice()) .expect("privkey validity is checked on coin creation"); Ok(key.public_key().to_string()) @@ -2461,6 +2455,8 @@ impl MarketCoinOps for TendermintCoin { #[inline] fn min_trading_vol(&self) -> MmNumber { self.min_tx_amount().into() } + + fn is_trezor(&self) -> bool { self.priv_key_policy.is_trezor() } } #[async_trait] @@ -2498,15 +2494,18 @@ impl SwapOps for TendermintCoin { ) } - fn send_maker_spends_taker_payment(&self, maker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { - let tx = try_tx_fus!(cosmrs::Tx::from_bytes(maker_spends_payment_args.other_payment_tx)); - let msg = try_tx_fus!(tx.body.messages.first().ok_or("Tx body couldn't be read.")); + async fn send_maker_spends_taker_payment( + &self, + maker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { + let tx = try_tx_s!(cosmrs::Tx::from_bytes(maker_spends_payment_args.other_payment_tx)); + let msg = try_tx_s!(tx.body.messages.first().ok_or("Tx body couldn't be read.")); - let htlc_proto = try_tx_fus!(CreateHtlcProto::decode( - try_tx_fus!(HtlcType::from_str(&self.account_prefix)), + let htlc_proto = try_tx_s!(CreateHtlcProto::decode( + try_tx_s!(HtlcType::from_str(&self.account_prefix)), msg.value.as_slice() )); - let htlc = try_tx_fus!(CreateHtlcMsg::try_from(htlc_proto)); + let htlc = try_tx_s!(CreateHtlcMsg::try_from(htlc_proto)); let mut amount = htlc.amount().to_vec(); amount.sort(); @@ -2520,50 +2519,48 @@ impl SwapOps for TendermintCoin { let htlc_id = self.calculate_htlc_id(htlc.sender(), htlc.to(), &amount, maker_spends_payment_args.secret_hash); - let claim_htlc_tx = try_tx_fus!(self.gen_claim_htlc_tx(htlc_id, maker_spends_payment_args.secret)); - let coin = self.clone(); + let claim_htlc_tx = try_tx_s!(self.gen_claim_htlc_tx(htlc_id, maker_spends_payment_args.secret)); - let fut = async move { - let current_block = try_tx_s!(coin.current_block().compat().await); - let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; - - let fee = try_tx_s!( - coin.calculate_fee( - claim_htlc_tx.msg_payload.clone(), - timeout_height, - TX_DEFAULT_MEMO.to_owned(), - None - ) - .await - ); + let current_block = try_tx_s!(self.current_block().compat().await); + let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; - let (_tx_id, tx_raw) = try_tx_s!( - coin.seq_safe_send_raw_tx_bytes( - claim_htlc_tx.msg_payload.clone(), - fee.clone(), - timeout_height, - TX_DEFAULT_MEMO.into(), - ) - .await - ); + let fee = try_tx_s!( + self.calculate_fee( + claim_htlc_tx.msg_payload.clone(), + timeout_height, + TX_DEFAULT_MEMO.to_owned(), + None + ) + .await + ); - Ok(TransactionEnum::CosmosTransaction(CosmosTransaction { - data: tx_raw.into(), - })) - }; + let (_tx_id, tx_raw) = try_tx_s!( + self.seq_safe_send_raw_tx_bytes( + claim_htlc_tx.msg_payload.clone(), + fee.clone(), + timeout_height, + TX_DEFAULT_MEMO.into(), + ) + .await + ); - Box::new(fut.boxed().compat()) + Ok(TransactionEnum::CosmosTransaction(CosmosTransaction { + data: tx_raw.into(), + })) } - fn send_taker_spends_maker_payment(&self, taker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { - let tx = try_tx_fus!(cosmrs::Tx::from_bytes(taker_spends_payment_args.other_payment_tx)); - let msg = try_tx_fus!(tx.body.messages.first().ok_or("Tx body couldn't be read.")); + async fn send_taker_spends_maker_payment( + &self, + taker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { + let tx = try_tx_s!(cosmrs::Tx::from_bytes(taker_spends_payment_args.other_payment_tx)); + let msg = try_tx_s!(tx.body.messages.first().ok_or("Tx body couldn't be read.")); - let htlc_proto = try_tx_fus!(CreateHtlcProto::decode( - try_tx_fus!(HtlcType::from_str(&self.account_prefix)), + let htlc_proto = try_tx_s!(CreateHtlcProto::decode( + try_tx_s!(HtlcType::from_str(&self.account_prefix)), msg.value.as_slice() )); - let htlc = try_tx_fus!(CreateHtlcMsg::try_from(htlc_proto)); + let htlc = try_tx_s!(CreateHtlcMsg::try_from(htlc_proto)); let mut amount = htlc.amount().to_vec(); amount.sort(); @@ -2577,39 +2574,34 @@ impl SwapOps for TendermintCoin { let htlc_id = self.calculate_htlc_id(htlc.sender(), htlc.to(), &amount, taker_spends_payment_args.secret_hash); - let claim_htlc_tx = try_tx_fus!(self.gen_claim_htlc_tx(htlc_id, taker_spends_payment_args.secret)); - let coin = self.clone(); - - let fut = async move { - let current_block = try_tx_s!(coin.current_block().compat().await); - let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; + let claim_htlc_tx = try_tx_s!(self.gen_claim_htlc_tx(htlc_id, taker_spends_payment_args.secret)); - let fee = try_tx_s!( - coin.calculate_fee( - claim_htlc_tx.msg_payload.clone(), - timeout_height, - TX_DEFAULT_MEMO.into(), - None - ) - .await - ); + let current_block = try_tx_s!(self.current_block().compat().await); + let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; - let (tx_id, tx_raw) = try_tx_s!( - coin.seq_safe_send_raw_tx_bytes( - claim_htlc_tx.msg_payload.clone(), - fee.clone(), - timeout_height, - TX_DEFAULT_MEMO.into(), - ) - .await - ); + let fee = try_tx_s!( + self.calculate_fee( + claim_htlc_tx.msg_payload.clone(), + timeout_height, + TX_DEFAULT_MEMO.into(), + None + ) + .await + ); - Ok(TransactionEnum::CosmosTransaction(CosmosTransaction { - data: tx_raw.into(), - })) - }; + let (tx_id, tx_raw) = try_tx_s!( + self.seq_safe_send_raw_tx_bytes( + claim_htlc_tx.msg_payload.clone(), + fee.clone(), + timeout_height, + TX_DEFAULT_MEMO.into(), + ) + .await + ); - Box::new(fut.boxed().compat()) + Ok(TransactionEnum::CosmosTransaction(CosmosTransaction { + data: tx_raw.into(), + })) } async fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { @@ -2859,24 +2851,29 @@ pub fn tendermint_priv_key_policy( conf: &TendermintConf, ticker: &str, priv_key_build_policy: PrivKeyBuildPolicy, - path_to_address: StandardHDCoinAddress, + path_to_address: HDPathAccountToAddressId, ) -> MmResult { match priv_key_build_policy { PrivKeyBuildPolicy::IguanaPrivKey(iguana) => Ok(TendermintPrivKeyPolicy::Iguana(iguana)), PrivKeyBuildPolicy::GlobalHDAccount(global_hd) => { - let derivation_path = conf.derivation_path.as_ref().or_mm_err(|| TendermintInitError { + let path_to_coin = conf.derivation_path.as_ref().or_mm_err(|| TendermintInitError { ticker: ticker.to_string(), kind: TendermintInitErrorKind::DerivationPathIsNotSet, })?; let activated_priv_key = global_hd - .derive_secp256k1_secret(derivation_path, &path_to_address) + .derive_secp256k1_secret(&path_to_address.to_derivation_path(path_to_coin).mm_err(|e| { + TendermintInitError { + ticker: ticker.to_string(), + kind: TendermintInitErrorKind::InvalidPathToAddress(e.to_string()), + } + })?) .mm_err(|e| TendermintInitError { ticker: ticker.to_string(), kind: TendermintInitErrorKind::InvalidPrivKey(e.to_string()), })?; let bip39_secp_priv_key = global_hd.root_priv_key().clone(); Ok(TendermintPrivKeyPolicy::HDWallet { - derivation_path: derivation_path.clone(), + path_to_coin: path_to_coin.clone(), activated_key: activated_priv_key, bip39_secp_priv_key, }) diff --git a/mm2src/coins/tendermint/tendermint_token.rs b/mm2src/coins/tendermint/tendermint_token.rs index 34a637ef67..1db1813364 100644 --- a/mm2src/coins/tendermint/tendermint_token.rs +++ b/mm2src/coins/tendermint/tendermint_token.rs @@ -20,8 +20,7 @@ use crate::{big_decimal_from_sat_unsigned, utxo::sat_from_big_decimal, BalanceFu UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherSearchForSwapTxSpendInput, - WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawError, WithdrawFrom, WithdrawFut, - WithdrawRequest}; + WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawError, WithdrawFut, WithdrawRequest}; use crate::{DexFee, MmCoinEnum, PaymentInstructionArgs, ValidateWatcherSpendInput, WatcherReward, WatcherRewardError}; use async_trait::async_trait; use bitcrypto::sha256; @@ -114,20 +113,16 @@ impl TendermintToken { AccountId::from_str(&req.to).map_to_mm(|e| WithdrawError::InvalidAddress(e.to_string()))?; let (account_id, priv_key) = match req.from { - Some(WithdrawFrom::HDWalletAddress(ref path_to_address)) => { + Some(from) => { + let path_to_coin = platform.priv_key_policy.path_to_coin_or_err()?; + let path_to_address = from.to_address_path(path_to_coin.coin_type())?; let priv_key = platform .priv_key_policy - .hd_wallet_derived_priv_key_or_err(path_to_address)?; + .hd_wallet_derived_priv_key_or_err(&path_to_address.to_derivation_path(path_to_coin)?)?; let account_id = account_id_from_privkey(priv_key.as_slice(), &platform.account_prefix) .map_err(|e| WithdrawError::InternalError(e.to_string()))?; (account_id, priv_key) }, - Some(WithdrawFrom::AddressId(_)) | Some(WithdrawFrom::DerivationPath { .. }) => { - return MmError::err(WithdrawError::UnexpectedFromAddress( - "Withdraw from 'AddressId' or 'DerivationPath' is not supported yet for Tendermint!" - .to_string(), - )) - }, None => ( platform.account_id.clone(), *platform.priv_key_policy.activated_key_or_err()?, @@ -302,14 +297,22 @@ impl SwapOps for TendermintToken { ) } - fn send_maker_spends_taker_payment(&self, maker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { + async fn send_maker_spends_taker_payment( + &self, + maker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { self.platform_coin .send_maker_spends_taker_payment(maker_spends_payment_args) + .await } - fn send_taker_spends_maker_payment(&self, taker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { + async fn send_taker_spends_maker_payment( + &self, + taker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { self.platform_coin .send_taker_spends_maker_payment(taker_spends_payment_args) + .await } async fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { @@ -545,8 +548,8 @@ impl MarketCoinOps for TendermintToken { fn my_address(&self) -> MmResult { self.platform_coin.my_address() } - fn get_public_key(&self) -> Result> { - self.platform_coin.get_public_key() + async fn get_public_key(&self) -> Result> { + self.platform_coin.get_public_key().await } fn sign_message_hash(&self, message: &str) -> Option<[u8; 32]> { self.platform_coin.sign_message_hash(message) } @@ -620,6 +623,8 @@ impl MarketCoinOps for TendermintToken { #[inline] fn min_trading_vol(&self) -> MmNumber { self.min_tx_amount().into() } + + fn is_trezor(&self) -> bool { self.platform_coin.priv_key_policy.is_trezor() } } #[async_trait] @@ -643,20 +648,16 @@ impl MmCoin for TendermintToken { } let (account_id, priv_key) = match req.from { - Some(WithdrawFrom::HDWalletAddress(ref path_to_address)) => { + Some(from) => { + let path_to_coin = platform.priv_key_policy.path_to_coin_or_err()?; + let path_to_address = from.to_address_path(path_to_coin.coin_type())?; let priv_key = platform .priv_key_policy - .hd_wallet_derived_priv_key_or_err(path_to_address)?; + .hd_wallet_derived_priv_key_or_err(&path_to_address.to_derivation_path(path_to_coin)?)?; let account_id = account_id_from_privkey(priv_key.as_slice(), &platform.account_prefix) .map_err(|e| WithdrawError::InternalError(e.to_string()))?; (account_id, priv_key) }, - Some(WithdrawFrom::AddressId(_)) | Some(WithdrawFrom::DerivationPath { .. }) => { - return MmError::err(WithdrawError::UnexpectedFromAddress( - "Withdraw from 'AddressId' or 'DerivationPath' is not supported yet for Tendermint!" - .to_string(), - )) - }, None => ( platform.account_id.clone(), *platform.priv_key_policy.activated_key_or_err()?, diff --git a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs index 7ae6f92228..86a2b40ab4 100644 --- a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs +++ b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs @@ -261,7 +261,7 @@ where let ctx_balances = ctx.balances.clone(); - let balances = match ctx.coin.all_balances().await { + let balances = match ctx.coin.get_all_balances().await { Ok(balances) => balances, Err(_) => { return Self::change_state(OnIoErrorCooldown::new(self.address.clone(), self.last_height_state)); @@ -885,7 +885,7 @@ pub async fn tendermint_history_loop( _ctx: MmArc, _current_balance: Option, ) { - let balances = match coin.all_balances().await { + let balances = match coin.get_all_balances().await { Ok(balances) => balances, Err(e) => { log::error!("{}", e); diff --git a/mm2src/coins/test_coin.rs b/mm2src/coins/test_coin.rs index 3d7ca55ed5..684edc6a29 100644 --- a/mm2src/coins/test_coin.rs +++ b/mm2src/coins/test_coin.rs @@ -58,12 +58,13 @@ impl TestCoin { #[async_trait] #[mockable] +#[async_trait] impl MarketCoinOps for TestCoin { fn ticker(&self) -> &str { &self.ticker } fn my_address(&self) -> MmResult { unimplemented!() } - fn get_public_key(&self) -> Result> { unimplemented!() } + async fn get_public_key(&self) -> Result> { unimplemented!() } fn sign_message_hash(&self, _message: &str) -> Option<[u8; 32]> { unimplemented!() } @@ -106,6 +107,8 @@ impl MarketCoinOps for TestCoin { fn min_tx_amount(&self) -> BigDecimal { Default::default() } fn min_trading_vol(&self) -> MmNumber { MmNumber::from("0.00777") } + + fn is_trezor(&self) -> bool { unimplemented!() } } #[async_trait] @@ -117,11 +120,17 @@ impl SwapOps for TestCoin { fn send_taker_payment(&self, _taker_payment_args: SendPaymentArgs) -> TransactionFut { unimplemented!() } - fn send_maker_spends_taker_payment(&self, _maker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { + async fn send_maker_spends_taker_payment( + &self, + _maker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { unimplemented!() } - fn send_taker_spends_maker_payment(&self, _taker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { + async fn send_taker_spends_maker_payment( + &self, + _taker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { unimplemented!() } @@ -425,6 +434,7 @@ impl ToBytes for TestSig { fn to_bytes(&self) -> Vec { vec![] } } +#[async_trait] impl ParseCoinAssocTypes for TestCoin { type Address = String; type AddressParseError = String; @@ -437,7 +447,7 @@ impl ParseCoinAssocTypes for TestCoin { type Sig = TestSig; type SigParseError = String; - fn my_addr(&self) -> &Self::Address { todo!() } + async fn my_addr(&self) -> Self::Address { todo!() } fn parse_address(&self, address: &str) -> Result { todo!() } diff --git a/mm2src/coins/utxo.rs b/mm2src/coins/utxo.rs index 033f496a72..6cf25fcf7f 100644 --- a/mm2src/coins/utxo.rs +++ b/mm2src/coins/utxo.rs @@ -36,6 +36,7 @@ pub mod utxo_balance_events; pub mod utxo_block_header_storage; pub mod utxo_builder; pub mod utxo_common; +pub mod utxo_hd_wallet; pub mod utxo_standard; pub mod utxo_tx_history_v2; pub mod utxo_withdraw; @@ -52,8 +53,7 @@ use common::first_char_to_upper; use common::jsonrpc_client::JsonRpcError; use common::log::LogOnError; use common::{now_sec, now_sec_u32}; -use crypto::{Bip32DerPathOps, Bip32Error, Bip44Chain, ChildNumber, DerivationPath, Secp256k1ExtendedPublicKey, - StandardHDCoinAddress, StandardHDPathError, StandardHDPathToAccount, StandardHDPathToCoin}; +use crypto::{Bip32Error, DerivationPath, HDPathToCoin, Secp256k1ExtendedPublicKey, StandardHDPathError}; use derive_more::Display; #[cfg(not(target_arch = "wasm32"))] use dirs::home_dir; use futures::channel::mpsc::{Receiver as AsyncReceiver, Sender as AsyncSender, UnboundedReceiver, UnboundedSender}; @@ -91,11 +91,11 @@ use std::num::{NonZeroU64, TryFromIntError}; use std::ops::Deref; #[cfg(not(target_arch = "wasm32"))] use std::path::{Path, PathBuf}; -use std::str::FromStr; use std::sync::atomic::{AtomicBool, AtomicU64}; use std::sync::{Arc, Mutex, Weak}; use utxo_builder::UtxoConfBuilder; use utxo_common::{big_decimal_from_sat, UtxoTxBuilder}; +use utxo_hd_wallet::UtxoHDWallet; use utxo_signer::with_key_pair::sign_tx; use utxo_signer::{TxProvider, TxProviderError, UtxoSignTxError, UtxoSignTxResult}; @@ -110,9 +110,8 @@ use super::{big_decimal_from_sat_unsigned, BalanceError, BalanceFut, BalanceResu TransactionEnum, TransactionErr, UnexpectedDerivationMethod, VerificationError, WithdrawError, WithdrawRequest}; use crate::coin_balance::{EnableCoinScanPolicy, EnabledCoinBalanceParams, HDAddressBalanceScanner}; -use crate::hd_wallet::{HDAccountOps, HDAccountsMutex, HDAddress, HDAddressId, HDWalletCoinOps, HDWalletOps, - InvalidBip44ChainError}; -use crate::hd_wallet_storage::{HDAccountStorageItem, HDWalletCoinStorage, HDWalletStorageError, HDWalletStorageResult}; +use crate::hd_wallet::{HDAccountOps, HDAddressOps, HDPathAccountToAddressId, HDWalletCoinOps, HDWalletOps, + HDWalletStorageError}; use crate::utxo::tx_cache::UtxoVerboseCacheShared; use crate::{ParseCoinAssocTypes, ToBytes}; @@ -136,13 +135,11 @@ const UTXO_DUST_AMOUNT: u64 = 1000; /// 11 > 0 const KMD_MTP_BLOCK_COUNT: NonZeroU64 = unsafe { NonZeroU64::new_unchecked(11u64) }; const DEFAULT_DYNAMIC_FEE_VOLATILITY_PERCENT: f64 = 0.5; -const DEFAULT_GAP_LIMIT: u32 = 20; pub type GenerateTxResult = Result<(TransactionInputSigner, AdditionalTxData), MmError>; pub type HistoryUtxoTxMap = HashMap; pub type MatureUnspentMap = HashMap; pub type RecentlySpentOutPointsGuard<'a> = AsyncMutexGuard<'a, RecentlySpentOutPoints>; -pub type UtxoHDAddress = HDAddress; pub enum ScripthashNotification { Triggered(String), @@ -579,7 +576,7 @@ pub struct UtxoCoinConf { /// This derivation path consists of `purpose` and `coin_type` only /// where the full `BIP44` address has the following structure: /// `m/purpose'/coin_type'/account'/change/address_index`. - pub derivation_path: Option, + pub derivation_path: Option, /// The average time in seconds needed to mine a new block for this coin. pub avg_blocktime: Option, } @@ -600,7 +597,7 @@ pub struct UtxoCoinFields { pub rpc_client: UtxoRpcClientEnum, /// Either ECDSA key pair or a Hardware Wallet info. pub priv_key_policy: PrivKeyPolicy, - /// Either an Iguana address or an info about last derived account/address. + /// Either an Iguana address or a 'UtxoHDWallet' instance. pub derivation_method: DerivationMethod, pub history_sync_state: Mutex, /// The cache of verbose transactions. @@ -1033,11 +1030,6 @@ pub trait UtxoCommonOps: fn addr_format_for_standard_scripts(&self) -> UtxoAddressFormat; fn address_from_pubkey(&self, pubkey: &Public) -> Address; - - fn address_from_extended_pubkey(&self, extended_pubkey: &Secp256k1ExtendedPublicKey) -> Address { - let pubkey = Public::Compressed(H264::from(extended_pubkey.public_key().serialize())); - self.address_from_pubkey(&pubkey) - } } impl ToBytes for UtxoTx { @@ -1048,6 +1040,7 @@ impl ToBytes for Signature { fn to_bytes(&self) -> Vec { self.to_vec() } } +#[async_trait] impl ParseCoinAssocTypes for T { type Address = Address; type AddressParseError = MmError; @@ -1060,10 +1053,15 @@ impl ParseCoinAssocTypes for T { type Sig = Signature; type SigParseError = MmError; - fn my_addr(&self) -> &Self::Address { + async fn my_addr(&self) -> Self::Address { match &self.as_ref().derivation_method { - DerivationMethod::SingleAddress(addr) => addr, - unimplemented => unimplemented!("{:?}", unimplemented), + DerivationMethod::SingleAddress(addr) => addr.clone(), + // Todo: Expect should not fail but we need to handle it properly + DerivationMethod::HDWallet(hd_wallet) => hd_wallet + .get_enabled_address() + .await + .expect("Getting enabled address should not fail!") + .address(), } } @@ -1457,7 +1455,7 @@ pub struct UtxoActivationParams { /// This determines which Address of the HD account to be used for swaps for this UTXO coin. /// If not specified, the first non-change address for the first account is used. #[serde(default)] - pub path_to_address: StandardHDCoinAddress, + pub path_to_address: HDPathAccountToAddressId, } #[derive(Debug, Display)] @@ -1512,7 +1510,7 @@ impl UtxoActivationParams { let priv_key_policy = json::from_value::>(req["priv_key_policy"].clone()) .map_to_mm(UtxoFromLegacyReqErr::InvalidPrivKeyPolicy)? .unwrap_or(PrivKeyActivationPolicy::ContextPrivKey); - let path_to_address = json::from_value::>(req["path_to_address"].clone()) + let path_to_address = json::from_value::>(req["path_to_address"].clone()) .map_to_mm(UtxoFromLegacyReqErr::InvalidAddressIndex)? .unwrap_or_default(); @@ -1561,113 +1559,6 @@ impl Default for ElectrumBuilderArgs { } } -#[derive(Debug)] -pub struct UtxoHDWallet { - pub hd_wallet_rmd160: H160, - pub hd_wallet_storage: HDWalletCoinStorage, - pub address_format: UtxoAddressFormat, - /// Derivation path of the coin. - /// This derivation path consists of `purpose` and `coin_type` only - /// where the full `BIP44` address has the following structure: - /// `m/purpose'/coin_type'/account'/change/address_index`. - pub derivation_path: StandardHDPathToCoin, - /// User accounts. - pub accounts: HDAccountsMutex, - // The max number of empty addresses in a row. - // If transactions were sent to an address outside the `gap_limit`, they will not be identified. - pub gap_limit: u32, -} - -impl HDWalletOps for UtxoHDWallet { - type HDAccount = UtxoHDAccount; - - fn coin_type(&self) -> u32 { self.derivation_path.coin_type() } - - fn gap_limit(&self) -> u32 { self.gap_limit } - - fn get_accounts_mutex(&self) -> &HDAccountsMutex { &self.accounts } -} - -#[derive(Clone, Debug, Default)] -pub struct HDAddressesCache { - cache: Arc>>, -} - -impl HDAddressesCache { - pub fn with_capacity(capacity: usize) -> HDAddressesCache { - HDAddressesCache { - cache: Arc::new(AsyncMutex::new(HashMap::with_capacity(capacity))), - } - } - - pub async fn lock(&self) -> AsyncMutexGuard<'_, HashMap> { self.cache.lock().await } -} - -#[derive(Clone, Debug)] -pub struct UtxoHDAccount { - pub account_id: u32, - /// [Extended public key](https://learnmeabitcoin.com/technical/extended-keys) that corresponds to the derivation path: - /// `m/purpose'/coin_type'/account'`. - pub extended_pubkey: Secp256k1ExtendedPublicKey, - /// [`UtxoHDWallet::derivation_path`] derived by [`UtxoHDAccount::account_id`]. - pub account_derivation_path: StandardHDPathToAccount, - /// The number of addresses that we know have been used by the user. - /// This is used in order not to check the transaction history for each address, - /// but to request the balance of addresses whose index is less than `address_number`. - pub external_addresses_number: u32, - pub internal_addresses_number: u32, - /// The cache of derived addresses. - /// This is used at [`HDWalletCoinOps::derive_address`]. - pub derived_addresses: HDAddressesCache, -} - -impl HDAccountOps for UtxoHDAccount { - fn known_addresses_number(&self, chain: Bip44Chain) -> MmResult { - match chain { - Bip44Chain::External => Ok(self.external_addresses_number), - Bip44Chain::Internal => Ok(self.internal_addresses_number), - } - } - - fn account_derivation_path(&self) -> DerivationPath { self.account_derivation_path.to_derivation_path() } - - fn account_id(&self) -> u32 { self.account_id } -} - -impl UtxoHDAccount { - pub fn try_from_storage_item( - wallet_der_path: &StandardHDPathToCoin, - account_info: &HDAccountStorageItem, - ) -> HDWalletStorageResult { - const ACCOUNT_CHILD_HARDENED: bool = true; - - let account_child = ChildNumber::new(account_info.account_id, ACCOUNT_CHILD_HARDENED)?; - let account_derivation_path = wallet_der_path - .derive(account_child) - .map_to_mm(StandardHDPathError::from)?; - let extended_pubkey = Secp256k1ExtendedPublicKey::from_str(&account_info.account_xpub)?; - let capacity = - account_info.external_addresses_number + account_info.internal_addresses_number + DEFAULT_GAP_LIMIT; - Ok(UtxoHDAccount { - account_id: account_info.account_id, - extended_pubkey, - account_derivation_path, - external_addresses_number: account_info.external_addresses_number, - internal_addresses_number: account_info.internal_addresses_number, - derived_addresses: HDAddressesCache::with_capacity(capacity as usize), - }) - } - - pub fn to_storage_item(&self) -> HDAccountStorageItem { - HDAccountStorageItem { - account_id: self.account_id, - account_xpub: self.extended_pubkey.to_string(bip32::Prefix::XPUB), - external_addresses_number: self.external_addresses_number, - internal_addresses_number: self.internal_addresses_number, - } - } -} - /// Function calculating KMD interest /// https://github.com/KomodoPlatform/komodo/blob/master/src/komodo_interest.h fn kmd_interest( @@ -1800,9 +1691,9 @@ pub async fn kmd_rewards_info(coin: &T) -> Result( where T: UtxoCommonOps + GetUtxoListOps, { - let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err()); - let (unspents, recently_sent_txs) = try_tx_s!(coin.get_unspent_ordered_list(my_address).await); + let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err().await); + let (unspents, recently_sent_txs) = try_tx_s!(coin.get_unspent_ordered_list(&my_address).await); generate_and_send_tx(&coin, unspents, None, FeePolicy::SendExact, recently_sent_txs, outputs).await } @@ -1884,10 +1775,11 @@ async fn generate_and_send_tx( where T: AsRef + UtxoTxGenerationOps + UtxoTxBroadcastOps, { - let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err()); + let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err().await); let key_pair = try_tx_s!(coin.as_ref().priv_key_policy.activated_key_or_err()); let mut builder = UtxoTxBuilder::new(coin) + .await .add_available_inputs(unspents) .add_outputs(outputs) .with_fee_policy(fee_policy); @@ -1911,7 +1803,7 @@ where _ => coin.as_ref().conf.signature_version, }; - let prev_script = utxo_common::output_script_checked(coin.as_ref(), my_address) + let prev_script = utxo_common::output_script_checked(coin.as_ref(), &my_address) .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; let signed = try_tx_s!(sign_tx( unsigned, @@ -1957,7 +1849,7 @@ pub fn address_by_conf_and_pubkey_str( priv_key_policy: PrivKeyActivationPolicy::ContextPrivKey, check_utxo_maturity: None, // This will not be used since the pubkey from orderbook/etc.. will be used to generate the address - path_to_address: StandardHDCoinAddress::default(), + path_to_address: HDPathAccountToAddressId::default(), }; let conf_builder = UtxoConfBuilder::new(conf, ¶ms, coin); let utxo_conf = try_s!(conf_builder.build()); @@ -1986,61 +1878,6 @@ fn parse_hex_encoded_u32(hex_encoded: &str) -> Result> { Ok(u32::from_be_bytes(be_bytes)) } -#[cfg(not(target_arch = "wasm32"))] -pub mod for_tests { - use crate::rpc_command::init_withdraw::{init_withdraw, withdraw_status, WithdrawStatusRequest}; - use crate::{TransactionDetails, WithdrawError, WithdrawFrom, WithdrawRequest}; - use common::executor::Timer; - use common::{now_ms, wait_until_ms}; - use mm2_core::mm_ctx::MmArc; - use mm2_err_handle::prelude::MmResult; - use mm2_number::BigDecimal; - use rpc_task::RpcTaskStatus; - use std::str::FromStr; - - /// Helper to call init_withdraw and wait for completion - pub async fn test_withdraw_init_loop( - ctx: MmArc, - ticker: &str, - to: &str, - amount: &str, - from_derivation_path: &str, - ) -> MmResult { - let withdraw_req = WithdrawRequest { - amount: BigDecimal::from_str(amount).unwrap(), - from: Some(WithdrawFrom::DerivationPath { - derivation_path: from_derivation_path.to_owned(), - }), - to: to.to_owned(), - coin: ticker.to_owned(), - max: false, - fee: None, - memo: None, - }; - let init = init_withdraw(ctx.clone(), withdraw_req).await.unwrap(); - let timeout = wait_until_ms(150000); - loop { - if now_ms() > timeout { - panic!("{} init_withdraw timed out", ticker); - } - let status = withdraw_status(ctx.clone(), WithdrawStatusRequest { - task_id: init.task_id, - forget_if_finished: true, - }) - .await; - if let Ok(status) = status { - match status { - RpcTaskStatus::Ok(tx_details) => break Ok(tx_details), - RpcTaskStatus::Error(e) => break Err(e), - _ => Timer::sleep(1.).await, - } - } else { - panic!("{} could not get withdraw_status", ticker) - } - } - } -} - #[test] fn test_parse_hex_encoded_u32() { assert_eq!(parse_hex_encoded_u32("0x892f2085"), Ok(2301567109)); diff --git a/mm2src/coins/utxo/bch.rs b/mm2src/coins/utxo/bch.rs index 57ba2ef720..c832d1a75d 100644 --- a/mm2src/coins/utxo/bch.rs +++ b/mm2src/coins/utxo/bch.rs @@ -1,25 +1,30 @@ use super::*; +use crate::coin_balance::{EnableCoinBalanceError, HDAddressBalance, HDBalanceAddress, HDWalletBalance, + HDWalletBalanceOps}; use crate::coin_errors::{MyAddressError, ValidatePaymentResult}; +use crate::hd_wallet::{ExtractExtendedPubkey, HDCoinAddress, HDCoinHDAccount, HDCoinHDAddress, HDCoinWithdrawOps, + HDExtractPubkeyError, HDXPubExtractor, TrezorCoinError, WithdrawSenderAddress}; use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHistoryTarget, TxDetailsBuilder, TxHistoryStorage}; use crate::tx_history_storage::{GetTxHistoryFilters, WalletId}; use crate::utxo::rpc_clients::UtxoRpcFut; use crate::utxo::slp::{parse_slp_script, SlpGenesisParams, SlpTokenInfo, SlpTransaction, SlpUnspent}; use crate::utxo::utxo_builder::{UtxoArcBuilder, UtxoCoinBuilder}; -use crate::utxo::utxo_common::big_decimal_from_sat_unsigned; +use crate::utxo::utxo_common::{big_decimal_from_sat_unsigned, utxo_prepare_addresses_for_balance_stream_if_enabled}; use crate::utxo::utxo_tx_history_v2::{UtxoMyAddressesHistoryError, UtxoTxDetailsError, UtxoTxDetailsParams, UtxoTxHistoryOps}; -use crate::{BlockHeightAndTime, CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinBalance, CoinProtocol, - CoinWithDerivationMethod, ConfirmPaymentInput, DexFee, IguanaPrivKey, MakerSwapTakerCoin, MmCoinEnum, - NegotiateSwapContractAddrErr, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, - PrivKeyBuildPolicy, RawTransactionFut, RawTransactionRequest, RawTransactionResult, RefundError, - RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, - SendPaymentArgs, SignRawTransactionRequest, SignatureResult, SpendPaymentArgs, SwapOps, - TakerSwapMakerCoin, TradePreimageValue, TransactionFut, TransactionResult, TransactionType, TxFeeDetails, - TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, - ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, - ValidatePaymentInput, ValidateWatcherSpendInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, - WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, +use crate::{coin_balance, BlockHeightAndTime, CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinBalance, CoinProtocol, + CoinWithDerivationMethod, CoinWithPrivKeyPolicy, ConfirmPaymentInput, DexFee, GetWithdrawSenderAddress, + IguanaBalanceOps, IguanaPrivKey, MakerSwapTakerCoin, MmCoinEnum, NegotiateSwapContractAddrErr, + PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, + RawTransactionFut, RawTransactionRequest, RawTransactionResult, RefundError, RefundPaymentArgs, + RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, + SignRawTransactionRequest, SignatureResult, SpendPaymentArgs, SwapOps, TakerSwapMakerCoin, + TradePreimageValue, TransactionFut, TransactionResult, TransactionType, TxFeeDetails, TxMarshalingErr, + UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, + ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, + ValidateWatcherSpendInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, + WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut}; use common::executor::{AbortableSystem, AbortedError}; use common::log::warn; @@ -304,8 +309,9 @@ impl BchCoin { .as_ref() .derivation_method .single_addr_or_err() + .await .mm_err(|e| UtxoRpcError::Internal(e.to_string()))?; - let (mut bch_unspents, recently_spent) = self.bch_unspents_for_spend(my_address).await?; + let (mut bch_unspents, recently_spent) = self.bch_unspents_for_spend(&my_address).await?; let (mut slp_unspents, standard_utxos) = ( bch_unspents.slp.remove(token_id).unwrap_or_default(), bch_unspents.standard, @@ -323,8 +329,9 @@ impl BchCoin { .as_ref() .derivation_method .single_addr_or_err() + .await .mm_err(|e| UtxoRpcError::Internal(e.to_string()))?; - let mut bch_unspents = self.bch_unspents_for_display(my_address).await?; + let mut bch_unspents = self.bch_unspents_for_display(&my_address).await?; let (mut slp_unspents, standard_utxos) = ( bch_unspents.slp.remove(token_id).unwrap_or_default(), bch_unspents.standard, @@ -342,8 +349,8 @@ impl BchCoin { self.slp_tokens_infos.lock().unwrap() } - pub fn get_my_slp_address(&self) -> Result { - let my_address = try_s!(self.as_ref().derivation_method.single_addr_or_err()); + pub async fn get_my_slp_address(&self) -> Result { + let my_address = try_s!(self.as_ref().derivation_method.single_addr_or_err().await); let slp_address = my_address.to_cashaddress(&self.slp_prefix().to_string(), &self.as_ref().conf.address_prefixes)?; Ok(slp_address) @@ -730,6 +737,31 @@ impl GetUtxoListOps for BchCoin { } } +#[async_trait] +#[cfg_attr(test, mockable)] +impl GetUtxoMapOps for BchCoin { + async fn get_unspent_ordered_map( + &self, + addresses: Vec
, + ) -> UtxoRpcResult<(UnspentMap, RecentlySpentOutPointsGuard<'_>)> { + utxo_common::get_unspent_ordered_map(self, addresses).await + } + + async fn get_all_unspent_ordered_map( + &self, + addresses: Vec
, + ) -> UtxoRpcResult<(UnspentMap, RecentlySpentOutPointsGuard<'_>)> { + utxo_common::get_all_unspent_ordered_map(self, addresses).await + } + + async fn get_mature_unspent_ordered_map( + &self, + addresses: Vec
, + ) -> UtxoRpcResult<(MatureUnspentMap, RecentlySpentOutPointsGuard<'_>)> { + utxo_common::get_mature_unspent_ordered_map(self, addresses).await + } +} + // if mockable is placed before async_trait there is `munmap_chunk(): invalid pointer` error on async fn mocking attempt #[async_trait] #[cfg_attr(test, mockable)] @@ -853,13 +885,19 @@ impl SwapOps for BchCoin { } #[inline] - fn send_maker_spends_taker_payment(&self, maker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { - utxo_common::send_maker_spends_taker_payment(self.clone(), maker_spends_payment_args) + async fn send_maker_spends_taker_payment( + &self, + maker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { + utxo_common::send_maker_spends_taker_payment(self.clone(), maker_spends_payment_args).await } #[inline] - fn send_taker_spends_maker_payment(&self, taker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { - utxo_common::send_taker_spends_maker_payment(self.clone(), taker_spends_payment_args) + async fn send_taker_spends_maker_payment( + &self, + taker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { + utxo_common::send_taker_spends_maker_payment(self.clone(), taker_spends_payment_args).await } #[inline] @@ -1141,7 +1179,7 @@ impl MarketCoinOps for BchCoin { fn my_address(&self) -> MmResult { utxo_common::my_address(self) } - fn get_public_key(&self) -> Result> { + async fn get_public_key(&self) -> Result> { let pubkey = utxo_common::my_public_key(&self.utxo_arc)?; Ok(pubkey.to_string()) } @@ -1161,8 +1199,8 @@ impl MarketCoinOps for BchCoin { fn my_balance(&self) -> BalanceFut { let coin = self.clone(); let fut = async move { - let my_address = coin.as_ref().derivation_method.single_addr_or_err()?; - let bch_unspents = coin.bch_unspents_for_display(my_address).await?; + let my_address = coin.as_ref().derivation_method.single_addr_or_err().await?; + let bch_unspents = coin.bch_unspents_for_display(&my_address).await?; Ok(bch_unspents.platform_balance(coin.as_ref().decimals)) }; Box::new(fut.boxed().compat()) @@ -1215,6 +1253,8 @@ impl MarketCoinOps for BchCoin { fn min_tx_amount(&self) -> BigDecimal { utxo_common::min_tx_amount(self.as_ref()) } fn min_trading_vol(&self) -> MmNumber { utxo_common::min_trading_vol(self.as_ref()) } + + fn is_trezor(&self) -> bool { self.as_ref().priv_key_policy.is_trezor() } } #[async_trait] @@ -1319,15 +1359,129 @@ impl MmCoin for BchCoin { } } -impl CoinWithDerivationMethod for BchCoin { +#[async_trait] +impl GetWithdrawSenderAddress for BchCoin { type Address = Address; - type HDWallet = UtxoHDWallet; + type Pubkey = Public; + + async fn get_withdraw_sender_address( + &self, + req: &WithdrawRequest, + ) -> MmResult, WithdrawError> { + utxo_common::get_withdraw_from_address(self, req).await + } +} - fn derivation_method(&self) -> &DerivationMethod { +impl CoinWithPrivKeyPolicy for BchCoin { + type KeyPair = KeyPair; + + fn priv_key_policy(&self) -> &PrivKeyPolicy { &self.utxo_arc.priv_key_policy } +} + +impl CoinWithDerivationMethod for BchCoin { + fn derivation_method(&self) -> &DerivationMethod, Self::HDWallet> { utxo_common::derivation_method(self.as_ref()) } } +#[async_trait] +impl IguanaBalanceOps for BchCoin { + type BalanceObject = CoinBalance; + + async fn iguana_balances(&self) -> BalanceResult { self.my_balance().compat().await } +} + +#[async_trait] +impl ExtractExtendedPubkey for BchCoin { + type ExtendedPublicKey = Secp256k1ExtendedPublicKey; + + async fn extract_extended_pubkey( + &self, + xpub_extractor: Option, + derivation_path: DerivationPath, + ) -> MmResult + where + XPubExtractor: HDXPubExtractor + Send, + { + crate::extract_extended_pubkey_impl(self, xpub_extractor, derivation_path).await + } +} + +#[async_trait] +impl HDWalletCoinOps for BchCoin { + type HDWallet = UtxoHDWallet; + + fn address_from_extended_pubkey( + &self, + extended_pubkey: &Secp256k1ExtendedPublicKey, + derivation_path: DerivationPath, + ) -> HDCoinHDAddress { + utxo_common::address_from_extended_pubkey(self, extended_pubkey, derivation_path) + } + + fn trezor_coin(&self) -> MmResult { utxo_common::trezor_coin(self) } +} + +impl HDCoinWithdrawOps for BchCoin {} + +#[async_trait] +impl HDWalletBalanceOps for BchCoin { + type HDAddressScanner = UtxoAddressScanner; + type BalanceObject = CoinBalance; + + async fn produce_hd_address_scanner(&self) -> BalanceResult { + utxo_common::produce_hd_address_scanner(self).await + } + + async fn enable_hd_wallet( + &self, + hd_wallet: &Self::HDWallet, + xpub_extractor: Option, + params: EnabledCoinBalanceParams, + path_to_address: &HDPathAccountToAddressId, + ) -> MmResult, EnableCoinBalanceError> + where + XPubExtractor: HDXPubExtractor + Send, + { + coin_balance::common_impl::enable_hd_wallet(self, hd_wallet, xpub_extractor, params, path_to_address).await + } + + async fn scan_for_new_addresses( + &self, + hd_wallet: &Self::HDWallet, + hd_account: &mut HDCoinHDAccount, + address_scanner: &Self::HDAddressScanner, + gap_limit: u32, + ) -> BalanceResult>> { + utxo_common::scan_for_new_addresses(self, hd_wallet, hd_account, address_scanner, gap_limit).await + } + + async fn all_known_addresses_balances( + &self, + hd_account: &HDCoinHDAccount, + ) -> BalanceResult>> { + utxo_common::all_known_addresses_balances(self, hd_account).await + } + + async fn known_address_balance(&self, address: &HDBalanceAddress) -> BalanceResult { + utxo_common::address_balance(self, address).await + } + + async fn known_addresses_balances( + &self, + addresses: Vec>, + ) -> BalanceResult, Self::BalanceObject)>> { + utxo_common::addresses_balances(self, addresses).await + } + + async fn prepare_addresses_for_balance_stream_if_enabled( + &self, + addresses: HashSet, + ) -> MmResult<(), String> { + utxo_prepare_addresses_for_balance_stream_if_enabled(self, addresses).await + } +} + #[async_trait] impl CoinWithTxHistoryV2 for BchCoin { fn history_wallet_id(&self) -> WalletId { WalletId::new(self.ticker().to_owned()) } @@ -1353,8 +1507,8 @@ impl CoinWithTxHistoryV2 for BchCoin { #[async_trait] impl UtxoTxHistoryOps for BchCoin { async fn my_addresses(&self) -> MmResult, UtxoMyAddressesHistoryError> { - let my_address = self.as_ref().derivation_method.single_addr_or_err()?; - Ok(std::iter::once(my_address.clone()).collect()) + let addresses = self.all_addresses().await?; + Ok(addresses) } async fn tx_details_by_hash( diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index 3bfe1f6959..9d1b16723d 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -1,13 +1,10 @@ use super::utxo_common::utxo_prepare_addresses_for_balance_stream_if_enabled; use super::*; use crate::coin_balance::{self, EnableCoinBalanceError, EnabledCoinBalanceParams, HDAccountBalance, HDAddressBalance, - HDWalletBalance, HDWalletBalanceOps}; + HDBalanceAddress, HDWalletBalance, HDWalletBalanceOps}; use crate::coin_errors::{MyAddressError, ValidatePaymentResult}; -use crate::hd_confirm_address::HDConfirmAddress; -use crate::hd_pubkey::{ExtractExtendedPubkey, HDExtractPubkeyError, HDXPubExtractor}; -use crate::hd_wallet::{AccountUpdatingError, AddressDerivingResult, HDAccountMut, NewAccountCreatingError, - NewAddressDeriveConfirmError}; -use crate::hd_wallet_storage::HDWalletCoinWithStorageOps; +use crate::hd_wallet::{ExtractExtendedPubkey, HDCoinAddress, HDCoinHDAccount, HDCoinHDAddress, HDCoinWithdrawOps, + HDConfirmAddress, HDExtractPubkeyError, HDXPubExtractor, TrezorCoinError, WithdrawSenderAddress}; use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHistoryTarget, TxHistoryStorage}; use crate::rpc_command::account_balance::{self, AccountBalanceParams, AccountBalanceRpcOps, HDAccountBalanceResponse}; use crate::rpc_command::get_new_address::{self, GetNewAddressParams, GetNewAddressResponse, GetNewAddressRpcError, @@ -25,20 +22,20 @@ use crate::utxo::utxo_builder::{MergeUtxoArcOps, UtxoCoinBuildError, UtxoCoinBui UtxoFieldsWithIguanaSecretBuilder}; use crate::utxo::utxo_tx_history_v2::{UtxoMyAddressesHistoryError, UtxoTxDetailsError, UtxoTxDetailsParams, UtxoTxHistoryOps}; -use crate::{eth, CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinBalance, CoinWithDerivationMethod, ConfirmPaymentInput, - DelegationError, DelegationFut, DexFee, GetWithdrawSenderAddress, IguanaPrivKey, MakerSwapTakerCoin, - MmCoinEnum, NegotiateSwapContractAddrErr, PaymentInstructionArgs, PaymentInstructions, - PaymentInstructionsErr, PrivKeyBuildPolicy, RawTransactionRequest, RawTransactionResult, RefundError, - RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, - SendPaymentArgs, SignRawTransactionRequest, SignatureResult, SpendPaymentArgs, StakingInfosFut, SwapOps, +use crate::{eth, CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinBalance, CoinWithDerivationMethod, + CoinWithPrivKeyPolicy, ConfirmPaymentInput, DelegationError, DelegationFut, DexFee, + GetWithdrawSenderAddress, IguanaBalanceOps, IguanaPrivKey, MakerSwapTakerCoin, MmCoinEnum, + NegotiateSwapContractAddrErr, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, + PrivKeyBuildPolicy, RawTransactionRequest, RawTransactionResult, RefundError, RefundPaymentArgs, + RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, + SignRawTransactionRequest, SignatureResult, SpendPaymentArgs, StakingInfosFut, SwapOps, TakerSwapMakerCoin, TradePreimageValue, TransactionFut, TransactionResult, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, ValidateWatcherSpendInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, - WatcherValidateTakerFeeInput, WithdrawFut, WithdrawSenderAddress}; + WatcherValidateTakerFeeInput, WithdrawFut}; use common::executor::{AbortableSystem, AbortedError}; -use crypto::Bip44Chain; use ethereum_types::H160; use futures::{FutureExt, TryFutureExt}; use keys::AddressHashEnum; @@ -159,8 +156,8 @@ pub trait QtumBasedCoin: UtxoCommonOps + MarketCoinOps { .expect("valid address props") } - fn my_addr_as_contract_addr(&self) -> MmResult { - let my_address = self.as_ref().derivation_method.single_addr_or_err()?.clone(); + async fn my_addr_as_contract_addr(&self) -> MmResult { + let my_address = self.as_ref().derivation_method.single_addr_or_err().await?; contract_addr_from_utxo_addr(my_address).mm_err(Qrc20AddressError::from) } @@ -542,13 +539,19 @@ impl SwapOps for QtumCoin { } #[inline] - fn send_maker_spends_taker_payment(&self, maker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { - utxo_common::send_maker_spends_taker_payment(self.clone(), maker_spends_payment_args) + async fn send_maker_spends_taker_payment( + &self, + maker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { + utxo_common::send_maker_spends_taker_payment(self.clone(), maker_spends_payment_args).await } #[inline] - fn send_taker_spends_maker_payment(&self, taker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { - utxo_common::send_taker_spends_maker_payment(self.clone(), taker_spends_payment_args) + async fn send_taker_spends_maker_payment( + &self, + taker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { + utxo_common::send_taker_spends_maker_payment(self.clone(), taker_spends_payment_args).await } #[inline] @@ -818,7 +821,7 @@ impl MarketCoinOps for QtumCoin { fn my_address(&self) -> MmResult { utxo_common::my_address(self) } - fn get_public_key(&self) -> Result> { + async fn get_public_key(&self) -> Result> { let pubkey = utxo_common::my_public_key(&self.utxo_arc)?; Ok(pubkey.to_string()) } @@ -884,6 +887,8 @@ impl MarketCoinOps for QtumCoin { fn min_tx_amount(&self) -> BigDecimal { utxo_common::min_tx_amount(self.as_ref()) } fn min_trading_vol(&self) -> MmNumber { utxo_common::min_trading_vol(self.as_ref()) } + + fn is_trezor(&self) -> bool { self.as_ref().priv_key_policy.is_trezor() } } #[async_trait] @@ -1034,87 +1039,62 @@ impl UtxoSignerOps for QtumCoin { fn tx_provider(&self) -> Self::TxGetter { self.utxo_arc.rpc_client.clone() } } -impl CoinWithDerivationMethod for QtumCoin { - type Address = Address; - type HDWallet = UtxoHDWallet; +impl CoinWithPrivKeyPolicy for QtumCoin { + type KeyPair = KeyPair; - fn derivation_method(&self) -> &DerivationMethod { + fn priv_key_policy(&self) -> &PrivKeyPolicy { &self.utxo_arc.priv_key_policy } +} + +impl CoinWithDerivationMethod for QtumCoin { + fn derivation_method(&self) -> &DerivationMethod, Self::HDWallet> { utxo_common::derivation_method(self.as_ref()) } } +#[async_trait] +impl IguanaBalanceOps for QtumCoin { + type BalanceObject = CoinBalance; + + async fn iguana_balances(&self) -> BalanceResult { self.my_balance().compat().await } +} + #[async_trait] impl ExtractExtendedPubkey for QtumCoin { type ExtendedPublicKey = Secp256k1ExtendedPublicKey; async fn extract_extended_pubkey( &self, - xpub_extractor: &XPubExtractor, + xpub_extractor: Option, derivation_path: DerivationPath, ) -> MmResult where - XPubExtractor: HDXPubExtractor, + XPubExtractor: HDXPubExtractor + Send, { - utxo_common::extract_extended_pubkey(&self.utxo_arc.conf, xpub_extractor, derivation_path).await + crate::extract_extended_pubkey_impl(self, xpub_extractor, derivation_path).await } } #[async_trait] impl HDWalletCoinOps for QtumCoin { - type Address = Address; - type Pubkey = Public; type HDWallet = UtxoHDWallet; - type HDAccount = UtxoHDAccount; - - async fn derive_addresses( - &self, - hd_account: &Self::HDAccount, - address_ids: Ids, - ) -> AddressDerivingResult>> - where - Ids: Iterator + Send, - { - utxo_common::derive_addresses(self, hd_account, address_ids).await - } - - async fn generate_and_confirm_new_address( - &self, - hd_wallet: &Self::HDWallet, - hd_account: &mut Self::HDAccount, - chain: Bip44Chain, - confirm_address: &ConfirmAddress, - ) -> MmResult, NewAddressDeriveConfirmError> - where - ConfirmAddress: HDConfirmAddress, - { - utxo_common::generate_and_confirm_new_address(self, hd_wallet, hd_account, chain, confirm_address).await - } - async fn create_new_account<'a, XPubExtractor>( + fn address_from_extended_pubkey( &self, - hd_wallet: &'a Self::HDWallet, - xpub_extractor: &XPubExtractor, - ) -> MmResult, NewAccountCreatingError> - where - XPubExtractor: HDXPubExtractor, - { - utxo_common::create_new_account(self, hd_wallet, xpub_extractor).await + extended_pubkey: &Secp256k1ExtendedPublicKey, + derivation_path: DerivationPath, + ) -> HDCoinHDAddress { + utxo_common::address_from_extended_pubkey(self, extended_pubkey, derivation_path) } - async fn set_known_addresses_number( - &self, - hd_wallet: &Self::HDWallet, - hd_account: &mut Self::HDAccount, - chain: Bip44Chain, - new_known_addresses_number: u32, - ) -> MmResult<(), AccountUpdatingError> { - utxo_common::set_known_addresses_number(self, hd_wallet, hd_account, chain, new_known_addresses_number).await - } + fn trezor_coin(&self) -> MmResult { utxo_common::trezor_coin(self) } } +impl HDCoinWithdrawOps for QtumCoin {} + #[async_trait] impl HDWalletBalanceOps for QtumCoin { type HDAddressScanner = UtxoAddressScanner; + type BalanceObject = CoinBalance; async fn produce_hd_address_scanner(&self) -> BalanceResult { utxo_common::produce_hd_address_scanner(self).await @@ -1123,60 +1103,60 @@ impl HDWalletBalanceOps for QtumCoin { async fn enable_hd_wallet( &self, hd_wallet: &Self::HDWallet, - xpub_extractor: &XPubExtractor, + xpub_extractor: Option, params: EnabledCoinBalanceParams, - ) -> MmResult + path_to_address: &HDPathAccountToAddressId, + ) -> MmResult, EnableCoinBalanceError> where - XPubExtractor: HDXPubExtractor, + XPubExtractor: HDXPubExtractor + Send, { - coin_balance::common_impl::enable_hd_wallet(self, hd_wallet, xpub_extractor, params).await + coin_balance::common_impl::enable_hd_wallet(self, hd_wallet, xpub_extractor, params, path_to_address).await } async fn scan_for_new_addresses( &self, hd_wallet: &Self::HDWallet, - hd_account: &mut Self::HDAccount, + hd_account: &mut HDCoinHDAccount, address_scanner: &Self::HDAddressScanner, gap_limit: u32, - ) -> BalanceResult> { + ) -> BalanceResult>> { utxo_common::scan_for_new_addresses(self, hd_wallet, hd_account, address_scanner, gap_limit).await } - async fn all_known_addresses_balances(&self, hd_account: &Self::HDAccount) -> BalanceResult> { + async fn all_known_addresses_balances( + &self, + hd_account: &HDCoinHDAccount, + ) -> BalanceResult>> { utxo_common::all_known_addresses_balances(self, hd_account).await } - async fn known_address_balance(&self, address: &Self::Address) -> BalanceResult { + async fn known_address_balance(&self, address: &HDBalanceAddress) -> BalanceResult { utxo_common::address_balance(self, address).await } async fn known_addresses_balances( &self, - addresses: Vec, - ) -> BalanceResult> { + addresses: Vec>, + ) -> BalanceResult, Self::BalanceObject)>> { utxo_common::addresses_balances(self, addresses).await } async fn prepare_addresses_for_balance_stream_if_enabled( &self, - addresses: HashSet, + addresses: HashSet, ) -> MmResult<(), String> { utxo_prepare_addresses_for_balance_stream_if_enabled(self, addresses).await } } -impl HDWalletCoinWithStorageOps for QtumCoin { - fn hd_wallet_storage<'a>(&self, hd_wallet: &'a Self::HDWallet) -> &'a HDWalletCoinStorage { - &hd_wallet.hd_wallet_storage - } -} - #[async_trait] impl GetNewAddressRpcOps for QtumCoin { + type BalanceObject = CoinBalance; + async fn get_new_address_rpc_without_conf( &self, params: GetNewAddressParams, - ) -> MmResult { + ) -> MmResult, GetNewAddressRpcError> { get_new_address::common_impl::get_new_address_rpc_without_conf(self, params).await } @@ -1184,7 +1164,7 @@ impl GetNewAddressRpcOps for QtumCoin { &self, params: GetNewAddressParams, confirm_address: &ConfirmAddress, - ) -> MmResult + ) -> MmResult, GetNewAddressRpcError> where ConfirmAddress: HDConfirmAddress, { @@ -1194,44 +1174,52 @@ impl GetNewAddressRpcOps for QtumCoin { #[async_trait] impl AccountBalanceRpcOps for QtumCoin { + type BalanceObject = CoinBalance; + async fn account_balance_rpc( &self, params: AccountBalanceParams, - ) -> MmResult { + ) -> MmResult, HDAccountBalanceRpcError> { account_balance::common_impl::account_balance_rpc(self, params).await } } #[async_trait] impl InitAccountBalanceRpcOps for QtumCoin { + type BalanceObject = CoinBalance; + async fn init_account_balance_rpc( &self, params: InitAccountBalanceParams, - ) -> MmResult { + ) -> MmResult, HDAccountBalanceRpcError> { init_account_balance::common_impl::init_account_balance_rpc(self, params).await } } #[async_trait] impl InitScanAddressesRpcOps for QtumCoin { + type BalanceObject = CoinBalance; + async fn init_scan_for_new_addresses_rpc( &self, params: ScanAddressesParams, - ) -> MmResult { + ) -> MmResult, HDAccountBalanceRpcError> { init_scan_for_new_addresses::common_impl::scan_for_new_addresses_rpc(self, params).await } } #[async_trait] impl InitCreateAccountRpcOps for QtumCoin { + type BalanceObject = CoinBalance; + async fn init_create_account_rpc( &self, params: CreateNewAccountParams, state: CreateAccountState, - xpub_extractor: &XPubExtractor, - ) -> MmResult + xpub_extractor: Option, + ) -> MmResult, CreateAccountRpcError> where - XPubExtractor: HDXPubExtractor, + XPubExtractor: HDXPubExtractor + Send, { init_create_account::common_impl::init_create_new_account_rpc(self, params, state, xpub_extractor).await } @@ -1256,7 +1244,8 @@ impl CoinWithTxHistoryV2 for QtumCoin { #[async_trait] impl UtxoTxHistoryOps for QtumCoin { async fn my_addresses(&self) -> MmResult, UtxoMyAddressesHistoryError> { - utxo_common::utxo_tx_history_v2_common::my_addresses(self).await + let addresses = self.all_addresses().await?; + Ok(addresses) } async fn tx_details_by_hash( diff --git a/mm2src/coins/utxo/qtum_delegation.rs b/mm2src/coins/utxo/qtum_delegation.rs index f146042112..4a62adcd26 100644 --- a/mm2src/coins/utxo/qtum_delegation.rs +++ b/mm2src/coins/utxo/qtum_delegation.rs @@ -141,7 +141,7 @@ impl QtumCoin { }, UtxoRpcClientEnum::Electrum(electrum) => electrum, }; - let address = self.my_addr_as_contract_addr()?; + let address = self.my_addr_as_contract_addr().await?; let address_rpc = contract_addr_into_rpc_format(&address); let add_delegation_history = client .blockchain_contract_event_get_history(&address_rpc, &contract_address, QTUM_ADD_DELEGATION_TOPIC) @@ -197,10 +197,10 @@ impl QtumCoin { async fn get_delegation_infos_impl(&self) -> StakingInfosResult { let coin = self.as_ref(); - let my_address = coin.derivation_method.single_addr_or_err()?; + let my_address = coin.derivation_method.single_addr_or_err().await?; let staker = self.am_i_currently_staking().await?; - let (unspents, _) = self.get_unspent_ordered_list(my_address).await?; + let (unspents, _) = self.get_unspent_ordered_list(&my_address).await?; let lower_bound = QTUM_LOWER_BOUND_DELEGATION_AMOUNT .try_into() .expect("Conversion should succeed"); @@ -268,9 +268,9 @@ impl QtumCoin { let utxo = self.as_ref(); let key_pair = utxo.priv_key_policy.activated_key_or_err()?; - let my_address = utxo.derivation_method.single_addr_or_err()?; + let my_address = utxo.derivation_method.single_addr_or_err().await?; - let (unspents, _) = self.get_unspent_ordered_list(my_address).await?; + let (unspents, _) = self.get_unspent_ordered_list(&my_address).await?; let mut gas_fee = 0; let mut outputs = Vec::with_capacity(contract_outputs.len()); for output in contract_outputs { @@ -279,6 +279,7 @@ impl QtumCoin { } let (unsigned, data) = UtxoTxBuilder::new(self) + .await .add_available_inputs(unspents) .add_outputs(outputs) .with_gas_fee(gas_fee) @@ -290,7 +291,7 @@ impl QtumCoin { })?; let prev_script = self - .script_for_address(my_address) + .script_for_address(&my_address) .map_err(|e| DelegationError::InternalError(e.to_string()))?; let signed = sign_tx( unsigned, diff --git a/mm2src/coins/utxo/slp.rs b/mm2src/coins/utxo/slp.rs index 0ba3c2e077..4138d3b7a8 100644 --- a/mm2src/coins/utxo/slp.rs +++ b/mm2src/coins/utxo/slp.rs @@ -13,9 +13,9 @@ use crate::utxo::utxo_common::{self, big_decimal_from_sat_unsigned, payment_scri use crate::utxo::{generate_and_send_tx, sat_from_big_decimal, ActualTxFee, AdditionalTxData, BroadcastTxErr, FeePolicy, GenerateTxError, RecentlySpentOutPointsGuard, UtxoCoinConf, UtxoCoinFields, UtxoCommonOps, UtxoTx, UtxoTxBroadcastOps, UtxoTxGenerationOps}; -use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, ConfirmPaymentInput, DexFee, - FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, MmCoinEnum, - NegotiateSwapContractAddrErr, NumConversError, PaymentInstructionArgs, PaymentInstructions, +use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, ConfirmPaymentInput, DerivationMethod, + DexFee, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, + MmCoinEnum, NegotiateSwapContractAddrErr, NumConversError, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, PrivKeyPolicyNotAllowed, RawTransactionFut, RawTransactionRequest, RawTransactionResult, RefundError, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignRawTransactionRequest, SignatureResult, @@ -646,6 +646,7 @@ impl SlpToken { let (_, bch_inputs, _recently_spent) = self.slp_unspents_for_spend().await?; let (mut unsigned, _) = UtxoTxBuilder::new(&self.platform_coin) + .await .add_required_inputs(std::iter::once(p2sh_utxo.bch_unspent)) .add_available_inputs(bch_inputs) .add_outputs(outputs) @@ -1094,7 +1095,14 @@ impl MarketCoinOps for SlpToken { fn ticker(&self) -> &str { &self.conf.ticker } fn my_address(&self) -> MmResult { - let my_address = self.as_ref().derivation_method.single_addr_or_err()?; + let my_address = match self.platform_coin.as_ref().derivation_method { + DerivationMethod::SingleAddress(ref my_address) => my_address, + DerivationMethod::HDWallet(_) => { + return MmError::err(MyAddressError::UnexpectedDerivationMethod( + "'my_address' is deprecated for HD wallets".to_string(), + )) + }, + }; let slp_address = self .platform_coin .slp_address(my_address) @@ -1102,7 +1110,7 @@ impl MarketCoinOps for SlpToken { slp_address.encode().map_to_mm(MyAddressError::InternalError) } - fn get_public_key(&self) -> Result> { + async fn get_public_key(&self) -> Result> { let pubkey = utxo_common::my_public_key(self.platform_coin.as_ref())?; Ok(pubkey.to_string()) } @@ -1200,6 +1208,8 @@ impl MarketCoinOps for SlpToken { fn min_tx_amount(&self) -> BigDecimal { big_decimal_from_sat_unsigned(1, self.decimals()) } fn min_trading_vol(&self) -> MmNumber { big_decimal_from_sat_unsigned(1, self.decimals()).into() } + + fn is_trezor(&self) -> bool { self.as_ref().priv_key_policy.is_trezor() } } #[async_trait] @@ -1263,42 +1273,38 @@ impl SwapOps for SlpToken { Box::new(fut.boxed().compat()) } - fn send_maker_spends_taker_payment(&self, maker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { + async fn send_maker_spends_taker_payment( + &self, + maker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { let tx = maker_spends_payment_args.other_payment_tx.to_owned(); - let taker_pub = try_tx_fus!(Public::from_slice(maker_spends_payment_args.other_pubkey)); + let taker_pub = try_tx_s!(Public::from_slice(maker_spends_payment_args.other_pubkey)); let secret = maker_spends_payment_args.secret.to_owned(); let secret_hash = maker_spends_payment_args.secret_hash.to_owned(); let htlc_keypair = self.derive_htlc_key_pair(maker_spends_payment_args.swap_unique_data); - let coin = self.clone(); - let time_lock = try_tx_fus!(maker_spends_payment_args.time_lock.try_into()); - - let fut = async move { - let tx = try_tx_s!( - coin.spend_htlc(&tx, &taker_pub, time_lock, &secret, &secret_hash, &htlc_keypair) - .await - ); - Ok(tx.into()) - }; - Box::new(fut.boxed().compat()) + let time_lock = try_tx_s!(maker_spends_payment_args.time_lock.try_into()); + let tx = try_tx_s!( + self.spend_htlc(&tx, &taker_pub, time_lock, &secret, &secret_hash, &htlc_keypair) + .await + ); + Ok(tx.into()) } - fn send_taker_spends_maker_payment(&self, taker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { + async fn send_taker_spends_maker_payment( + &self, + taker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { let tx = taker_spends_payment_args.other_payment_tx.to_owned(); - let maker_pub = try_tx_fus!(Public::from_slice(taker_spends_payment_args.other_pubkey)); + let maker_pub = try_tx_s!(Public::from_slice(taker_spends_payment_args.other_pubkey)); let secret = taker_spends_payment_args.secret.to_owned(); let secret_hash = taker_spends_payment_args.secret_hash.to_owned(); let htlc_keypair = self.derive_htlc_key_pair(taker_spends_payment_args.swap_unique_data); - let coin = self.clone(); - let time_lock = try_tx_fus!(taker_spends_payment_args.time_lock.try_into()); - - let fut = async move { - let tx = try_tx_s!( - coin.spend_htlc(&tx, &maker_pub, time_lock, &secret, &secret_hash, &htlc_keypair) - .await - ); - Ok(tx.into()) - }; - Box::new(fut.boxed().compat()) + let time_lock = try_tx_s!(taker_spends_payment_args.time_lock.try_into()); + let tx = try_tx_s!( + self.spend_htlc(&tx, &maker_pub, time_lock, &secret, &secret_hash, &htlc_keypair) + .await + ); + Ok(tx.into()) } async fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { @@ -1610,7 +1616,18 @@ impl MmCoin for SlpToken { fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut { let coin = self.clone(); let fut = async move { - let my_address = coin.platform_coin.as_ref().derivation_method.single_addr_or_err()?; + if req.from.is_some() { + return MmError::err(WithdrawError::UnsupportedError( + "Withdraw from a specific address is not supported for slp yet".to_owned(), + )); + } + + let my_address = coin + .platform_coin + .as_ref() + .derivation_method + .single_addr_or_err() + .await?; let key_pair = coin.platform_coin.as_ref().priv_key_policy.activated_key_or_err()?; let address = CashAddress::decode(&req.to).map_to_mm(WithdrawError::InvalidAddress)?; @@ -1648,6 +1665,7 @@ impl MmCoin for SlpToken { let slp_output = SlpOutput { amount, script_pubkey }; let (slp_preimage, _) = coin.generate_slp_tx_preimage(vec![slp_output]).await?; let mut tx_builder = UtxoTxBuilder::new(&coin.platform_coin) + .await .add_required_inputs(slp_preimage.slp_inputs.into_iter().map(|slp| slp.bch_unspent)) .add_available_inputs(slp_preimage.available_bch_inputs) .add_outputs(slp_preimage.outputs); @@ -1678,7 +1696,7 @@ impl MmCoin for SlpToken { let prev_script = coin .platform_coin - .script_for_address(my_address) + .script_for_address(&my_address) .map_err(|e| WithdrawError::InvalidAddress(e.to_string()))?; let signed = sign_tx( unsigned, @@ -2144,8 +2162,8 @@ mod slp_tests { let token_id = H256::from("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"); let fusd = SlpToken::new(4, "FUSD".into(), token_id, bch.clone(), 0).unwrap(); - let bch_address = bch.as_ref().derivation_method.unwrap_single_addr(); - let (unspents, recently_spent) = block_on(bch.get_unspent_ordered_list(bch_address)).unwrap(); + let bch_address = block_on(bch.as_ref().derivation_method.unwrap_single_addr()); + let (unspents, recently_spent) = block_on(bch.get_unspent_ordered_list(&bch_address)).unwrap(); let secret_hash = hex::decode("5d9e149ad9ccb20e9f931a69b605df2ffde60242").unwrap(); let other_pub = hex::decode("036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202c").unwrap(); diff --git a/mm2src/coins/utxo/utxo_balance_events.rs b/mm2src/coins/utxo/utxo_balance_events.rs index 9a929b9e36..2d97ef5cc9 100644 --- a/mm2src/coins/utxo/utxo_balance_events.rs +++ b/mm2src/coins/utxo/utxo_balance_events.rs @@ -6,16 +6,15 @@ use futures_util::StreamExt; use keys::Address; use mm2_core::mm_ctx::MmArc; use mm2_event_stream::{behaviour::{EventBehaviour, EventInitStatus}, - Event, EventStreamConfiguration}; + ErrorEventName, Event, EventName, EventStreamConfiguration}; use std::collections::{BTreeMap, HashSet}; use super::utxo_standard::UtxoStandardCoin; use crate::{utxo::{output_script, rpc_clients::electrum_script_hash, utxo_common::{address_balance, address_to_scripthash}, - utxo_tx_history_v2::UtxoTxHistoryOps, ScripthashNotification, UtxoCoinFields}, - MarketCoinOps, MmCoin}; + CoinWithDerivationMethod, MarketCoinOps, MmCoin}; macro_rules! try_or_continue { ($exp:expr) => { @@ -31,8 +30,9 @@ macro_rules! try_or_continue { #[async_trait] impl EventBehaviour for UtxoStandardCoin { - const EVENT_NAME: &'static str = "COIN_BALANCE"; - const ERROR_EVENT_NAME: &'static str = "COIN_BALANCE_ERROR"; + fn event_name() -> EventName { EventName::CoinBalance } + + fn error_event_name() -> ErrorEventName { ErrorEventName::CoinBalanceError } async fn handle(self, _interval: f64, tx: oneshot::Sender) { const RECEIVER_DROPPED_MSG: &str = "Receiver is dropped, which should never happen."; @@ -108,7 +108,7 @@ impl EventBehaviour for UtxoStandardCoin { ctx.stream_channel_controller .broadcast(Event::new( - format!("{}:{}", Self::ERROR_EVENT_NAME, self.ticker()), + format!("{}:{}", Self::error_event_name(), self.ticker()), json!({ "error": e }).to_string(), )) .await; @@ -118,7 +118,7 @@ impl EventBehaviour for UtxoStandardCoin { continue; }, ScripthashNotification::RefreshSubscriptions => { - let my_addresses = try_or_continue!(self.my_addresses().await); + let my_addresses = try_or_continue!(self.all_addresses().await); match subscribe_to_addresses(self.as_ref(), my_addresses).await { Ok(map) => scripthash_to_address_map = map, Err(e) => { @@ -126,7 +126,7 @@ impl EventBehaviour for UtxoStandardCoin { ctx.stream_channel_controller .broadcast(Event::new( - format!("{}:{}", Self::ERROR_EVENT_NAME, self.ticker()), + format!("{}:{}", Self::error_event_name(), self.ticker()), json!({ "error": e }).to_string(), )) .await; @@ -139,7 +139,7 @@ impl EventBehaviour for UtxoStandardCoin { let address = match scripthash_to_address_map.get(¬ified_scripthash) { Some(t) => Some(t.clone()), - None => try_or_continue!(self.my_addresses().await) + None => try_or_continue!(self.all_addresses().await) .into_iter() .find_map(|addr| { let script = match output_script(&addr) { @@ -181,7 +181,7 @@ impl EventBehaviour for UtxoStandardCoin { ctx.stream_channel_controller .broadcast(Event::new( - format!("{}:{}", Self::ERROR_EVENT_NAME, ticker), + format!("{}:{}", Self::error_event_name(), ticker), e.to_string(), )) .await; @@ -198,7 +198,7 @@ impl EventBehaviour for UtxoStandardCoin { ctx.stream_channel_controller .broadcast(Event::new( - Self::EVENT_NAME.to_string(), + Self::event_name().to_string(), json!(vec![payload]).to_string(), )) .await; @@ -206,18 +206,21 @@ impl EventBehaviour for UtxoStandardCoin { } async fn spawn_if_active(self, config: &EventStreamConfiguration) -> EventInitStatus { - if let Some(event) = config.get_event(Self::EVENT_NAME) { + if let Some(event) = config.get_event(&Self::event_name()) { log::info!( "{} event is activated for {}. `stream_interval_seconds`({}) has no effect on this.", - Self::EVENT_NAME, + Self::event_name(), self.ticker(), event.stream_interval_seconds ); let (tx, rx): (Sender, Receiver) = oneshot::channel(); let fut = self.clone().handle(event.stream_interval_seconds, tx); - let settings = - AbortSettings::info_on_abort(format!("{} event is stopped for {}.", Self::EVENT_NAME, self.ticker())); + let settings = AbortSettings::info_on_abort(format!( + "{} event is stopped for {}.", + Self::event_name(), + self.ticker() + )); self.spawner().spawn_with_settings(fut, settings); rx.await.unwrap_or_else(|e| { diff --git a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs index 3657144a66..edf34ebb65 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs @@ -1,14 +1,14 @@ -use crate::hd_wallet::{HDAccountsMap, HDAccountsMutex}; -use crate::hd_wallet_storage::{HDWalletCoinStorage, HDWalletStorageError}; +use crate::hd_wallet::{load_hd_accounts_from_storage, HDAccountsMutex, HDWallet, HDWalletCoinStorage, + HDWalletStorageError, DEFAULT_GAP_LIMIT}; use crate::utxo::rpc_clients::{ElectrumClient, ElectrumClientImpl, ElectrumRpcRequest, EstimateFeeMethod, UtxoRpcClientEnum}; use crate::utxo::tx_cache::{UtxoVerboseCacheOps, UtxoVerboseCacheShared}; use crate::utxo::utxo_block_header_storage::BlockHeaderStorage; use crate::utxo::utxo_builder::utxo_conf_builder::{UtxoConfBuilder, UtxoConfError}; -use crate::utxo::{output_script, utxo_common, ElectrumBuilderArgs, ElectrumProtoVerifier, ElectrumProtoVerifierEvent, +use crate::utxo::{output_script, ElectrumBuilderArgs, ElectrumProtoVerifier, ElectrumProtoVerifierEvent, RecentlySpentOutPoints, ScripthashNotification, ScripthashNotificationSender, TxFee, UtxoCoinConf, - UtxoCoinFields, UtxoHDAccount, UtxoHDWallet, UtxoRpcMode, UtxoSyncStatus, UtxoSyncStatusLoopHandle, - DEFAULT_GAP_LIMIT, UTXO_DUST_AMOUNT}; + UtxoCoinFields, UtxoHDWallet, UtxoRpcMode, UtxoSyncStatus, UtxoSyncStatusLoopHandle, + UTXO_DUST_AMOUNT}; use crate::{BlockchainNetwork, CoinTransportMetrics, DerivationMethod, HistorySyncState, IguanaPrivKey, PrivKeyBuildPolicy, PrivKeyPolicy, PrivKeyPolicyNotAllowed, RpcClientType, UtxoActivationParams}; use async_trait::async_trait; @@ -18,8 +18,7 @@ use common::executor::{abortable_queue::AbortableQueue, AbortSettings, Abortable Timer}; use common::log::{error, info, LogOnError}; use common::{now_sec, small_rng}; -use crypto::{Bip32DerPathError, CryptoCtx, CryptoCtxError, GlobalHDAccountArc, HwWalletType, StandardHDPathError, - StandardHDPathToCoin}; +use crypto::{Bip32DerPathError, CryptoCtx, CryptoCtxError, GlobalHDAccountArc, HwWalletType, StandardHDPathError}; use derive_more::Display; use futures::channel::mpsc::{channel, unbounded, Receiver as AsyncReceiver, UnboundedReceiver, UnboundedSender}; use futures::compat::Future01CompatExt; @@ -96,6 +95,7 @@ pub enum UtxoCoinBuildError { UnsupportedModeForBalanceEvents { mode: String, }, + InvalidPathToAddress(String), } impl From for UtxoCoinBuildError { @@ -168,7 +168,19 @@ pub trait UtxoFieldsWithIguanaSecretBuilder: UtxoCoinBuilderCommonOps { }; let key_pair = KeyPair::from_private(private).map_to_mm(|e| UtxoCoinBuildError::Internal(e.to_string()))?; let priv_key_policy = PrivKeyPolicy::Iguana(key_pair); - build_utxo_coin_fields_with_conf_and_policy(self, conf, priv_key_policy).await + let addr_format = self.address_format()?; + let my_address = AddressBuilder::new( + addr_format, + AddressHashEnum::AddressHash(key_pair.public().address_hash()), + conf.checksum_type, + conf.address_prefixes.clone(), + conf.bech32_hrp.clone(), + ) + .as_pkh() + .build() + .map_to_mm(UtxoCoinBuildError::Internal)?; + let derivation_method = DerivationMethod::SingleAddress(my_address); + build_utxo_coin_fields_with_conf_and_policy(self, conf, priv_key_policy, derivation_method).await } } @@ -180,12 +192,17 @@ pub trait UtxoFieldsWithGlobalHDBuilder: UtxoCoinBuilderCommonOps { ) -> UtxoCoinBuildResult { let conf = UtxoConfBuilder::new(self.conf(), self.activation_params(), self.ticker()).build()?; - let derivation_path = conf + let path_to_address = self.activation_params().path_to_address; + let path_to_coin = conf .derivation_path .as_ref() .or_mm_err(|| UtxoConfError::DerivationPathIsNotSet)?; let secret = global_hd_ctx - .derive_secp256k1_secret(derivation_path, &self.activation_params().path_to_address) + .derive_secp256k1_secret( + &path_to_address + .to_derivation_path(path_to_coin) + .mm_err(|e| UtxoCoinBuildError::InvalidPathToAddress(e.to_string()))?, + ) .mm_err(|e| UtxoCoinBuildError::Internal(e.to_string()))?; let private = Private { prefix: conf.wif_prefix, @@ -196,12 +213,35 @@ pub trait UtxoFieldsWithGlobalHDBuilder: UtxoCoinBuilderCommonOps { let activated_key_pair = KeyPair::from_private(private).map_to_mm(|e| UtxoCoinBuildError::Internal(e.to_string()))?; let priv_key_policy = PrivKeyPolicy::HDWallet { - derivation_path: derivation_path.clone(), + path_to_coin: path_to_coin.clone(), activated_key: activated_key_pair, bip39_secp_priv_key: global_hd_ctx.root_priv_key().clone(), }; - build_utxo_coin_fields_with_conf_and_policy(self, conf, priv_key_policy).await + + let address_format = self.address_format()?; + let hd_wallet_rmd160 = *self.ctx().rmd160(); + let hd_wallet_storage = + HDWalletCoinStorage::init_with_rmd160(self.ctx(), self.ticker().to_owned(), hd_wallet_rmd160).await?; + let accounts = load_hd_accounts_from_storage(&hd_wallet_storage, path_to_coin) + .await + .mm_err(UtxoCoinBuildError::from)?; + let gap_limit = self.gap_limit(); + let hd_wallet = UtxoHDWallet { + inner: HDWallet { + hd_wallet_rmd160, + hd_wallet_storage, + derivation_path: path_to_coin.clone(), + accounts: HDAccountsMutex::new(accounts), + enabled_address: path_to_address, + gap_limit, + }, + address_format, + }; + let derivation_method = DerivationMethod::HDWallet(hd_wallet); + build_utxo_coin_fields_with_conf_and_policy(self, conf, priv_key_policy, derivation_method).await } + + fn gap_limit(&self) -> u32 { self.activation_params().gap_limit.unwrap_or(DEFAULT_GAP_LIMIT) } } // The return type is one-time used only. No need to create a type for it. @@ -227,6 +267,7 @@ async fn build_utxo_coin_fields_with_conf_and_policy( builder: &Builder, conf: UtxoCoinConf, priv_key_policy: PrivKeyPolicy, + derivation_method: DerivationMethod, ) -> UtxoCoinBuildResult where Builder: UtxoCoinBuilderCommonOps + Sync + ?Sized, @@ -245,7 +286,6 @@ where .map_to_mm(UtxoCoinBuildError::Internal)?; let my_script_pubkey = output_script(&my_address).map(|script| script.to_bytes())?; - let derivation_method = DerivationMethod::SingleAddress(my_address); let (scripthash_notification_sender, scripthash_notification_handler) = match get_scripthash_notification_handlers(builder.ctx()) { @@ -311,24 +351,27 @@ pub trait UtxoFieldsWithHardwareWalletBuilder: UtxoCoinBuilderCommonOps { let recently_spent_outpoints = AsyncMutex::new(RecentlySpentOutPoints::new(my_script_pubkey)); let address_format = self.address_format()?; - let derivation_path = conf + let path_to_coin = conf .derivation_path .clone() .or_mm_err(|| UtxoConfError::DerivationPathIsNotSet)?; let hd_wallet_storage = HDWalletCoinStorage::init(self.ctx(), ticker).await?; - let accounts = self - .load_hd_wallet_accounts(&hd_wallet_storage, &derivation_path) - .await?; + let accounts = load_hd_accounts_from_storage(&hd_wallet_storage, &path_to_coin) + .await + .mm_err(UtxoCoinBuildError::from)?; let gap_limit = self.gap_limit(); let hd_wallet = UtxoHDWallet { - hd_wallet_rmd160, - hd_wallet_storage, + inner: HDWallet { + hd_wallet_rmd160, + hd_wallet_storage, + derivation_path: path_to_coin, + accounts: HDAccountsMutex::new(accounts), + enabled_address: self.activation_params().path_to_address, + gap_limit, + }, address_format, - derivation_path, - accounts: HDAccountsMutex::new(accounts), - gap_limit, }; let (scripthash_notification_sender, scripthash_notification_handler) = @@ -377,16 +420,6 @@ pub trait UtxoFieldsWithHardwareWalletBuilder: UtxoCoinBuilderCommonOps { Ok(coin) } - async fn load_hd_wallet_accounts( - &self, - hd_wallet_storage: &HDWalletCoinStorage, - derivation_path: &StandardHDPathToCoin, - ) -> UtxoCoinBuildResult> { - utxo_common::load_hd_accounts_from_storage(hd_wallet_storage, derivation_path) - .await - .mm_err(UtxoCoinBuildError::from) - } - fn gap_limit(&self) -> u32 { self.activation_params().gap_limit.unwrap_or(DEFAULT_GAP_LIMIT) } fn supports_trezor(&self, conf: &UtxoCoinConf) -> bool { conf.trezor_coin.is_some() } diff --git a/mm2src/coins/utxo/utxo_builder/utxo_conf_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_conf_builder.rs index a154a7135b..befbae70f9 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_conf_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_conf_builder.rs @@ -3,7 +3,7 @@ use crate::utxo::{parse_hex_encoded_u32, UtxoCoinConf, DEFAULT_DYNAMIC_FEE_VOLAT MATURE_CONFIRMATIONS_DEFAULT}; use crate::UtxoActivationParams; use bitcrypto::ChecksumType; -use crypto::{Bip32Error, StandardHDPathToCoin}; +use crypto::{Bip32Error, HDPathToCoin}; use derive_more::Display; use keys::NetworkAddressPrefixes; pub use keys::{Address, AddressFormat as UtxoAddressFormat, AddressHashEnum, AddressScriptType, KeyPair, Private, @@ -307,7 +307,7 @@ impl<'a> UtxoConfBuilder<'a> { .map_to_mm(|e| UtxoConfError::ErrorDeserializingSPVConf(e.to_string())) } - fn derivation_path(&self) -> UtxoConfResult> { + fn derivation_path(&self) -> UtxoConfResult> { json::from_value(self.conf["derivation_path"].clone()) .map_to_mm(|e| UtxoConfError::ErrorDeserializingDerivationPath(e.to_string())) } diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index ec9dd060ea..6e5ee35510 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -1,22 +1,19 @@ use super::*; -use crate::coin_balance::{AddressBalanceStatus, HDAddressBalance, HDWalletBalanceOps}; +use crate::coin_balance::{HDAddressBalance, HDWalletBalanceObject, HDWalletBalanceOps}; use crate::coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentResult}; use crate::eth::EthCoinType; -use crate::hd_confirm_address::HDConfirmAddress; -use crate::hd_pubkey::{ExtractExtendedPubkey, HDExtractPubkeyError, HDXPubExtractor}; -use crate::hd_wallet::{AccountUpdatingError, AddressDerivingResult, HDAccountMut, HDAccountsMap, - NewAccountCreatingError, NewAddressDeriveConfirmError, NewAddressDerivingError}; -use crate::hd_wallet_storage::{HDWalletCoinWithStorageOps, HDWalletStorageResult}; +use crate::hd_wallet::{HDCoinAddress, HDCoinHDAccount, HDCoinWithdrawOps, TrezorCoinError}; use crate::lp_price::get_base_price_in_rel; use crate::rpc_command::init_withdraw::WithdrawTaskHandleShared; use crate::utxo::rpc_clients::{electrum_script_hash, BlockHashOrHeight, UnspentInfo, UnspentMap, UtxoRpcClientEnum, UtxoRpcClientOps, UtxoRpcResult}; use crate::utxo::spv::SimplePaymentVerification; use crate::utxo::tx_cache::TxCacheResult; +use crate::utxo::utxo_hd_wallet::UtxoHDAddress; use crate::utxo::utxo_withdraw::{InitUtxoWithdraw, StandardUtxoWithdraw, UtxoWithdraw}; use crate::watcher_common::validate_watcher_reward; -use crate::{CanRefundHtlc, CoinBalance, CoinWithDerivationMethod, ConfirmPaymentInput, DexFee, GenPreimageResult, - GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, GetWithdrawSenderAddress, HDAccountAddressId, +use crate::{scan_for_new_addresses_impl, CanRefundHtlc, CoinBalance, CoinWithDerivationMethod, ConfirmPaymentInput, + DexFee, GenPreimageResult, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, GetWithdrawSenderAddress, RawTransactionError, RawTransactionRequest, RawTransactionRes, RawTransactionResult, RefundFundingSecretArgs, RefundMakerPaymentArgs, RefundPaymentArgs, RewardTarget, SearchForSwapTxSpendInput, SendMakerPaymentArgs, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, @@ -28,9 +25,9 @@ use crate::{CanRefundHtlc, CoinBalance, CoinWithDerivationMethod, ConfirmPayment ValidateTakerFundingSpendPreimageError, ValidateTakerFundingSpendPreimageResult, ValidateTakerPaymentSpendPreimageError, ValidateTakerPaymentSpendPreimageResult, ValidateWatcherSpendInput, VerificationError, VerificationResult, WatcherSearchForSwapTxSpendInput, - WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFrom, WithdrawResult, - WithdrawSenderAddress, EARLY_CONFIRMATION_ERR_LOG, INVALID_RECEIVER_ERR_LOG, INVALID_REFUND_TX_ERR_LOG, - INVALID_SCRIPT_ERR_LOG, INVALID_SENDER_ERR_LOG, OLD_TRANSACTION_ERR_LOG}; + WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawResult, WithdrawSenderAddress, + EARLY_CONFIRMATION_ERR_LOG, INVALID_RECEIVER_ERR_LOG, INVALID_REFUND_TX_ERR_LOG, INVALID_SCRIPT_ERR_LOG, + INVALID_SENDER_ERR_LOG, OLD_TRANSACTION_ERR_LOG}; use crate::{MmCoinEnum, WatcherReward, WatcherRewardError}; use base64::engine::general_purpose::STANDARD; use base64::Engine; @@ -40,8 +37,8 @@ use chain::constants::SEQUENCE_FINAL; use chain::{OutPoint, TransactionInput, TransactionOutput}; use common::executor::Timer; use common::jsonrpc_client::JsonRpcErrorType; -use common::log::{debug, error, warn}; -use crypto::{Bip32DerPathOps, Bip44Chain, RpcDerivationPath, StandardHDPath, StandardHDPathError}; +use common::log::{debug, error}; +use crypto::Bip44Chain; use futures::compat::Future01CompatExt; use futures::future::{FutureExt, TryFutureExt}; use futures01::future::Either; @@ -69,8 +66,6 @@ use utxo_signer::with_key_pair::{calc_and_sign_sighash, p2sh_spend, signature_ha SIGHASH_SINGLE}; use utxo_signer::UtxoSignerOps; -pub use chain::Transaction as UtxoTx; - pub mod utxo_tx_history_v2_common; pub const DEFAULT_FEE_VOUT: usize = 0; @@ -111,234 +106,33 @@ pub async fn get_tx_fee(coin: &UtxoCoinFields) -> UtxoRpcResult { } } -fn derive_address_with_cache( +pub(crate) fn address_from_extended_pubkey( coin: &T, - hd_account: &UtxoHDAccount, - hd_addresses_cache: &mut HashMap, - hd_address_id: HDAddressId, -) -> AddressDerivingResult + extended_pubkey: &Secp256k1ExtendedPublicKey, + derivation_path: DerivationPath, +) -> UtxoHDAddress where T: UtxoCommonOps, { - // Check if the given HD address has been derived already. - if let Some(hd_address) = hd_addresses_cache.get(&hd_address_id) { - return Ok(hd_address.clone()); - } - - let change_child = hd_address_id.chain.to_child_number(); - let address_id_child = ChildNumber::from(hd_address_id.address_id); - - let derived_pubkey = hd_account - .extended_pubkey - .derive_child(change_child)? - .derive_child(address_id_child)?; - let address = coin.address_from_extended_pubkey(&derived_pubkey); - let pubkey = Public::Compressed(H264::from(derived_pubkey.public_key().serialize())); + let pubkey = Public::Compressed(H264::from(extended_pubkey.public_key().serialize())); + let address = coin.address_from_pubkey(&pubkey); - let mut derivation_path = hd_account.account_derivation_path.to_derivation_path(); - derivation_path.push(change_child); - derivation_path.push(address_id_child); - - let hd_address = HDAddress { + UtxoHDAddress { address, pubkey, derivation_path, - }; - - // Cache the derived `hd_address`. - hd_addresses_cache.insert(hd_address_id, hd_address.clone()); - Ok(hd_address) -} - -/// [`HDWalletCoinOps::derive_addresses`] native implementation. -/// -/// # Important -/// -/// The [`HDAddressesCache::cache`] mutex is locked once for the entire duration of this function. -#[cfg(not(target_arch = "wasm32"))] -pub async fn derive_addresses( - coin: &T, - hd_account: &UtxoHDAccount, - address_ids: Ids, -) -> AddressDerivingResult> -where - T: UtxoCommonOps, - Ids: Iterator, -{ - let mut hd_addresses_cache = hd_account.derived_addresses.lock().await; - address_ids - .map(|hd_address_id| derive_address_with_cache(coin, hd_account, &mut hd_addresses_cache, hd_address_id)) - .collect() -} - -/// [`HDWalletCoinOps::derive_addresses`] WASM implementation. -/// -/// # Important -/// -/// This function locks [`HDAddressesCache::cache`] mutex at each iteration. -/// -/// # Performance -/// -/// Locking the [`HDAddressesCache::cache`] mutex at each iteration may significantly degrade performance. -/// But this is required at least for now due the facts that: -/// 1) mm2 runs in the same thread as `KomodoPlatform/air_dex` runs; -/// 2) [`ExtendedPublicKey::derive_child`] is a synchronous operation, and it takes a long time. -/// So we need to periodically invoke Javascript runtime to handle UI events and other asynchronous tasks. -#[cfg(target_arch = "wasm32")] -pub async fn derive_addresses( - coin: &T, - hd_account: &UtxoHDAccount, - address_ids: Ids, -) -> AddressDerivingResult> -where - T: UtxoCommonOps, - Ids: Iterator, -{ - let mut result = Vec::new(); - for hd_address_id in address_ids { - let mut hd_addresses_cache = hd_account.derived_addresses.lock().await; - - let hd_address = derive_address_with_cache(coin, hd_account, &mut hd_addresses_cache, hd_address_id)?; - result.push(hd_address); } - - Ok(result) } -pub async fn generate_and_confirm_new_address( - coin: &Coin, - hd_wallet: &Coin::HDWallet, - hd_account: &mut Coin::HDAccount, - chain: Bip44Chain, - confirm_address: &ConfirmAddress, -) -> MmResult, NewAddressDeriveConfirmError> +pub(crate) fn trezor_coin(coin: &Coin) -> MmResult where - Coin: HDWalletCoinWithStorageOps
- + AsRef - + Sync, - ConfirmAddress: HDConfirmAddress, + Coin: AsRef, { - use crate::hd_wallet::inner_impl; - - let inner_impl::NewAddress { - address, - new_known_addresses_number, - } = inner_impl::generate_new_address_immutable(coin, hd_wallet, hd_account, chain).await?; - - let trezor_coin = coin.as_ref().conf.trezor_coin.clone().or_mm_err(|| { + coin.as_ref().conf.trezor_coin.clone().or_mm_err(|| { let ticker = &coin.as_ref().conf.ticker; - let error = format!("'{ticker}' coin must contain the 'trezor_coin' field in the coins config"); - NewAddressDeriveConfirmError::DeriveError(NewAddressDerivingError::Internal(error)) - })?; - let expected_address = address.address.to_string(); - // Ask the user to confirm if the given `expected_address` is the same as on the HW display. - confirm_address - .confirm_utxo_address(trezor_coin, address.derivation_path.clone(), expected_address) - .await?; - - let actual_known_addresses_number = hd_account.known_addresses_number(chain)?; - // Check if the actual `known_addresses_number` hasn't been changed while we waited for the user confirmation. - // If the actual value is greater than the new one, we don't need to update. - if actual_known_addresses_number < new_known_addresses_number { - coin.set_known_addresses_number(hd_wallet, hd_account, chain, new_known_addresses_number) - .await?; - } - - Ok(address) -} - -pub async fn create_new_account<'a, Coin, XPubExtractor>( - coin: &Coin, - hd_wallet: &'a UtxoHDWallet, - xpub_extractor: &XPubExtractor, -) -> MmResult, NewAccountCreatingError> -where - Coin: ExtractExtendedPubkey - + HDWalletCoinWithStorageOps - + Sync, - XPubExtractor: HDXPubExtractor, -{ - const INIT_ACCOUNT_ID: u32 = 0; - let new_account_id = hd_wallet - .accounts - .lock() - .await - .iter() - // The last element of the BTreeMap has the max account index. - .last() - .map(|(account_id, _account)| *account_id + 1) - .unwrap_or(INIT_ACCOUNT_ID); - let max_accounts_number = hd_wallet.account_limit(); - if new_account_id >= max_accounts_number { - return MmError::err(NewAccountCreatingError::AccountLimitReached { max_accounts_number }); - } - - let account_child_hardened = true; - let account_child = ChildNumber::new(new_account_id, account_child_hardened) - .map_to_mm(|e| NewAccountCreatingError::Internal(e.to_string()))?; - - let account_derivation_path: StandardHDPathToAccount = hd_wallet.derivation_path.derive(account_child)?; - let account_pubkey = coin - .extract_extended_pubkey(xpub_extractor, account_derivation_path.to_derivation_path()) - .await?; - - let new_account = UtxoHDAccount { - account_id: new_account_id, - extended_pubkey: account_pubkey, - account_derivation_path, - // We don't know how many addresses are used by the user at this moment. - external_addresses_number: 0, - internal_addresses_number: 0, - derived_addresses: HDAddressesCache::default(), - }; - - let accounts = hd_wallet.accounts.lock().await; - if accounts.contains_key(&new_account_id) { - let error = format!( - "Account '{}' has been activated while we proceed the 'create_new_account' function", - new_account_id - ); - return MmError::err(NewAccountCreatingError::Internal(error)); - } - - coin.upload_new_account(hd_wallet, new_account.to_storage_item()) - .await?; - - Ok(AsyncMutexGuard::map(accounts, |accounts| { - accounts - .entry(new_account_id) - // the `entry` method should return [`Entry::Vacant`] due to the checks above - .or_insert(new_account) - })) -} - -pub async fn set_known_addresses_number( - coin: &T, - hd_wallet: &UtxoHDWallet, - hd_account: &mut UtxoHDAccount, - chain: Bip44Chain, - new_known_addresses_number: u32, -) -> MmResult<(), AccountUpdatingError> -where - T: HDWalletCoinWithStorageOps + Sync, -{ - let max_addresses_number = hd_wallet.address_limit(); - if new_known_addresses_number >= max_addresses_number { - return MmError::err(AccountUpdatingError::AddressLimitReached { max_addresses_number }); - } - match chain { - Bip44Chain::External => { - coin.update_external_addresses_number(hd_wallet, hd_account.account_id, new_known_addresses_number) - .await?; - hd_account.external_addresses_number = new_known_addresses_number; - }, - Bip44Chain::Internal => { - coin.update_internal_addresses_number(hd_wallet, hd_account.account_id, new_known_addresses_number) - .await?; - hd_account.internal_addresses_number = new_known_addresses_number; - }, - } - Ok(()) + let error = format!("'{ticker}' coin has 'trezor_coin' field as `None` in the coins config"); + TrezorCoinError::Internal(error) + }) } pub async fn produce_hd_address_scanner(coin: &T) -> BalanceResult @@ -351,13 +145,13 @@ where pub async fn scan_for_new_addresses( coin: &T, hd_wallet: &T::HDWallet, - hd_account: &mut T::HDAccount, + hd_account: &mut HDCoinHDAccount, address_scanner: &T::HDAddressScanner, gap_limit: u32, -) -> BalanceResult> +) -> BalanceResult>>> where T: HDWalletBalanceOps + Sync, - T::Address: std::fmt::Display, + HDCoinAddress: std::fmt::Display, { let mut addresses = scan_for_new_addresses_impl( coin, @@ -383,93 +177,13 @@ where Ok(addresses) } -/// Checks addresses that either had empty transaction history last time we checked or has not been checked before. -/// The checking stops at the moment when we find `gap_limit` consecutive empty addresses. -pub async fn scan_for_new_addresses_impl( - coin: &T, - hd_wallet: &T::HDWallet, - hd_account: &mut T::HDAccount, - address_scanner: &T::HDAddressScanner, - chain: Bip44Chain, - gap_limit: u32, -) -> BalanceResult> -where - T: HDWalletBalanceOps + Sync, - T::Address: std::fmt::Display, -{ - let mut balances = Vec::with_capacity(gap_limit as usize); - - // Get the first unknown address id. - let mut checking_address_id = hd_account - .known_addresses_number(chain) - // A UTXO coin should support both [`Bip44Chain::External`] and [`Bip44Chain::Internal`]. - .mm_err(|e| BalanceError::Internal(e.to_string()))?; - - let mut unused_addresses_counter = 0; - let max_addresses_number = hd_wallet.address_limit(); - while checking_address_id < max_addresses_number && unused_addresses_counter <= gap_limit { - let HDAddress { - address: checking_address, - derivation_path: checking_address_der_path, - .. - } = coin.derive_address(hd_account, chain, checking_address_id).await?; - - match coin.is_address_used(&checking_address, address_scanner).await? { - // We found a non-empty address, so we have to fill up the balance list - // with zeros starting from `last_non_empty_address_id = checking_address_id - unused_addresses_counter`. - AddressBalanceStatus::Used(non_empty_balance) => { - let last_non_empty_address_id = checking_address_id - unused_addresses_counter; - - // First, derive all empty addresses and put it into `balances` with default balance. - let address_ids = (last_non_empty_address_id..checking_address_id) - .into_iter() - .map(|address_id| HDAddressId { chain, address_id }); - let empty_addresses = - coin.derive_addresses(hd_account, address_ids) - .await? - .into_iter() - .map(|empty_address| HDAddressBalance { - address: empty_address.address.to_string(), - derivation_path: RpcDerivationPath(empty_address.derivation_path), - chain, - balance: CoinBalance::default(), - }); - balances.extend(empty_addresses); - - // Then push this non-empty address. - balances.push(HDAddressBalance { - address: checking_address.to_string(), - derivation_path: RpcDerivationPath(checking_address_der_path), - chain, - balance: non_empty_balance, - }); - // Reset the counter of unused addresses to zero since we found a non-empty address. - unused_addresses_counter = 0; - }, - AddressBalanceStatus::NotUsed => unused_addresses_counter += 1, - } - - checking_address_id += 1; - } - - coin.set_known_addresses_number( - hd_wallet, - hd_account, - chain, - checking_address_id - unused_addresses_counter, - ) - .await?; - - Ok(balances) -} - pub async fn all_known_addresses_balances( coin: &T, - hd_account: &T::HDAccount, -) -> BalanceResult> + hd_account: &HDCoinHDAccount, +) -> BalanceResult>>> where T: HDWalletBalanceOps + Sync, - T::Address: std::fmt::Display + Clone, + HDCoinAddress: std::fmt::Display + Clone, { let external_addresses = hd_account .known_addresses_number(Bip44Chain::External) @@ -491,29 +205,6 @@ where Ok(balances) } -pub async fn load_hd_accounts_from_storage( - hd_wallet_storage: &HDWalletCoinStorage, - derivation_path: &StandardHDPathToCoin, -) -> HDWalletStorageResult> { - let accounts = hd_wallet_storage.load_all_accounts().await?; - let res: HDWalletStorageResult> = accounts - .iter() - .map(|account_info| { - let account = UtxoHDAccount::try_from_storage_item(derivation_path, account_info)?; - Ok((account.account_id, account)) - }) - .collect(); - match res { - Ok(accounts) => Ok(accounts), - Err(e) if e.get_inner().is_deserializing_err() => { - warn!("Error loading HD accounts from the storage: '{}'. Clear accounts", e); - hd_wallet_storage.clear_accounts().await?; - Ok(HDAccountsMap::new()) - }, - Err(e) => Err(e), - } -} - /// Requests balance of the given `address`. pub async fn address_balance(coin: &T, address: &Address) -> BalanceResult where @@ -575,22 +266,6 @@ where pub fn derivation_method(coin: &UtxoCoinFields) -> &DerivationMethod { &coin.derivation_method } -pub async fn extract_extended_pubkey( - conf: &UtxoCoinConf, - xpub_extractor: &XPubExtractor, - derivation_path: DerivationPath, -) -> MmResult -where - XPubExtractor: HDXPubExtractor, -{ - let trezor_coin = conf - .trezor_coin - .clone() - .or_mm_err(|| HDExtractPubkeyError::CoinDoesntSupportTrezor)?; - let xpub = xpub_extractor.extract_utxo_xpub(trezor_coin, derivation_path).await?; - Secp256k1ExtendedPublicKey::from_str(&xpub).map_to_mm(|e| HDExtractPubkeyError::InvalidXpub(e.to_string())) -} - /// returns the fee required to be paid for HTLC spend transaction pub async fn get_htlc_spend_fee( coin: &T, @@ -806,11 +481,11 @@ pub struct UtxoTxBuilder<'a, T: AsRef + UtxoTxGenerationOps> { } impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { - pub fn new(coin: &'a T) -> Self { + pub async fn new(coin: &'a T) -> UtxoTxBuilder<'a, T> { UtxoTxBuilder { tx: coin.as_ref().transaction_preimage(), coin, - from: coin.as_ref().derivation_method.single_addr().cloned(), + from: coin.as_ref().derivation_method.single_addr().await, available_inputs: vec![], fee_policy: FeePolicy::SendExact, fee: None, @@ -1732,10 +1407,10 @@ pub async fn sign_and_broadcast_taker_payment_spend( return TX_PLAIN_ERR!("Payment amount is too small to cover miner fee + dust + dex_fee_sat"); } - let maker_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err()); + let maker_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err().await); let maker_output = TransactionOutput { value: payment_output.value - miner_fee - dex_fee_sat, - script_pubkey: try_tx_s!(output_script(maker_address)).to_bytes(), + script_pubkey: try_tx_s!(output_script(&maker_address)).to_bytes(), }; signer.outputs.push(maker_output); } @@ -1895,13 +1570,14 @@ where Box::new(send_fut) } -pub fn send_maker_spends_taker_payment(coin: T, args: SpendPaymentArgs) -> TransactionFut { - let my_address = try_tx_fus!(coin.as_ref().derivation_method.single_addr_or_err()).clone(); - let mut prev_transaction: UtxoTx = try_tx_fus!(deserialize(args.other_payment_tx).map_err(|e| ERRL!("{:?}", e))); +pub async fn send_maker_spends_taker_payment( + coin: T, + args: SpendPaymentArgs<'_>, +) -> TransactionResult { + let mut prev_transaction: UtxoTx = try_tx_s!(deserialize(args.other_payment_tx).map_err(|e| ERRL!("{:?}", e))); prev_transaction.tx_hash_algo = coin.as_ref().tx_hash_algo; drop_mutability!(prev_transaction); - - let payment_value = try_tx_fus!(prev_transaction.first_output()).value; + let payment_value = try_tx_s!(prev_transaction.first_output()).value; let key_pair = coin.derive_htlc_key_pair(args.swap_unique_data); let script_data = Builder::default() @@ -1909,49 +1585,47 @@ pub fn send_maker_spends_taker_payment(coin: T, args .push_opcode(Opcode::OP_0) .into_script(); - let time_lock = try_tx_fus!(args.time_lock.try_into()); + let time_lock = try_tx_s!(args.time_lock.try_into()); let redeem_script = payment_script( time_lock, args.secret_hash, - &try_tx_fus!(Public::from_slice(args.other_pubkey)), + &try_tx_s!(Public::from_slice(args.other_pubkey)), key_pair.public(), ) .into(); - let fut = async move { - let fee = try_tx_s!( - coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) - .await + let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err().await); + let fee = try_tx_s!( + coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) + .await + ); + if fee >= payment_value { + return TX_PLAIN_ERR!( + "HTLC spend fee {} is greater than transaction output {}", + fee, + payment_value ); - if fee >= payment_value { - return TX_PLAIN_ERR!( - "HTLC spend fee {} is greater than transaction output {}", - fee, - payment_value - ); - } - let script_pubkey = output_script(&my_address).map(|script| script.to_bytes())?; - let output = TransactionOutput { - value: payment_value - fee, - script_pubkey, - }; + } + let script_pubkey = output_script(&my_address).map(|script| script.to_bytes())?; + let output = TransactionOutput { + value: payment_value - fee, + script_pubkey, + }; - let input = P2SHSpendingTxInput { - prev_transaction, - redeem_script, - outputs: vec![output], - script_data, - sequence: SEQUENCE_FINAL, - lock_time: time_lock, - keypair: &key_pair, - }; - let transaction = try_tx_s!(coin.p2sh_spending_tx(input).await); + let input = P2SHSpendingTxInput { + prev_transaction, + redeem_script, + outputs: vec![output], + script_data, + sequence: SEQUENCE_FINAL, + lock_time: time_lock, + keypair: &key_pair, + }; + let transaction = try_tx_s!(coin.p2sh_spending_tx(input).await); - let tx_fut = coin.as_ref().rpc_client.send_transaction(&transaction).compat(); - try_tx_s!(tx_fut.await, transaction); + let tx_fut = coin.as_ref().rpc_client.send_transaction(&transaction).compat(); + try_tx_s!(tx_fut.await, transaction); - Ok(transaction.into()) - }; - Box::new(fut.boxed().compat()) + Ok(transaction.into()) } pub fn send_maker_payment_spend_preimage( @@ -2005,7 +1679,6 @@ pub fn create_maker_payment_spend_preimage( secret_hash: &[u8], swap_unique_data: &[u8], ) -> TransactionFut { - let my_address = try_tx_fus!(coin.as_ref().derivation_method.single_addr_or_err()).clone(); let mut prev_transaction: UtxoTx = try_tx_fus!(deserialize(maker_payment_tx).map_err(|e| ERRL!("{:?}", e))); prev_transaction.tx_hash_algo = coin.as_ref().tx_hash_algo; drop_mutability!(prev_transaction); @@ -2023,6 +1696,7 @@ pub fn create_maker_payment_spend_preimage( .into(); let coin = coin.clone(); let fut = async move { + let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err().await); let fee = try_tx_s!( coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WatcherPreimage) .await @@ -2066,7 +1740,6 @@ pub fn create_taker_payment_refund_preimage( swap_unique_data: &[u8], ) -> TransactionFut { let coin = coin.clone(); - let my_address = try_tx_fus!(coin.as_ref().derivation_method.single_addr_or_err()).clone(); let mut prev_transaction: UtxoTx = try_tx_fus!(deserialize(taker_payment_tx).map_err(|e| TransactionErr::Plain(format!("{:?}", e)))); prev_transaction.tx_hash_algo = coin.as_ref().tx_hash_algo; @@ -2083,6 +1756,7 @@ pub fn create_taker_payment_refund_preimage( ) .into(); let fut = async move { + let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err().await); let fee = try_tx_s!( coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WatcherPreimage) .await @@ -2116,12 +1790,14 @@ pub fn create_taker_payment_refund_preimage( Box::new(fut.boxed().compat()) } -pub fn send_taker_spends_maker_payment(coin: T, args: SpendPaymentArgs) -> TransactionFut { - let my_address = try_tx_fus!(coin.as_ref().derivation_method.single_addr_or_err()).clone(); - let mut prev_transaction: UtxoTx = try_tx_fus!(deserialize(args.other_payment_tx).map_err(|e| ERRL!("{:?}", e))); +pub async fn send_taker_spends_maker_payment( + coin: T, + args: SpendPaymentArgs<'_>, +) -> TransactionResult { + let mut prev_transaction: UtxoTx = try_tx_s!(deserialize(args.other_payment_tx).map_err(|e| ERRL!("{:?}", e))); prev_transaction.tx_hash_algo = coin.as_ref().tx_hash_algo; drop_mutability!(prev_transaction); - let payment_value = try_tx_fus!(prev_transaction.first_output()).value; + let payment_value = try_tx_s!(prev_transaction.first_output()).value; let key_pair = coin.derive_htlc_key_pair(args.swap_unique_data); @@ -2130,57 +1806,55 @@ pub fn send_taker_spends_maker_payment(coin: T, args .push_opcode(Opcode::OP_0) .into_script(); - let time_lock = try_tx_fus!(args.time_lock.try_into()); + let time_lock = try_tx_s!(args.time_lock.try_into()); let redeem_script = payment_script( time_lock, args.secret_hash, - &try_tx_fus!(Public::from_slice(args.other_pubkey)), + &try_tx_s!(Public::from_slice(args.other_pubkey)), key_pair.public(), ) .into(); - let fut = async move { - let fee = try_tx_s!( - coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) - .await + let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err().await); + let fee = try_tx_s!( + coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) + .await + ); + if fee >= payment_value { + return TX_PLAIN_ERR!( + "HTLC spend fee {} is greater than transaction output {}", + fee, + payment_value ); - if fee >= payment_value { - return TX_PLAIN_ERR!( - "HTLC spend fee {} is greater than transaction output {}", - fee, - payment_value - ); - } - let script_pubkey = output_script(&my_address).map(|script| script.to_bytes())?; - let output = TransactionOutput { - value: payment_value - fee, - script_pubkey, - }; + } + let script_pubkey = output_script(&my_address).map(|script| script.to_bytes())?; + let output = TransactionOutput { + value: payment_value - fee, + script_pubkey, + }; - let input = P2SHSpendingTxInput { - prev_transaction, - redeem_script, - outputs: vec![output], - script_data, - sequence: SEQUENCE_FINAL, - lock_time: time_lock, - keypair: &key_pair, - }; - let transaction = try_tx_s!(coin.p2sh_spending_tx(input).await); + let input = P2SHSpendingTxInput { + prev_transaction, + redeem_script, + outputs: vec![output], + script_data, + sequence: SEQUENCE_FINAL, + lock_time: time_lock, + keypair: &key_pair, + }; + let transaction = try_tx_s!(coin.p2sh_spending_tx(input).await); - let tx_fut = coin.as_ref().rpc_client.send_transaction(&transaction).compat(); - try_tx_s!(tx_fut.await, transaction); + let tx_fut = coin.as_ref().rpc_client.send_transaction(&transaction).compat(); + try_tx_s!(tx_fut.await, transaction); - Ok(transaction.into()) - }; - Box::new(fut.boxed().compat()) + Ok(transaction.into()) } pub async fn refund_htlc_payment( coin: T, args: RefundPaymentArgs<'_>, ) -> Result { - let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err()).clone(); + let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err().await).clone(); let mut prev_transaction: UtxoTx = try_tx_s!(deserialize(args.payment_tx).map_err(|e| TransactionErr::Plain(format!("{:?}", e)))); prev_transaction.tx_hash_algo = coin.as_ref().tx_hash_algo; @@ -2713,24 +2387,26 @@ pub fn validate_payment_spend_or_refund( let mut payment_spend_tx: UtxoTx = try_f!(deserialize(input.payment_tx.as_slice())); payment_spend_tx.tx_hash_algo = coin.as_ref().tx_hash_algo; - let my_address = try_f!(coin.as_ref().derivation_method.single_addr_or_err()); - let expected_script_pubkey = try_f!(output_script(my_address).map(|script| script.to_bytes())); - let output = try_f!(payment_spend_tx - .outputs - .get(DEFAULT_SWAP_VOUT) - .ok_or_else(|| ValidatePaymentError::WrongPaymentTx("Payment tx has no outputs".to_string(),))); + let coin = coin.clone(); + let fut = async move { + let my_address = coin.as_ref().derivation_method.single_addr_or_err().await?; + let expected_script_pubkey = output_script(&my_address).map(|script| script.to_bytes())?; + let output = payment_spend_tx + .outputs + .get(DEFAULT_SWAP_VOUT) + .ok_or_else(|| ValidatePaymentError::WrongPaymentTx("Payment tx has no outputs".to_string()))?; - if expected_script_pubkey != output.script_pubkey { - return Box::new(futures01::future::err( - ValidatePaymentError::WrongPaymentTx(format!( + if expected_script_pubkey != output.script_pubkey { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "Provided payment tx script pubkey doesn't match expected {:?} {:?}", output.script_pubkey, expected_script_pubkey - )) - .into(), - )); - } + ))); + } - Box::new(futures01::future::ok(())) + Ok(()) + }; + + Box::new(fut.boxed().compat()) } pub fn check_if_my_payment_sent( @@ -2974,13 +2650,15 @@ pub fn my_balance(coin: T) -> BalanceFut where T: UtxoCommonOps + GetUtxoListOps + MarketCoinOps, { - let my_address = try_f!(coin - .as_ref() - .derivation_method - .single_addr_or_err() - .mm_err(BalanceError::from)) - .clone(); - let fut = async move { address_balance(&coin, &my_address).await }; + let fut = async move { + let my_address = coin + .as_ref() + .derivation_method + .single_addr_or_err() + .await + .mm_err(BalanceError::from)?; + address_balance(&coin, &my_address).await + }; Box::new(fut.boxed().compat()) } @@ -3151,6 +2829,7 @@ async fn sign_raw_utxo_tx + UtxoTxGenerationOps>( input_signer_incomplete.consensus_branch_id = coin.as_ref().conf.consensus_branch_id; let builder = UtxoTxBuilder::new(coin) + .await .with_transaction_input_signer(input_signer_incomplete) .add_available_inputs(unspents); let unsigned = builder @@ -3302,7 +2981,7 @@ pub fn display_priv_key(coin: &UtxoCoinFields) -> Result { activated_key: ref activated_key_pair, .. } => Ok(activated_key_pair.private().to_string()), - PrivKeyPolicy::Trezor => ERR!("'display_priv_key' doesn't support Hardware Wallets"), + PrivKeyPolicy::Trezor => ERR!("'display_priv_key' is not supported for Hardware Wallets"), #[cfg(target_arch = "wasm32")] PrivKeyPolicy::Metamask(_) => ERR!("'display_priv_key' doesn't support Metamask"), } @@ -3345,9 +3024,13 @@ pub async fn get_tx_hex_by_hash(coin: &UtxoCoinFields, tx_hash: Vec) -> RawT pub async fn withdraw(coin: T, req: WithdrawRequest) -> WithdrawResult where - T: UtxoCommonOps + GetUtxoListOps + MarketCoinOps, + T: UtxoCommonOps + + GetUtxoListOps + + MarketCoinOps + + CoinWithDerivationMethod + + GetWithdrawSenderAddress
, { - StandardUtxoWithdraw::new(coin, req)?.build().await + StandardUtxoWithdraw::new(coin, req).await?.build().await } pub async fn init_withdraw( @@ -3371,13 +3054,16 @@ pub async fn get_withdraw_from_address( req: &WithdrawRequest, ) -> MmResult, WithdrawError> where - T: CoinWithDerivationMethod
::HDWallet> - + HDWalletCoinOps
- + UtxoCommonOps, + T: CoinWithDerivationMethod + HDWalletCoinOps + HDCoinWithdrawOps + UtxoCommonOps, { match coin.derivation_method() { DerivationMethod::SingleAddress(my_address) => get_withdraw_iguana_sender(coin, req, my_address), - DerivationMethod::HDWallet(hd_wallet) => get_withdraw_hd_sender(coin, req, hd_wallet).await, + DerivationMethod::HDWallet(hd_wallet) => { + let from = req.from.clone().or_mm_err(|| WithdrawError::FromAddressNotFound)?; + coin.get_withdraw_hd_sender(hd_wallet, &from) + .await + .mm_err(WithdrawError::from) + }, } } @@ -3401,60 +3087,6 @@ pub fn get_withdraw_iguana_sender( }) } -pub async fn get_withdraw_hd_sender( - coin: &T, - req: &WithdrawRequest, - hd_wallet: &T::HDWallet, -) -> MmResult, WithdrawError> -where - T: HDWalletCoinOps
+ Sync, -{ - let HDAccountAddressId { - account_id, - chain, - address_id, - } = match req.from.clone().or_mm_err(|| WithdrawError::FromAddressNotFound)? { - WithdrawFrom::AddressId(id) => id, - WithdrawFrom::DerivationPath { derivation_path } => { - let derivation_path = StandardHDPath::from_str(&derivation_path) - .map_to_mm(StandardHDPathError::from) - .mm_err(|e| WithdrawError::UnexpectedFromAddress(e.to_string()))?; - let coin_type = derivation_path.coin_type(); - let expected_coin_type = hd_wallet.coin_type(); - if coin_type != expected_coin_type { - let error = format!( - "Derivation path '{}' must has '{}' coin type", - derivation_path, expected_coin_type - ); - return MmError::err(WithdrawError::UnexpectedFromAddress(error)); - } - HDAccountAddressId::from(derivation_path) - }, - WithdrawFrom::HDWalletAddress(_) => { - return MmError::err(WithdrawError::UnsupportedError( - "`WithdrawFrom::HDWalletAddress` is not supported for `get_withdraw_hd_sender`".to_string(), - )) - }, - }; - - let hd_account = hd_wallet - .get_account(account_id) - .await - .or_mm_err(|| WithdrawError::UnknownAccount { account_id })?; - let hd_address = coin.derive_address(&hd_account, chain, address_id).await?; - - let is_address_activated = hd_account - .is_address_activated(chain, address_id) - // If [`HDWalletCoinOps::derive_address`] succeeds, [`HDAccountOps::is_address_activated`] shouldn't fails with an `InvalidBip44ChainError`. - .mm_err(|e| WithdrawError::InternalError(e.to_string()))?; - if !is_address_activated { - let error = format!("'{}' address is not activated", hd_address.address); - return MmError::err(WithdrawError::UnexpectedFromAddress(error)); - } - - Ok(WithdrawSenderAddress::from(hd_address)) -} - pub fn decimals(coin: &UtxoCoinFields) -> u8 { coin.decimals } pub fn convert_to_address(coin: &T, from: &str, to_address_format: Json) -> Result { @@ -3873,11 +3505,11 @@ where .collect() }, UtxoRpcClientEnum::Electrum(client) => { - let my_address = match coin.as_ref().derivation_method.single_addr_or_err() { + let my_address = match coin.as_ref().derivation_method.single_addr_or_err().await { Ok(my_address) => my_address, Err(e) => return RequestTxHistoryResult::CriticalError(e.to_string()), }; - let script = match output_script(my_address) { + let script = match output_script(&my_address) { Ok(script) => script, Err(err) => return RequestTxHistoryResult::CriticalError(err.to_string()), }; @@ -3939,7 +3571,7 @@ pub async fn tx_details_by_hash( let verbose_tx = try_s!(coin.as_ref().rpc_client.get_verbose_transaction(&hash).compat().await); let mut tx: UtxoTx = try_s!(deserialize(verbose_tx.hex.as_slice()).map_err(|e| ERRL!("{:?}", e))); tx.tx_hash_algo = coin.as_ref().tx_hash_algo; - let my_address = try_s!(coin.as_ref().derivation_method.single_addr_or_err()); + let my_address = try_s!(coin.as_ref().derivation_method.single_addr_or_err().await); input_transactions.insert(hash, HistoryUtxoTx { tx: tx.clone(), @@ -3978,7 +3610,7 @@ pub async fn tx_details_by_hash( ))?; input_amount += prev_tx_output.value; let from: Vec
= try_s!(coin.addresses_from_script(&prev_tx_output.script_pubkey.clone().into())); - if from.contains(my_address) { + if from.contains(&my_address) { spent_by_me += prev_tx_output.value; } from_addresses.extend(from.into_iter()); @@ -3987,7 +3619,7 @@ pub async fn tx_details_by_hash( for output in tx.outputs.iter() { output_amount += output.value; let to = try_s!(coin.addresses_from_script(&output.script_pubkey.clone().into())); - if to.contains(my_address) { + if to.contains(&my_address) { received_by_me += output.value; } to_addresses.extend(to.into_iter()); @@ -4138,6 +3770,7 @@ where })); } + // Todo: https://github.com/KomodoPlatform/komodo-defi-framework/issues/1625 let my_address = &coin.my_address()?; let claimed_by_me = tx_details.from.iter().all(|from| from == my_address) && tx_details.to.contains(my_address); @@ -4229,7 +3862,7 @@ where let tx_fee = coin.get_tx_fee().await?; // [`FeePolicy::DeductFromOutput`] is used if the value is [`TradePreimageValue::UpperBound`] only let is_amount_upper_bound = matches!(fee_policy, FeePolicy::DeductFromOutput(_)); - let my_address = coin.as_ref().derivation_method.single_addr_or_err()?; + let my_address = coin.as_ref().derivation_method.single_addr_or_err().await?; match tx_fee { // if it's a dynamic fee, we should generate a swap transaction to get an actual trade fee @@ -4238,11 +3871,12 @@ where let dynamic_fee = coin.increase_dynamic_fee_by_stage(fee, stage); let outputs_count = outputs.len(); - let (unspents, _recently_sent_txs) = coin.get_unspent_ordered_list(my_address).await?; + let (unspents, _recently_sent_txs) = coin.get_unspent_ordered_list(&my_address).await?; let actual_tx_fee = ActualTxFee::Dynamic(dynamic_fee); let mut tx_builder = UtxoTxBuilder::new(coin) + .await .add_available_inputs(unspents) .add_outputs(outputs) .with_fee_policy(fee_policy) @@ -4266,9 +3900,10 @@ where }, ActualTxFee::FixedPerKb(fee) => { let outputs_count = outputs.len(); - let (unspents, _recently_sent_txs) = coin.get_unspent_ordered_list(my_address).await?; + let (unspents, _recently_sent_txs) = coin.get_unspent_ordered_list(&my_address).await?; let mut tx_builder = UtxoTxBuilder::new(coin) + .await .add_available_inputs(unspents) .add_outputs(outputs) .with_fee_policy(fee_policy) @@ -5219,7 +4854,7 @@ pub async fn refund_taker_funding_secret( where T: UtxoCommonOps + GetUtxoListOps + SwapOps, { - let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err()).clone(); + let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err().await).clone(); let payment_value = try_tx_s!(args.funding_tx.first_output()).value; let key_pair = coin.derive_htlc_key_pair(args.swap_unique_data); @@ -5385,15 +5020,20 @@ pub fn address_to_scripthash(address: &Address) -> Result { pub async fn utxo_prepare_addresses_for_balance_stream_if_enabled( coin: &T, - addresses: HashSet
, + addresses: HashSet, ) -> MmResult<(), String> where T: UtxoCommonOps, { + let mut valid_addresses = HashSet::with_capacity(addresses.len()); + for address in addresses { + let valid_address = address_from_str_unchecked(coin.as_ref(), &address).mm_err(|e| e.to_string())?; + valid_addresses.insert(valid_address); + } if let UtxoRpcClientEnum::Electrum(electrum_client) = &coin.as_ref().rpc_client { if let Some(sender) = &electrum_client.scripthash_notification_sender { sender - .unbounded_send(ScripthashNotification::SubscribeToAddresses(addresses)) + .unbounded_send(ScripthashNotification::SubscribeToAddresses(valid_addresses)) .map_err(|e| ERRL!("Failed sending scripthash message. {}", e))?; } }; @@ -5405,7 +5045,7 @@ pub async fn spend_maker_payment_v2( coin: &T, args: SpendMakerPaymentArgs<'_, T>, ) -> Result { - let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err()).clone(); + let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err().await).clone(); let payment_value = try_tx_s!(args.maker_payment_tx.first_output()).value; let key_pair = coin.derive_htlc_key_pair(args.swap_unique_data); @@ -5467,7 +5107,7 @@ pub async fn refund_maker_payment_v2_secret( where T: UtxoCommonOps + SwapOps, { - let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err()).clone(); + let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err().await).clone(); let payment_value = try_tx_s!(args.maker_payment_tx.first_output()).value; let key_pair = coin.derive_htlc_key_pair(args.swap_unique_data); diff --git a/mm2src/coins/utxo/utxo_common/utxo_tx_history_v2_common.rs b/mm2src/coins/utxo/utxo_common/utxo_tx_history_v2_common.rs index 3b4e7959e1..32577de810 100644 --- a/mm2src/coins/utxo/utxo_common/utxo_tx_history_v2_common.rs +++ b/mm2src/coins/utxo/utxo_common/utxo_tx_history_v2_common.rs @@ -1,16 +1,15 @@ use crate::coin_balance::CoinBalanceReportOps; -use crate::hd_wallet::{HDAccountOps, HDWalletCoinOps, HDWalletOps}; +use crate::hd_wallet::{HDAccountOps, HDAddressOps, HDCoinAddress, HDWalletCoinOps, HDWalletOps}; use crate::my_tx_history_v2::{CoinWithTxHistoryV2, DisplayAddress, MyTxHistoryErrorV2, MyTxHistoryTarget, TxDetailsBuilder, TxHistoryStorage}; use crate::tx_history_storage::{GetTxHistoryFilters, WalletId}; use crate::utxo::rpc_clients::{electrum_script_hash, ElectrumClient, NativeClient, UtxoRpcClientEnum}; use crate::utxo::utxo_common::{big_decimal_from_sat, HISTORY_TOO_LARGE_ERROR}; -use crate::utxo::utxo_tx_history_v2::{UtxoMyAddressesHistoryError, UtxoTxDetailsError, UtxoTxDetailsParams, - UtxoTxHistoryOps}; -use crate::utxo::{output_script, RequestTxHistoryResult, UtxoCoinFields, UtxoCommonOps, UtxoHDAccount}; +use crate::utxo::utxo_tx_history_v2::{UtxoTxDetailsError, UtxoTxDetailsParams, UtxoTxHistoryOps}; +use crate::utxo::{output_script, RequestTxHistoryResult, UtxoCoinFields, UtxoCommonOps}; use crate::{big_decimal_from_sat_unsigned, compare_transactions, BalanceResult, CoinWithDerivationMethod, - DerivationMethod, HDAccountAddressId, MarketCoinOps, NumConversError, TransactionDetails, TxFeeDetails, - TxIdHeight, UtxoFeeDetails, UtxoTx}; + DerivationMethod, HDPathAccountToAddressId, MarketCoinOps, NumConversError, TransactionDetails, + TxFeeDetails, TxIdHeight, UtxoFeeDetails, UtxoTx}; use common::jsonrpc_client::JsonRpcErrorType; use crypto::Bip44Chain; use futures::compat::Future01CompatExt; @@ -23,7 +22,6 @@ use rpc::v1::types::{TransactionInputEnum, H256 as H256Json}; use serialization::deserialize; use std::collections::{HashMap, HashSet}; use std::convert::{TryFrom, TryInto}; -use std::iter; use std::num::TryFromIntError; /// [`CoinWithTxHistoryV2::history_wallet_id`] implementation. @@ -36,11 +34,8 @@ pub async fn get_tx_history_filters( target: MyTxHistoryTarget, ) -> MmResult where - Coin: CoinWithDerivationMethod::HDWallet> - + HDWalletCoinOps - + MarketCoinOps - + Sync, - ::Address: DisplayAddress, + Coin: CoinWithDerivationMethod + MarketCoinOps + Sync, + HDCoinAddress: DisplayAddress, { match (coin.derivation_method(), target) { (DerivationMethod::SingleAddress(_), MyTxHistoryTarget::Iguana) => { @@ -57,7 +52,7 @@ where get_tx_history_filters_for_hd_address(coin, hd_wallet, hd_address_id).await }, (DerivationMethod::HDWallet(hd_wallet), MyTxHistoryTarget::AddressDerivationPath(derivation_path)) => { - let hd_address_id = HDAccountAddressId::from(derivation_path); + let hd_address_id = HDPathAccountToAddressId::from(derivation_path); get_tx_history_filters_for_hd_address(coin, hd_wallet, hd_address_id).await }, (DerivationMethod::HDWallet(_), target) => MmError::err(MyTxHistoryErrorV2::with_expected_target( @@ -75,7 +70,7 @@ async fn get_tx_history_filters_for_hd_account( ) -> MmResult where Coin: HDWalletCoinOps + Sync, - Coin::Address: DisplayAddress, + HDCoinAddress: DisplayAddress, { let hd_account = hd_wallet .get_account(account_id) @@ -88,7 +83,7 @@ where let addresses_iter = external_addresses .into_iter() .chain(internal_addresses) - .map(|hd_address| DisplayAddress::display_address(&hd_address.address)); + .map(|hd_address| DisplayAddress::display_address(&hd_address.address())); Ok(GetTxHistoryFilters::for_addresses(addresses_iter)) } @@ -96,11 +91,11 @@ where async fn get_tx_history_filters_for_hd_address( coin: &Coin, hd_wallet: &Coin::HDWallet, - hd_address_id: HDAccountAddressId, + hd_address_id: HDPathAccountToAddressId, ) -> MmResult where Coin: HDWalletCoinOps + Sync, - Coin::Address: DisplayAddress, + HDCoinAddress: DisplayAddress, { let hd_account = hd_wallet .get_account(hd_address_id.account_id) @@ -119,36 +114,7 @@ where let hd_address = coin .derive_address(&hd_account, hd_address_id.chain, hd_address_id.address_id) .await?; - Ok(GetTxHistoryFilters::for_address(hd_address.address.display_address())) -} - -/// [`UtxoTxHistoryOps::my_addresses`] implementation. -pub async fn my_addresses(coin: &Coin) -> MmResult, UtxoMyAddressesHistoryError> -where - Coin: HDWalletCoinOps
+ UtxoCommonOps, -{ - const ADDRESSES_CAPACITY: usize = 60; - - match coin.as_ref().derivation_method { - DerivationMethod::SingleAddress(ref my_address) => Ok(iter::once(my_address.clone()).collect()), - DerivationMethod::HDWallet(ref hd_wallet) => { - let hd_accounts = hd_wallet.get_accounts().await; - - let mut all_addresses = HashSet::with_capacity(ADDRESSES_CAPACITY); - for (_, hd_account) in hd_accounts { - let external_addresses = coin.derive_known_addresses(&hd_account, Bip44Chain::External).await?; - let internal_addresses = coin.derive_known_addresses(&hd_account, Bip44Chain::Internal).await?; - - let addresses_it = external_addresses - .into_iter() - .chain(internal_addresses) - .map(|hd_address| hd_address.address); - all_addresses.extend(addresses_it); - } - - Ok(all_addresses) - }, - } + Ok(GetTxHistoryFilters::for_address(hd_address.address().display_address())) } /// [`UtxoTxHistoryOps::tx_details_by_hash`] implementation. @@ -280,10 +246,14 @@ where /// Requests balances of all activated addresses. pub async fn my_addresses_balances(coin: &Coin) -> BalanceResult> where - Coin: CoinBalanceReportOps, + Coin: CoinBalanceReportOps + MarketCoinOps, { let coin_balance = coin.coin_balance_report().await?; - Ok(coin_balance.to_addresses_total_balances()) + let addresses_balances = coin_balance.to_addresses_total_balances(coin.ticker()); + Ok(addresses_balances + .into_iter() + .map(|(addr, balance)| (addr, balance.unwrap_or_default())) + .collect()) } /// [`UtxoTxHistoryOps::request_tx_history`] implementation. diff --git a/mm2src/coins/utxo/utxo_common_tests.rs b/mm2src/coins/utxo/utxo_common_tests.rs index 0e5254cfea..53e89599f3 100644 --- a/mm2src/coins/utxo/utxo_common_tests.rs +++ b/mm2src/coins/utxo/utxo_common_tests.rs @@ -1,11 +1,12 @@ use super::*; -use crate::hd_wallet::HDAccountsMap; +use crate::hd_wallet::{HDAccountsMap, HDAccountsMutex, HDAddressesCache, HDWallet, HDWalletCoinStorage}; use crate::my_tx_history_v2::{my_tx_history_v2_impl, CoinWithTxHistoryV2, MyTxHistoryDetails, MyTxHistoryRequestV2, MyTxHistoryResponseV2, MyTxHistoryTarget}; use crate::tx_history_storage::TxHistoryStorageBuilder; use crate::utxo::rpc_clients::{ElectrumClient, UtxoRpcClientOps}; use crate::utxo::tx_cache::dummy_tx_cache::DummyVerboseCache; use crate::utxo::tx_cache::UtxoVerboseCacheOps; +use crate::utxo::utxo_hd_wallet::UtxoHDAccount; use crate::utxo::utxo_tx_history_v2::{utxo_history_loop, UtxoTxHistoryOps}; use crate::{compare_transaction_details, UtxoStandardCoin}; use common::custom_futures::repeatable::{Ready, Retry}; @@ -14,11 +15,13 @@ use common::jsonrpc_client::JsonRpcErrorType; use common::log::info; use common::PagingOptionsEnum; use crypto::privkey::key_pair_from_seed; +use crypto::HDPathToAccount; use itertools::Itertools; use keys::prefixes::*; use mm2_test_helpers::for_tests::mm_ctx_with_custom_db; use std::convert::TryFrom; use std::num::NonZeroUsize; +use std::str::FromStr; use std::time::Duration; pub(super) const TEST_COIN_NAME: &str = "DOC"; @@ -264,7 +267,7 @@ pub(super) async fn test_hd_utxo_tx_history_impl(rpc_client: ElectrumClient) { let hd_account_for_test = UtxoHDAccount { account_id: 0, extended_pubkey: Secp256k1ExtendedPublicKey::from_str("xpub6DEHSksajpRPM59RPw7Eg6PKdU7E2ehxJWtYdrfQ6JFmMGBsrR6jA78ANCLgzKYm4s5UqQ4ydLEYPbh3TRVvn5oAZVtWfi4qJLMntpZ8uGJ").unwrap(), - account_derivation_path: StandardHDPathToAccount::from_str("m/44'/141'/0'").unwrap(), + account_derivation_path: HDPathToAccount::from_str("m/44'/141'/0'").unwrap(), external_addresses_number: 10, internal_addresses_number: 0, derived_addresses: HDAddressesCache::default(), @@ -276,12 +279,15 @@ pub(super) async fn test_hd_utxo_tx_history_impl(rpc_client: ElectrumClient) { let mut fields = utxo_coin_fields_for_test(rpc_client.into(), None, false); fields.conf.ticker = "DOC".to_string(); fields.derivation_method = DerivationMethod::HDWallet(UtxoHDWallet { - hd_wallet_rmd160: "6d9d2b554d768232320587df75c4338ecc8bf37d".into(), - hd_wallet_storage: HDWalletCoinStorage::default(), + inner: HDWallet { + hd_wallet_rmd160: "6d9d2b554d768232320587df75c4338ecc8bf37d".into(), + hd_wallet_storage: HDWalletCoinStorage::default(), + derivation_path: HDPathToCoin::from_str("m/44'/141'").unwrap(), + accounts: HDAccountsMutex::new(hd_accounts), + enabled_address: HDPathAccountToAddressId::default(), + gap_limit: 20, + }, address_format: UtxoAddressFormat::Standard, - derivation_path: StandardHDPathToCoin::from_str("m/44'/141'").unwrap(), - accounts: HDAccountsMutex::new(hd_accounts), - gap_limit: 20, }); let coin = utxo_coin_from_fields(fields); @@ -305,7 +311,7 @@ pub(super) async fn test_hd_utxo_tx_history_impl(rpc_client: ElectrumClient) { // Activate new `RYM6yDMn8vdqtkYKLzY5dNe7p3T6YmMWvq` address. match coin.as_ref().derivation_method { DerivationMethod::HDWallet(ref hd_wallet) => { - let mut accounts = hd_wallet.accounts.lock().await; + let mut accounts = hd_wallet.inner.accounts.lock().await; accounts.get_mut(&0).unwrap().external_addresses_number += 1 }, _ => unimplemented!(), diff --git a/mm2src/coins/utxo/utxo_hd_wallet.rs b/mm2src/coins/utxo/utxo_hd_wallet.rs new file mode 100644 index 0000000000..a646041f2d --- /dev/null +++ b/mm2src/coins/utxo/utxo_hd_wallet.rs @@ -0,0 +1,58 @@ +use crate::hd_wallet::{HDAccount, HDAccountMut, HDAccountOps, HDAccountsMap, HDAccountsMut, HDAccountsMutex, + HDAddress, HDWallet, HDWalletCoinStorage, HDWalletOps, HDWalletStorageOps, + WithdrawSenderAddress}; +use async_trait::async_trait; +use crypto::{Bip44Chain, HDPathToCoin}; +use keys::{Address, AddressFormat as UtxoAddressFormat, Public}; + +pub type UtxoHDAddress = HDAddress; +pub type UtxoHDAccount = HDAccount; +pub type UtxoWithdrawSender = WithdrawSenderAddress; + +/// A struct to encapsulate the types needed for a UTXO HD wallet. +pub struct UtxoHDWallet { + /// The inner HD wallet field that makes use of the generic `HDWallet` struct. + pub inner: HDWallet, + /// Specifies the UTXO address format for all addresses in the wallet. + pub address_format: UtxoAddressFormat, +} + +#[async_trait] +impl HDWalletStorageOps for UtxoHDWallet { + fn hd_wallet_storage(&self) -> &HDWalletCoinStorage { self.inner.hd_wallet_storage() } +} + +#[async_trait] +impl HDWalletOps for UtxoHDWallet { + type HDAccount = UtxoHDAccount; + + fn coin_type(&self) -> u32 { self.inner.coin_type() } + + fn derivation_path(&self) -> &HDPathToCoin { self.inner.derivation_path() } + + fn gap_limit(&self) -> u32 { self.inner.gap_limit() } + + fn account_limit(&self) -> u32 { self.inner.account_limit() } + + fn default_receiver_chain(&self) -> Bip44Chain { self.inner.default_receiver_chain() } + + fn get_accounts_mutex(&self) -> &HDAccountsMutex { self.inner.get_accounts_mutex() } + + async fn get_account(&self, account_id: u32) -> Option { self.inner.get_account(account_id).await } + + async fn get_account_mut(&self, account_id: u32) -> Option> { + self.inner.get_account_mut(account_id).await + } + + async fn get_accounts(&self) -> HDAccountsMap { self.inner.get_accounts().await } + + async fn get_accounts_mut(&self) -> HDAccountsMut<'_, Self::HDAccount> { self.inner.get_accounts_mut().await } + + async fn remove_account_if_last(&self, account_id: u32) -> Option { + self.inner.remove_account_if_last(account_id).await + } + + async fn get_enabled_address(&self) -> Option<::HDAddress> { + self.inner.get_enabled_address().await + } +} diff --git a/mm2src/coins/utxo/utxo_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index 1453acc346..93c0535b74 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -1,13 +1,10 @@ use super::utxo_common::utxo_prepare_addresses_for_balance_stream_if_enabled; use super::*; use crate::coin_balance::{self, EnableCoinBalanceError, EnabledCoinBalanceParams, HDAccountBalance, HDAddressBalance, - HDWalletBalance, HDWalletBalanceOps}; + HDBalanceAddress, HDWalletBalance, HDWalletBalanceOps}; use crate::coin_errors::{MyAddressError, ValidatePaymentResult}; -use crate::hd_confirm_address::HDConfirmAddress; -use crate::hd_pubkey::{ExtractExtendedPubkey, HDExtractPubkeyError, HDXPubExtractor}; -use crate::hd_wallet::{AccountUpdatingError, AddressDerivingResult, HDAccountMut, NewAccountCreatingError, - NewAddressDeriveConfirmError}; -use crate::hd_wallet_storage::HDWalletCoinWithStorageOps; +use crate::hd_wallet::{ExtractExtendedPubkey, HDCoinAddress, HDCoinHDAccount, HDCoinHDAddress, HDCoinWithdrawOps, + HDConfirmAddress, HDExtractPubkeyError, HDXPubExtractor, TrezorCoinError, WithdrawSenderAddress}; use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHistoryTarget, TxHistoryStorage}; use crate::rpc_command::account_balance::{self, AccountBalanceParams, AccountBalanceRpcOps, HDAccountBalanceResponse}; use crate::rpc_command::get_new_address::{self, GetNewAddressParams, GetNewAddressResponse, GetNewAddressRpcError, @@ -24,25 +21,24 @@ use crate::utxo::rpc_clients::BlockHashOrHeight; use crate::utxo::utxo_builder::{UtxoArcBuilder, UtxoCoinBuilder}; use crate::utxo::utxo_tx_history_v2::{UtxoMyAddressesHistoryError, UtxoTxDetailsError, UtxoTxDetailsParams, UtxoTxHistoryOps}; -use crate::{CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinBalance, CoinWithDerivationMethod, ConfirmPaymentInput, - DexFee, FundingTxSpend, GenPreimageResult, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, - GetWithdrawSenderAddress, IguanaPrivKey, MakerCoinSwapOpsV2, MakerSwapTakerCoin, MmCoinEnum, - NegotiateSwapContractAddrErr, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, - PrivKeyBuildPolicy, RawTransactionRequest, RawTransactionResult, RefundError, RefundFundingSecretArgs, - RefundMakerPaymentArgs, RefundPaymentArgs, RefundResult, SearchForFundingSpendErr, - SearchForSwapTxSpendInput, SendMakerPaymentArgs, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, - SendTakerFundingArgs, SignRawTransactionRequest, SignatureResult, SpendMakerPaymentArgs, SpendPaymentArgs, - SwapOps, SwapTxTypeWithSecretHash, TakerCoinSwapOpsV2, TakerSwapMakerCoin, ToBytes, TradePreimageValue, - TransactionFut, TransactionResult, TxMarshalingErr, TxPreimageWithSig, ValidateAddressResult, - ValidateFeeArgs, ValidateInstructionsErr, ValidateMakerPaymentArgs, ValidateOtherPubKeyErr, - ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, ValidateSwapV2TxResult, - ValidateTakerFundingArgs, ValidateTakerFundingSpendPreimageResult, - ValidateTakerPaymentSpendPreimageResult, ValidateWatcherSpendInput, VerificationResult, - WaitForHTLCTxSpendArgs, WaitForTakerPaymentSpendError, WatcherOps, WatcherReward, WatcherRewardError, - WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut, - WithdrawSenderAddress}; +use crate::{CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinBalance, CoinWithDerivationMethod, CoinWithPrivKeyPolicy, + ConfirmPaymentInput, DexFee, FundingTxSpend, GenPreimageResult, GenTakerFundingSpendArgs, + GenTakerPaymentSpendArgs, GetWithdrawSenderAddress, IguanaBalanceOps, IguanaPrivKey, MakerCoinSwapOpsV2, + MakerSwapTakerCoin, MmCoinEnum, NegotiateSwapContractAddrErr, PaymentInstructionArgs, PaymentInstructions, + PaymentInstructionsErr, PrivKeyBuildPolicy, RawTransactionRequest, RawTransactionResult, RefundError, + RefundFundingSecretArgs, RefundMakerPaymentArgs, RefundPaymentArgs, RefundResult, + SearchForFundingSpendErr, SearchForSwapTxSpendInput, SendMakerPaymentArgs, + SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SendTakerFundingArgs, SignRawTransactionRequest, + SignatureResult, SpendMakerPaymentArgs, SpendPaymentArgs, SwapOps, SwapTxTypeWithSecretHash, + TakerCoinSwapOpsV2, TakerSwapMakerCoin, ToBytes, TradePreimageValue, TransactionFut, TransactionResult, + TxMarshalingErr, TxPreimageWithSig, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, + ValidateMakerPaymentArgs, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, + ValidatePaymentInput, ValidateSwapV2TxResult, ValidateTakerFundingArgs, + ValidateTakerFundingSpendPreimageResult, ValidateTakerPaymentSpendPreimageResult, + ValidateWatcherSpendInput, VerificationResult, WaitForHTLCTxSpendArgs, WaitForTakerPaymentSpendError, + WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, + WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut}; use common::executor::{AbortableSystem, AbortedError}; -use crypto::Bip44Chain; use futures::{FutureExt, TryFutureExt}; use mm2_metrics::MetricsArc; use mm2_number::MmNumber; @@ -320,13 +316,19 @@ impl SwapOps for UtxoStandardCoin { } #[inline] - fn send_maker_spends_taker_payment(&self, maker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { - utxo_common::send_maker_spends_taker_payment(self.clone(), maker_spends_payment_args) + async fn send_maker_spends_taker_payment( + &self, + maker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { + utxo_common::send_maker_spends_taker_payment(self.clone(), maker_spends_payment_args).await } #[inline] - fn send_taker_spends_maker_payment(&self, taker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { - utxo_common::send_taker_spends_maker_payment(self.clone(), taker_spends_payment_args) + async fn send_taker_spends_maker_payment( + &self, + taker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { + utxo_common::send_taker_spends_maker_payment(self.clone(), taker_spends_payment_args).await } #[inline] @@ -831,7 +833,7 @@ impl TakerCoinSwapOpsV2 for UtxoStandardCoin { impl MarketCoinOps for UtxoStandardCoin { fn ticker(&self) -> &str { &self.utxo_arc.conf.ticker } - fn get_public_key(&self) -> Result> { + async fn get_public_key(&self) -> Result> { let pubkey = utxo_common::my_public_key(&self.utxo_arc)?; Ok(pubkey.to_string()) } @@ -899,6 +901,8 @@ impl MarketCoinOps for UtxoStandardCoin { fn min_tx_amount(&self) -> BigDecimal { utxo_common::min_tx_amount(self.as_ref()) } fn min_trading_vol(&self) -> MmNumber { utxo_common::min_trading_vol(self.as_ref()) } + + fn is_trezor(&self) -> bool { self.as_ref().priv_key_policy.is_trezor() } } #[async_trait] @@ -1048,90 +1052,66 @@ impl UtxoSignerOps for UtxoStandardCoin { fn tx_provider(&self) -> Self::TxGetter { self.utxo_arc.rpc_client.clone() } } -impl CoinWithDerivationMethod for UtxoStandardCoin { - type Address = Address; - type HDWallet = UtxoHDWallet; +impl CoinWithPrivKeyPolicy for UtxoStandardCoin { + type KeyPair = KeyPair; - fn derivation_method(&self) -> &DerivationMethod { + fn priv_key_policy(&self) -> &PrivKeyPolicy { &self.utxo_arc.priv_key_policy } +} + +impl CoinWithDerivationMethod for UtxoStandardCoin { + fn derivation_method(&self) -> &DerivationMethod, Self::HDWallet> { utxo_common::derivation_method(self.as_ref()) } } +#[async_trait] +impl IguanaBalanceOps for UtxoStandardCoin { + type BalanceObject = CoinBalance; + + async fn iguana_balances(&self) -> BalanceResult { self.my_balance().compat().await } +} + #[async_trait] impl ExtractExtendedPubkey for UtxoStandardCoin { type ExtendedPublicKey = Secp256k1ExtendedPublicKey; async fn extract_extended_pubkey( &self, - xpub_extractor: &XPubExtractor, + xpub_extractor: Option, derivation_path: DerivationPath, ) -> MmResult where - XPubExtractor: HDXPubExtractor, + XPubExtractor: HDXPubExtractor + Send, { - utxo_common::extract_extended_pubkey(&self.utxo_arc.conf, xpub_extractor, derivation_path).await + crate::extract_extended_pubkey_impl(self, xpub_extractor, derivation_path).await } } #[async_trait] impl HDWalletCoinOps for UtxoStandardCoin { - type Address = Address; - type Pubkey = Public; type HDWallet = UtxoHDWallet; - type HDAccount = UtxoHDAccount; - - async fn derive_addresses( - &self, - hd_account: &Self::HDAccount, - address_ids: Ids, - ) -> AddressDerivingResult>> - where - Ids: Iterator + Send, - { - utxo_common::derive_addresses(self, hd_account, address_ids).await - } - - async fn generate_and_confirm_new_address( - &self, - hd_wallet: &Self::HDWallet, - hd_account: &mut Self::HDAccount, - chain: Bip44Chain, - confirm_address: &ConfirmAddress, - ) -> MmResult, NewAddressDeriveConfirmError> - where - ConfirmAddress: HDConfirmAddress, - { - utxo_common::generate_and_confirm_new_address(self, hd_wallet, hd_account, chain, confirm_address).await - } - async fn create_new_account<'a, XPubExtractor>( + fn address_from_extended_pubkey( &self, - hd_wallet: &'a Self::HDWallet, - xpub_extractor: &XPubExtractor, - ) -> MmResult, NewAccountCreatingError> - where - XPubExtractor: HDXPubExtractor, - { - utxo_common::create_new_account(self, hd_wallet, xpub_extractor).await + extended_pubkey: &Secp256k1ExtendedPublicKey, + derivation_path: DerivationPath, + ) -> HDCoinHDAddress { + utxo_common::address_from_extended_pubkey(self, extended_pubkey, derivation_path) } - async fn set_known_addresses_number( - &self, - hd_wallet: &Self::HDWallet, - hd_account: &mut Self::HDAccount, - chain: Bip44Chain, - new_known_addresses_number: u32, - ) -> MmResult<(), AccountUpdatingError> { - utxo_common::set_known_addresses_number(self, hd_wallet, hd_account, chain, new_known_addresses_number).await - } + fn trezor_coin(&self) -> MmResult { utxo_common::trezor_coin(self) } } +impl HDCoinWithdrawOps for UtxoStandardCoin {} + #[async_trait] impl GetNewAddressRpcOps for UtxoStandardCoin { + type BalanceObject = CoinBalance; + async fn get_new_address_rpc_without_conf( &self, params: GetNewAddressParams, - ) -> MmResult { + ) -> MmResult, GetNewAddressRpcError> { get_new_address::common_impl::get_new_address_rpc_without_conf(self, params).await } @@ -1139,7 +1119,7 @@ impl GetNewAddressRpcOps for UtxoStandardCoin { &self, params: GetNewAddressParams, confirm_address: &ConfirmAddress, - ) -> MmResult + ) -> MmResult, GetNewAddressRpcError> where ConfirmAddress: HDConfirmAddress, { @@ -1150,6 +1130,7 @@ impl GetNewAddressRpcOps for UtxoStandardCoin { #[async_trait] impl HDWalletBalanceOps for UtxoStandardCoin { type HDAddressScanner = UtxoAddressScanner; + type BalanceObject = CoinBalance; async fn produce_hd_address_scanner(&self) -> BalanceResult { utxo_common::produce_hd_address_scanner(self).await @@ -1158,94 +1139,100 @@ impl HDWalletBalanceOps for UtxoStandardCoin { async fn enable_hd_wallet( &self, hd_wallet: &Self::HDWallet, - xpub_extractor: &XPubExtractor, + xpub_extractor: Option, params: EnabledCoinBalanceParams, - ) -> MmResult + path_to_address: &HDPathAccountToAddressId, + ) -> MmResult, EnableCoinBalanceError> where - XPubExtractor: HDXPubExtractor, + XPubExtractor: HDXPubExtractor + Send, { - coin_balance::common_impl::enable_hd_wallet(self, hd_wallet, xpub_extractor, params).await + coin_balance::common_impl::enable_hd_wallet(self, hd_wallet, xpub_extractor, params, path_to_address).await } async fn scan_for_new_addresses( &self, hd_wallet: &Self::HDWallet, - hd_account: &mut Self::HDAccount, + hd_account: &mut HDCoinHDAccount, address_scanner: &Self::HDAddressScanner, gap_limit: u32, - ) -> BalanceResult> { + ) -> BalanceResult>> { utxo_common::scan_for_new_addresses(self, hd_wallet, hd_account, address_scanner, gap_limit).await } - async fn all_known_addresses_balances(&self, hd_account: &Self::HDAccount) -> BalanceResult> { + async fn all_known_addresses_balances( + &self, + hd_account: &HDCoinHDAccount, + ) -> BalanceResult>> { utxo_common::all_known_addresses_balances(self, hd_account).await } - async fn known_address_balance(&self, address: &Self::Address) -> BalanceResult { + async fn known_address_balance(&self, address: &HDBalanceAddress) -> BalanceResult { utxo_common::address_balance(self, address).await } async fn known_addresses_balances( &self, - addresses: Vec, - ) -> BalanceResult> { + addresses: Vec>, + ) -> BalanceResult, Self::BalanceObject)>> { utxo_common::addresses_balances(self, addresses).await } async fn prepare_addresses_for_balance_stream_if_enabled( &self, - addresses: HashSet, + addresses: HashSet, ) -> MmResult<(), String> { utxo_prepare_addresses_for_balance_stream_if_enabled(self, addresses).await } } -impl HDWalletCoinWithStorageOps for UtxoStandardCoin { - fn hd_wallet_storage<'a>(&self, hd_wallet: &'a Self::HDWallet) -> &'a HDWalletCoinStorage { - &hd_wallet.hd_wallet_storage - } -} - #[async_trait] impl AccountBalanceRpcOps for UtxoStandardCoin { + type BalanceObject = CoinBalance; + async fn account_balance_rpc( &self, params: AccountBalanceParams, - ) -> MmResult { + ) -> MmResult, HDAccountBalanceRpcError> { account_balance::common_impl::account_balance_rpc(self, params).await } } #[async_trait] impl InitAccountBalanceRpcOps for UtxoStandardCoin { + type BalanceObject = CoinBalance; + async fn init_account_balance_rpc( &self, params: InitAccountBalanceParams, - ) -> MmResult { + ) -> MmResult, HDAccountBalanceRpcError> { init_account_balance::common_impl::init_account_balance_rpc(self, params).await } } #[async_trait] impl InitScanAddressesRpcOps for UtxoStandardCoin { + type BalanceObject = CoinBalance; + async fn init_scan_for_new_addresses_rpc( &self, params: ScanAddressesParams, - ) -> MmResult { + ) -> MmResult, HDAccountBalanceRpcError> { init_scan_for_new_addresses::common_impl::scan_for_new_addresses_rpc(self, params).await } } #[async_trait] impl InitCreateAccountRpcOps for UtxoStandardCoin { + type BalanceObject = CoinBalance; + async fn init_create_account_rpc( &self, params: CreateNewAccountParams, state: CreateAccountState, - xpub_extractor: &XPubExtractor, - ) -> MmResult + xpub_extractor: Option, + ) -> MmResult, CreateAccountRpcError> where - XPubExtractor: HDXPubExtractor, + XPubExtractor: HDXPubExtractor + Send, { init_create_account::common_impl::init_create_new_account_rpc(self, params, state, xpub_extractor).await } @@ -1270,7 +1257,8 @@ impl CoinWithTxHistoryV2 for UtxoStandardCoin { #[async_trait] impl UtxoTxHistoryOps for UtxoStandardCoin { async fn my_addresses(&self) -> MmResult, UtxoMyAddressesHistoryError> { - utxo_common::utxo_tx_history_v2_common::my_addresses(self).await + let addresses = self.all_addresses().await?; + Ok(addresses) } async fn tx_details_by_hash( diff --git a/mm2src/coins/utxo/utxo_tests.rs b/mm2src/coins/utxo/utxo_tests.rs index 7cbc37ad91..a358d224aa 100644 --- a/mm2src/coins/utxo/utxo_tests.rs +++ b/mm2src/coins/utxo/utxo_tests.rs @@ -1,10 +1,9 @@ use super::*; use crate::coin_balance::HDAddressBalance; use crate::coin_errors::ValidatePaymentError; -use crate::hd_confirm_address::for_tests::MockableConfirmAddress; -use crate::hd_confirm_address::{HDConfirmAddress, HDConfirmAddressError}; -use crate::hd_wallet::HDAccountsMap; -use crate::hd_wallet_storage::{HDWalletMockStorage, HDWalletStorageInternalOps}; +use crate::hd_wallet::{HDAccountsMap, HDAccountsMutex, HDAddressesCache, HDConfirmAddress, HDConfirmAddressError, + HDWallet, HDWalletCoinStorage, HDWalletMockStorage, HDWalletStorageInternalOps, + MockableConfirmAddress}; use crate::my_tx_history_v2::for_tests::init_storage_for; use crate::my_tx_history_v2::CoinWithTxHistoryV2; use crate::rpc_command::account_balance::{AccountBalanceParams, AccountBalanceRpcOps, HDAccountBalanceResponse}; @@ -25,6 +24,7 @@ use crate::utxo::utxo_common::UtxoTxBuilder; #[cfg(not(target_arch = "wasm32"))] use crate::utxo::utxo_common_tests::TEST_COIN_DECIMALS; use crate::utxo::utxo_common_tests::{self, utxo_coin_fields_for_test, utxo_coin_from_fields, TEST_COIN_NAME}; +use crate::utxo::utxo_hd_wallet::UtxoHDAccount; use crate::utxo::utxo_standard::{utxo_standard_coin_with_priv_key, UtxoStandardCoin}; use crate::utxo::utxo_tx_history_v2::{UtxoTxDetailsParams, UtxoTxHistoryOps}; use crate::{BlockHeightAndTime, CoinBalance, ConfirmPaymentInput, DexFee, IguanaPrivKey, PrivKeyBuildPolicy, @@ -35,7 +35,7 @@ use crate::{WaitForHTLCTxSpendArgs, WithdrawFee}; use chain::{BlockHeader, BlockHeaderBits, OutPoint}; use common::executor::Timer; use common::{block_on, wait_until_sec, OrdRange, PagingOptionsEnum, DEX_FEE_ADDR_RAW_PUBKEY}; -use crypto::{privkey::key_pair_from_seed, Bip44Chain, RpcDerivationPath, Secp256k1Secret}; +use crypto::{privkey::key_pair_from_seed, Bip44Chain, HDPathToAccount, RpcDerivationPath, Secp256k1Secret}; #[cfg(not(target_arch = "wasm32"))] use db_common::sqlite::rusqlite::Connection; use futures::channel::mpsc::channel; @@ -56,6 +56,7 @@ use spv_validation::work::DifficultyAlgorithm; #[cfg(not(target_arch = "wasm32"))] use std::convert::TryFrom; use std::iter; use std::num::NonZeroUsize; +use std::str::FromStr; #[cfg(not(target_arch = "wasm32"))] const TAKER_PAYMENT_SPEND_SEARCH_INTERVAL: f64 = 1.; @@ -173,9 +174,7 @@ fn test_send_maker_spends_taker_payment_recoverable_tx() { swap_unique_data: &[], watcher_reward: false, }; - let tx_err = coin - .send_maker_spends_taker_payment(maker_spends_payment_args) - .wait() + let tx_err = block_on(coin.send_maker_spends_taker_payment(maker_spends_payment_args)) .expect_err("!send_maker_spends_taker_payment should error missing tx inputs"); // The error variant should be `TxRecoverable` @@ -197,7 +196,7 @@ fn test_generate_transaction() { value: 999, }]; - let builder = UtxoTxBuilder::new(&coin) + let builder = block_on(UtxoTxBuilder::new(&coin)) .add_available_inputs(unspents) .add_outputs(outputs); let generated = block_on(builder.build()); @@ -215,7 +214,7 @@ fn test_generate_transaction() { value: 98001, }]; - let builder = UtxoTxBuilder::new(&coin) + let builder = block_on(UtxoTxBuilder::new(&coin)) .add_available_inputs(unspents) .add_outputs(outputs); let generated = block_on(builder.build()).unwrap(); @@ -235,12 +234,13 @@ fn test_generate_transaction() { }]; let outputs = vec![TransactionOutput { - script_pubkey: Builder::build_p2pkh(coin.as_ref().derivation_method.unwrap_single_addr().hash()).to_bytes(), + script_pubkey: Builder::build_p2pkh(block_on(coin.as_ref().derivation_method.unwrap_single_addr()).hash()) + .to_bytes(), value: 100000, }]; // test that fee is properly deducted from output amount equal to input amount (max withdraw case) - let builder = UtxoTxBuilder::new(&coin) + let builder = block_on(UtxoTxBuilder::new(&coin)) .add_available_inputs(unspents) .add_outputs(outputs) .with_fee_policy(FeePolicy::DeductFromOutput(0)); @@ -266,7 +266,7 @@ fn test_generate_transaction() { }]; // test that generate_transaction returns an error when input amount is not sufficient to cover output + fee - let builder = UtxoTxBuilder::new(&coin) + let builder = block_on(UtxoTxBuilder::new(&coin)) .add_available_inputs(unspents) .add_outputs(outputs); @@ -964,7 +964,8 @@ fn test_utxo_lock() { let coin = utxo_coin_for_test(client.into(), None, false); let output = TransactionOutput { value: 1000000, - script_pubkey: Builder::build_p2pkh(coin.as_ref().derivation_method.unwrap_single_addr().hash()).to_bytes(), + script_pubkey: Builder::build_p2pkh(block_on(coin.as_ref().derivation_method.unwrap_single_addr()).hash()) + .to_bytes(), }; let mut futures = vec![]; for _ in 0..5 { @@ -1149,7 +1150,7 @@ fn test_generate_transaction_relay_fee_is_used_when_dynamic_fee_is_lower() { value: 900000000, }]; - let builder = UtxoTxBuilder::new(&coin) + let builder = block_on(UtxoTxBuilder::new(&coin)) .add_available_inputs(unspents) .add_outputs(outputs) .with_fee(ActualTxFee::Dynamic(100)); @@ -1191,7 +1192,7 @@ fn test_generate_transaction_relay_fee_is_used_when_dynamic_fee_is_lower_and_ded value: 1000000000, }]; - let tx_builder = UtxoTxBuilder::new(&coin) + let tx_builder = block_on(UtxoTxBuilder::new(&coin)) .add_available_inputs(unspents) .add_outputs(outputs) .with_fee_policy(FeePolicy::DeductFromOutput(0)) @@ -1239,7 +1240,7 @@ fn test_generate_tx_fee_is_correct_when_dynamic_fee_is_larger_than_relay() { value: 19000000000, }]; - let builder = UtxoTxBuilder::new(&coin) + let builder = block_on(UtxoTxBuilder::new(&coin)) .add_available_inputs(unspents) .add_outputs(outputs) .with_fee(ActualTxFee::Dynamic(1000)); @@ -1540,7 +1541,8 @@ fn test_spam_rick() { let output = TransactionOutput { value: 1000000, - script_pubkey: Builder::build_p2pkh(coin.as_ref().derivation_method.unwrap_single_addr().hash()).to_bytes(), + script_pubkey: Builder::build_p2pkh(block_on(coin.as_ref().derivation_method.unwrap_single_addr()).hash()) + .to_bytes(), }; let mut futures = vec![]; for _ in 0..5 { @@ -2732,7 +2734,7 @@ fn test_generate_tx_doge_fee() { value: 100000000, script_pubkey: vec![0; 26].into(), }]; - let builder = UtxoTxBuilder::new(&doge) + let builder = block_on(UtxoTxBuilder::new(&doge)) .add_available_inputs(unspents) .add_outputs(outputs); let (_, data) = block_on(builder.build()).unwrap(); @@ -2752,7 +2754,7 @@ fn test_generate_tx_doge_fee() { 40 ]; - let builder = UtxoTxBuilder::new(&doge) + let builder = block_on(UtxoTxBuilder::new(&doge)) .add_available_inputs(unspents) .add_outputs(outputs); let (_, data) = block_on(builder.build()).unwrap(); @@ -2772,7 +2774,7 @@ fn test_generate_tx_doge_fee() { 60 ]; - let builder = UtxoTxBuilder::new(&doge) + let builder = block_on(UtxoTxBuilder::new(&doge)) .add_available_inputs(unspents) .add_outputs(outputs); let (_, data) = block_on(builder.build()).unwrap(); @@ -3084,8 +3086,10 @@ fn test_withdraw_to_p2pkh() { // Create a p2pkh address for the test coin let p2pkh_address = AddressBuilder::new( UtxoAddressFormat::Standard, - coin.as_ref().derivation_method.unwrap_single_addr().hash().clone(), - *coin.as_ref().derivation_method.unwrap_single_addr().checksum_type(), + block_on(coin.as_ref().derivation_method.unwrap_single_addr()) + .hash() + .clone(), + *block_on(coin.as_ref().derivation_method.unwrap_single_addr()).checksum_type(), coin.as_ref().conf.address_prefixes.clone(), coin.as_ref().conf.bech32_hrp.clone(), ) @@ -3134,8 +3138,10 @@ fn test_withdraw_to_p2sh() { // Create a p2sh address for the test coin let p2sh_address = AddressBuilder::new( UtxoAddressFormat::Standard, - coin.as_ref().derivation_method.unwrap_single_addr().hash().clone(), - *coin.as_ref().derivation_method.unwrap_single_addr().checksum_type(), + block_on(coin.as_ref().derivation_method.unwrap_single_addr()) + .hash() + .clone(), + *block_on(coin.as_ref().derivation_method.unwrap_single_addr()).checksum_type(), coin.as_ref().conf.address_prefixes.clone(), coin.as_ref().conf.bech32_hrp.clone(), ) @@ -3184,8 +3190,10 @@ fn test_withdraw_to_p2wpkh() { // Create a p2wpkh address for the test coin let p2wpkh_address = AddressBuilder::new( UtxoAddressFormat::Segwit, - coin.as_ref().derivation_method.unwrap_single_addr().hash().clone(), - *coin.as_ref().derivation_method.unwrap_single_addr().checksum_type(), + block_on(coin.as_ref().derivation_method.unwrap_single_addr()) + .hash() + .clone(), + *block_on(coin.as_ref().derivation_method.unwrap_single_addr()).checksum_type(), NetworkAddressPrefixes::default(), coin.as_ref().conf.bech32_hrp.clone(), ) @@ -3354,10 +3362,10 @@ fn test_split_qtum() { let ctx = MmCtxBuilder::new().into_mm_arc(); let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); let coin = block_on(qtum_coin_with_priv_key(&ctx, "QTUM", &conf, ¶ms, priv_key)).unwrap(); - let p2pkh_address = coin.as_ref().derivation_method.unwrap_single_addr(); - let script: Script = output_script(p2pkh_address).expect("valid previous script must be built"); + let p2pkh_address = block_on(coin.as_ref().derivation_method.unwrap_single_addr()); + let script: Script = output_script(&p2pkh_address).expect("valid previous script must be built"); let key_pair = coin.as_ref().priv_key_policy.activated_key_or_err().unwrap(); - let (unspents, _) = block_on(coin.get_mature_unspent_ordered_list(p2pkh_address)).expect("Unspent list is empty"); + let (unspents, _) = block_on(coin.get_mature_unspent_ordered_list(&p2pkh_address)).expect("Unspent list is empty"); log!("Mature unspents vec = {:?}", unspents.mature); let outputs = vec![ TransactionOutput { @@ -3366,7 +3374,7 @@ fn test_split_qtum() { }; 40 ]; - let builder = UtxoTxBuilder::new(&coin) + let builder = block_on(UtxoTxBuilder::new(&coin)) .add_available_inputs(unspents.mature) .add_outputs(outputs); let (unsigned, data) = block_on(builder.build()).unwrap(); @@ -3377,7 +3385,7 @@ fn test_split_qtum() { UtxoAddressFormat::Segwit => SignatureVersion::WitnessV0, _ => coin.as_ref().conf.signature_version, }; - let prev_script = output_script(p2pkh_address).expect("valid previous script must be built"); + let prev_script = output_script(&p2pkh_address).expect("valid previous script must be built"); let signed = sign_tx( unsigned, key_pair, @@ -3440,7 +3448,7 @@ fn test_qtum_with_check_utxo_maturity_false() { #[test] fn test_account_balance_rpc() { let mut addresses_map: HashMap = HashMap::new(); - let mut balances_by_der_path: HashMap = HashMap::new(); + let mut balances_by_der_path: HashMap> = HashMap::new(); macro_rules! known_address { ($der_path:literal, $address:literal, $chain:expr, balance = $balance:literal) => { @@ -3500,7 +3508,7 @@ fn test_account_balance_rpc() { hd_accounts.insert(0, UtxoHDAccount { account_id: 0, extended_pubkey: Secp256k1ExtendedPublicKey::from_str("xpub6DEHSksajpRPM59RPw7Eg6PKdU7E2ehxJWtYdrfQ6JFmMGBsrR6jA78ANCLgzKYm4s5UqQ4ydLEYPbh3TRVvn5oAZVtWfi4qJLMntpZ8uGJ").unwrap(), - account_derivation_path: StandardHDPathToAccount::from_str("m/44'/141'/0'").unwrap(), + account_derivation_path: HDPathToAccount::from_str("m/44'/141'/0'").unwrap(), external_addresses_number: 7, internal_addresses_number: 3, derived_addresses: HDAddressesCache::default(), @@ -3508,18 +3516,21 @@ fn test_account_balance_rpc() { hd_accounts.insert(1, UtxoHDAccount { account_id: 1, extended_pubkey: Secp256k1ExtendedPublicKey::from_str("xpub6DEHSksajpRPQq2FdGT6JoieiQZUpTZ3WZn8fcuLJhFVmtCpXbuXxp5aPzaokwcLV2V9LE55Dwt8JYkpuMv7jXKwmyD28WbHYjBH2zhbW2p").unwrap(), - account_derivation_path: StandardHDPathToAccount::from_str("m/44'/141'/1'").unwrap(), + account_derivation_path: HDPathToAccount::from_str("m/44'/141'/1'").unwrap(), external_addresses_number: 0, internal_addresses_number: 1, derived_addresses: HDAddressesCache::default(), }); fields.derivation_method = DerivationMethod::HDWallet(UtxoHDWallet { - hd_wallet_rmd160: "21605444b36ec72780bdf52a5ffbc18288893664".into(), - hd_wallet_storage: HDWalletCoinStorage::default(), + inner: HDWallet { + hd_wallet_rmd160: "21605444b36ec72780bdf52a5ffbc18288893664".into(), + hd_wallet_storage: HDWalletCoinStorage::default(), + derivation_path: HDPathToCoin::from_str("m/44'/141'").unwrap(), + accounts: HDAccountsMutex::new(hd_accounts), + enabled_address: HDPathAccountToAddressId::default(), + gap_limit: 3, + }, address_format: UtxoAddressFormat::Standard, - derivation_path: StandardHDPathToCoin::from_str("m/44'/141'").unwrap(), - accounts: HDAccountsMutex::new(hd_accounts), - gap_limit: 3, }); let coin = utxo_coin_from_fields(fields); @@ -3732,7 +3743,7 @@ fn test_scan_for_new_addresses() { // The list of addresses with a non-empty transaction history. let mut non_empty_addresses: HashSet = HashSet::new(); // The map of results by the addresses. - let mut balances_by_der_path: HashMap = HashMap::new(); + let mut balances_by_der_path: HashMap> = HashMap::new(); macro_rules! new_address { ($der_path:literal, $address:literal, $chain:expr, balance = $balance:expr) => {{ @@ -3829,7 +3840,7 @@ fn test_scan_for_new_addresses() { hd_accounts.insert(0, UtxoHDAccount { account_id: 0, extended_pubkey: Secp256k1ExtendedPublicKey::from_str("xpub6DEHSksajpRPM59RPw7Eg6PKdU7E2ehxJWtYdrfQ6JFmMGBsrR6jA78ANCLgzKYm4s5UqQ4ydLEYPbh3TRVvn5oAZVtWfi4qJLMntpZ8uGJ").unwrap(), - account_derivation_path: StandardHDPathToAccount::from_str("m/44'/141'/0'").unwrap(), + account_derivation_path: HDPathToAccount::from_str("m/44'/141'/0'").unwrap(), external_addresses_number: 3, internal_addresses_number: 1, derived_addresses: HDAddressesCache::default(), @@ -3837,18 +3848,21 @@ fn test_scan_for_new_addresses() { hd_accounts.insert(1, UtxoHDAccount { account_id: 1, extended_pubkey: Secp256k1ExtendedPublicKey::from_str("xpub6DEHSksajpRPQq2FdGT6JoieiQZUpTZ3WZn8fcuLJhFVmtCpXbuXxp5aPzaokwcLV2V9LE55Dwt8JYkpuMv7jXKwmyD28WbHYjBH2zhbW2p").unwrap(), - account_derivation_path: StandardHDPathToAccount::from_str("m/44'/141'/1'").unwrap(), + account_derivation_path: HDPathToAccount::from_str("m/44'/141'/1'").unwrap(), external_addresses_number: 0, internal_addresses_number: 2, derived_addresses: HDAddressesCache::default(), }); fields.derivation_method = DerivationMethod::HDWallet(UtxoHDWallet { - hd_wallet_rmd160: "21605444b36ec72780bdf52a5ffbc18288893664".into(), - hd_wallet_storage: HDWalletCoinStorage::default(), + inner: HDWallet { + hd_wallet_rmd160: "21605444b36ec72780bdf52a5ffbc18288893664".into(), + hd_wallet_storage: HDWalletCoinStorage::default(), + derivation_path: HDPathToCoin::from_str("m/44'/141'").unwrap(), + accounts: HDAccountsMutex::new(hd_accounts), + enabled_address: HDPathAccountToAddressId::default(), + gap_limit: 3, + }, address_format: UtxoAddressFormat::Standard, - derivation_path: StandardHDPathToCoin::from_str("m/44'/141'").unwrap(), - accounts: HDAccountsMutex::new(hd_accounts), - gap_limit: 3, }); let coin = utxo_coin_from_fields(fields); @@ -3904,7 +3918,7 @@ fn test_scan_for_new_addresses() { assert_eq!(actual, expected); let accounts = match coin.as_ref().derivation_method { - DerivationMethod::HDWallet(UtxoHDWallet { ref accounts, .. }) => block_on(accounts.lock()).clone(), + DerivationMethod::HDWallet(UtxoHDWallet { ref inner, .. }) => block_on(inner.accounts.lock()).clone(), _ => unreachable!(), }; assert_eq!(accounts[&0].external_addresses_number, 4); @@ -3955,7 +3969,7 @@ fn test_get_new_address() { } }); - MockableConfirmAddress::confirm_utxo_address + MockableConfirmAddress::confirm_address .mock_safe(move |_, _, _, _| MockResult::Return(Box::pin(futures::future::ok(())))); // This mock is required just not to fail on [`UtxoAddressScanner::init`]. @@ -3970,7 +3984,7 @@ fn test_get_new_address() { let hd_account_for_test = UtxoHDAccount { account_id: 0, extended_pubkey: Secp256k1ExtendedPublicKey::from_str("xpub6DEHSksajpRPM59RPw7Eg6PKdU7E2ehxJWtYdrfQ6JFmMGBsrR6jA78ANCLgzKYm4s5UqQ4ydLEYPbh3TRVvn5oAZVtWfi4qJLMntpZ8uGJ").unwrap(), - account_derivation_path: StandardHDPathToAccount::from_str("m/44'/141'/0'").unwrap(), + account_derivation_path: HDPathToAccount::from_str("m/44'/141'/0'").unwrap(), external_addresses_number: 4, internal_addresses_number: 0, derived_addresses: HDAddressesCache::default(), @@ -3982,12 +3996,15 @@ fn test_get_new_address() { hd_accounts.insert(2, hd_account_for_test); fields.derivation_method = DerivationMethod::HDWallet(UtxoHDWallet { - hd_wallet_rmd160: "21605444b36ec72780bdf52a5ffbc18288893664".into(), - hd_wallet_storage: HDWalletCoinStorage::default(), + inner: HDWallet { + hd_wallet_rmd160: "21605444b36ec72780bdf52a5ffbc18288893664".into(), + hd_wallet_storage: HDWalletCoinStorage::default(), + derivation_path: HDPathToCoin::from_str("m/44'/141'").unwrap(), + accounts: HDAccountsMutex::new(hd_accounts), + enabled_address: HDPathAccountToAddressId::default(), + gap_limit: 2, + }, address_format: UtxoAddressFormat::Standard, - derivation_path: StandardHDPathToCoin::from_str("m/44'/141'").unwrap(), - accounts: HDAccountsMutex::new(hd_accounts), - gap_limit: 2, }); fields.conf.trezor_coin = Some("Komodo".to_string()); let coin = utxo_coin_from_fields(fields); @@ -4093,9 +4110,9 @@ fn test_get_new_address() { block_on(coin.get_new_address_rpc(params, &confirm_address)).unwrap(); unsafe { assert_eq!(CHECKED_ADDRESSES, EXPECTED_CHECKED_ADDRESSES) }; - // Check if `get_new_address_rpc` fails on the `HDAddressConfirm::confirm_utxo_address` error. + // Check if `get_new_address_rpc` fails on the `HDAddressConfirm::confirm_address` error. - MockableConfirmAddress::confirm_utxo_address.mock_safe(move |_, _, _, _| { + MockableConfirmAddress::confirm_address.mock_safe(move |_, _, _, _| { MockResult::Return(Box::pin(futures::future::ready(MmError::err( HDConfirmAddressError::HwContextNotInitialized, )))) diff --git a/mm2src/coins/utxo/utxo_tx_history_v2.rs b/mm2src/coins/utxo/utxo_tx_history_v2.rs index 29861a23dc..6cbd2877b8 100644 --- a/mm2src/coins/utxo/utxo_tx_history_v2.rs +++ b/mm2src/coins/utxo/utxo_tx_history_v2.rs @@ -5,8 +5,9 @@ use crate::tx_history_storage::FilteringAddresses; use crate::utxo::bch::BchCoin; use crate::utxo::slp::ParseSlpScriptError; use crate::utxo::{utxo_common, AddrFromStrError, GetBlockHeaderError}; -use crate::{BalanceError, BalanceResult, BlockHeightAndTime, HistorySyncState, MarketCoinOps, NumConversError, - ParseBigDecimalError, TransactionDetails, UnexpectedDerivationMethod, UtxoRpcError, UtxoTx}; +use crate::{BalanceError, BalanceResult, BlockHeightAndTime, CoinWithDerivationMethod, HistorySyncState, + MarketCoinOps, NumConversError, ParseBigDecimalError, TransactionDetails, UnexpectedDerivationMethod, + UtxoRpcError, UtxoTx}; use async_trait::async_trait; use common::executor::Timer; use common::log::{error, info}; @@ -102,7 +103,9 @@ pub struct UtxoTxDetailsParams<'a, Storage> { } #[async_trait] -pub trait UtxoTxHistoryOps: CoinWithTxHistoryV2 + MarketCoinOps + Send + Sync + 'static { +pub trait UtxoTxHistoryOps: + CoinWithTxHistoryV2 + CoinWithDerivationMethod + MarketCoinOps + Send + Sync + 'static +{ /// Returns addresses for those we need to request Transaction history. async fn my_addresses(&self) -> MmResult, UtxoMyAddressesHistoryError>; diff --git a/mm2src/coins/utxo/utxo_withdraw.rs b/mm2src/coins/utxo/utxo_withdraw.rs index 795da12006..41350a1be2 100644 --- a/mm2src/coins/utxo/utxo_withdraw.rs +++ b/mm2src/coins/utxo/utxo_withdraw.rs @@ -1,10 +1,9 @@ use crate::rpc_command::init_withdraw::{WithdrawInProgressStatus, WithdrawTaskHandleShared}; use crate::utxo::utxo_common::{big_decimal_from_sat, UtxoTxBuilder}; -use crate::utxo::{output_script, sat_from_big_decimal, ActualTxFee, Address, AddressBuilder, FeePolicy, - GetUtxoListOps, PrivKeyPolicy, UtxoAddressFormat, UtxoCoinFields, UtxoCommonOps, UtxoFeeDetails, - UtxoTx, UTXO_LOCK}; +use crate::utxo::{output_script, sat_from_big_decimal, ActualTxFee, Address, FeePolicy, GetUtxoListOps, PrivKeyPolicy, + UtxoAddressFormat, UtxoCoinFields, UtxoCommonOps, UtxoFeeDetails, UtxoTx, UTXO_LOCK}; use crate::{CoinWithDerivationMethod, GetWithdrawSenderAddress, MarketCoinOps, TransactionDetails, WithdrawError, - WithdrawFee, WithdrawFrom, WithdrawRequest, WithdrawResult}; + WithdrawFee, WithdrawRequest, WithdrawResult}; use async_trait::async_trait; use chain::TransactionOutput; use common::log::info; @@ -13,7 +12,7 @@ use crypto::hw_rpc_task::HwRpcTaskAwaitingStatus; use crypto::trezor::trezor_rpc_task::{TrezorRequestStatuses, TrezorRpcTaskProcessor}; use crypto::trezor::{TrezorError, TrezorProcessingError}; use crypto::{from_hw_error, CryptoCtx, CryptoCtxError, DerivationPath, HwError, HwProcessingError, HwRpcError}; -use keys::{AddressFormat, AddressHashEnum, KeyPair, Private, Public as PublicKey}; +use keys::{AddressFormat, KeyPair, Private, Public as PublicKey}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use rpc::v1::types::ToTxHash; @@ -163,6 +162,7 @@ where let outputs = vec![TransactionOutput { value, script_pubkey }]; let mut tx_builder = UtxoTxBuilder::new(coin) + .await .with_from_address(self.sender_address()) .add_available_inputs(unspents) .add_outputs(outputs) @@ -408,8 +408,8 @@ pub struct StandardUtxoWithdraw { coin: Coin, req: WithdrawRequest, key_pair: KeyPair, - my_address: Address, - my_address_string: String, + from_address: Address, + from_address_string: String, } #[async_trait] @@ -419,9 +419,9 @@ where { fn coin(&self) -> &Coin { &self.coin } - fn sender_address(&self) -> Address { self.my_address.clone() } + fn sender_address(&self) -> Address { self.from_address.clone() } - fn sender_address_string(&self) -> String { self.my_address_string.clone() } + fn sender_address_string(&self) -> String { self.from_address_string.clone() } fn request(&self) -> &WithdrawRequest { &self.req } @@ -442,61 +442,44 @@ where impl StandardUtxoWithdraw where - Coin: AsRef + MarketCoinOps, + Coin: AsRef + + MarketCoinOps + + CoinWithDerivationMethod + + GetWithdrawSenderAddress
, { #[allow(clippy::result_large_err)] - pub fn new(coin: Coin, req: WithdrawRequest) -> Result> { - let (key_pair, my_address) = match req.from { - Some(WithdrawFrom::HDWalletAddress(ref path_to_address)) => { + pub async fn new(coin: Coin, req: WithdrawRequest) -> Result> { + let from = coin.get_withdraw_sender_address(&req).await?; + let from_address_string = from.address.display_address().map_to_mm(WithdrawError::InternalError)?; + + let key_pair = match from.derivation_path { + Some(der_path) => { let secret = coin .as_ref() .priv_key_policy - .hd_wallet_derived_priv_key_or_err(path_to_address)?; + .hd_wallet_derived_priv_key_or_err(&der_path)?; let private = Private { prefix: coin.as_ref().conf.wif_prefix, secret, compressed: true, checksum_type: coin.as_ref().conf.checksum_type, }; - let key_pair = - KeyPair::from_private(private).map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - let addr_format = coin - .as_ref() - .derivation_method - .single_addr_or_err()? - .clone() - .addr_format() - .clone(); - let my_address = AddressBuilder::new( - addr_format, - AddressHashEnum::AddressHash(key_pair.public().address_hash()), - coin.as_ref().conf.checksum_type, - coin.as_ref().conf.address_prefixes.clone(), - coin.as_ref().conf.bech32_hrp.clone(), - ) - .as_pkh() - .build() - .map_to_mm(WithdrawError::InternalError)?; - (key_pair, my_address) + KeyPair::from_private(private).map_to_mm(|e| WithdrawError::InternalError(e.to_string()))? }, - Some(WithdrawFrom::AddressId(_)) | Some(WithdrawFrom::DerivationPath { .. }) => { - return MmError::err(WithdrawError::UnsupportedError( - "Only `WithdrawFrom::HDWalletAddress` is supported for `StandardUtxoWithdraw`".to_string(), - )) - }, - None => { - let key_pair = coin.as_ref().priv_key_policy.activated_key_or_err()?; - let my_address = coin.as_ref().derivation_method.single_addr_or_err()?.clone(); - (*key_pair, my_address) + // [`WithdrawSenderAddress::derivation_path`] is not set, but the coin is initialized with an HD wallet derivation method. + None if coin.has_hd_wallet_derivation_method() => { + let error = "Cannot determine 'from' address derivation path".to_owned(); + return MmError::err(WithdrawError::UnexpectedFromAddress(error)); }, + None => *coin.as_ref().priv_key_policy.activated_key_or_err()?, }; - let my_address_string = my_address.display_address().map_to_mm(WithdrawError::InternalError)?; + Ok(StandardUtxoWithdraw { coin, req, key_pair, - my_address, - my_address_string, + from_address: from.address, + from_address_string, }) } } diff --git a/mm2src/coins/utxo_signer/src/sign_common.rs b/mm2src/coins/utxo_signer/src/sign_common.rs index a9b7005f9d..1ba966aa6b 100644 --- a/mm2src/coins/utxo_signer/src/sign_common.rs +++ b/mm2src/coins/utxo_signer/src/sign_common.rs @@ -29,6 +29,7 @@ pub(crate) fn complete_tx(unsigned: TransactionInputSigner, signed_inputs: Vec, // `z_derivation_path` can be the same or different from [`UtxoCoinFields::derivation_path`]. - z_derivation_path: Option, + z_derivation_path: Option, } impl Parameters for ZcoinConsensusParams { @@ -982,7 +983,7 @@ impl<'a> ZCoinBuilder<'a> { priv_key_policy: PrivKeyActivationPolicy::ContextPrivKey, check_utxo_maturity: None, // This is not used for Zcoin so we just provide a default value - path_to_address: StandardHDCoinAddress::default(), + path_to_address: HDPathAccountToAddressId::default(), }; ZCoinBuilder { ctx, @@ -1085,7 +1086,7 @@ impl MarketCoinOps for ZCoin { fn my_address(&self) -> MmResult { Ok(self.z_fields.my_z_addr_encoded.clone()) } - fn get_public_key(&self) -> Result> { + async fn get_public_key(&self) -> Result> { let pubkey = utxo_common::my_public_key(self.as_ref())?; Ok(pubkey.to_string()) } @@ -1193,6 +1194,8 @@ impl MarketCoinOps for ZCoin { fn min_trading_vol(&self) -> MmNumber { utxo_common::min_trading_vol(self.as_ref()) } fn is_privacy(&self) -> bool { true } + + fn is_trezor(&self) -> bool { self.as_ref().priv_key_policy.is_trezor() } } #[async_trait] @@ -1255,66 +1258,68 @@ impl SwapOps for ZCoin { Box::new(fut.boxed().compat()) } - fn send_maker_spends_taker_payment(&self, maker_spends_payment_args: SpendPaymentArgs<'_>) -> TransactionFut { - let tx = try_tx_fus!(ZTransaction::read(maker_spends_payment_args.other_payment_tx)); + async fn send_maker_spends_taker_payment( + &self, + maker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { + let tx = try_tx_s!(ZTransaction::read(maker_spends_payment_args.other_payment_tx)); let key_pair = self.derive_htlc_key_pair(maker_spends_payment_args.swap_unique_data); - let time_lock = try_tx_fus!(maker_spends_payment_args.time_lock.try_into()); + let time_lock = try_tx_s!(maker_spends_payment_args.time_lock.try_into()); let redeem_script = payment_script( time_lock, maker_spends_payment_args.secret_hash, - &try_tx_fus!(Public::from_slice(maker_spends_payment_args.other_pubkey)), + &try_tx_s!(Public::from_slice(maker_spends_payment_args.other_pubkey)), key_pair.public(), ); let script_data = ScriptBuilder::default() .push_data(maker_spends_payment_args.secret) .push_opcode(Opcode::OP_0) .into_script(); - let selfi = self.clone(); - let fut = async move { - let tx_fut = z_p2sh_spend( - &selfi, + let tx = try_ztx_s!( + z_p2sh_spend( + self, tx, time_lock, SEQUENCE_FINAL, redeem_script, script_data, &key_pair, - ); - let tx = try_ztx_s!(tx_fut.await); - Ok(tx.into()) - }; - Box::new(fut.boxed().compat()) + ) + .await + ); + Ok(tx.into()) } - fn send_taker_spends_maker_payment(&self, taker_spends_payment_args: SpendPaymentArgs<'_>) -> TransactionFut { - let tx = try_tx_fus!(ZTransaction::read(taker_spends_payment_args.other_payment_tx)); + async fn send_taker_spends_maker_payment( + &self, + taker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { + let tx = try_tx_s!(ZTransaction::read(taker_spends_payment_args.other_payment_tx)); let key_pair = self.derive_htlc_key_pair(taker_spends_payment_args.swap_unique_data); - let time_lock = try_tx_fus!(taker_spends_payment_args.time_lock.try_into()); + let time_lock = try_tx_s!(taker_spends_payment_args.time_lock.try_into()); let redeem_script = payment_script( time_lock, taker_spends_payment_args.secret_hash, - &try_tx_fus!(Public::from_slice(taker_spends_payment_args.other_pubkey)), + &try_tx_s!(Public::from_slice(taker_spends_payment_args.other_pubkey)), key_pair.public(), ); let script_data = ScriptBuilder::default() .push_data(taker_spends_payment_args.secret) .push_opcode(Opcode::OP_0) .into_script(); - let selfi = self.clone(); - let fut = async move { - let tx_fut = z_p2sh_spend( - &selfi, + let tx = try_ztx_s!( + z_p2sh_spend( + self, tx, time_lock, SEQUENCE_FINAL, redeem_script, script_data, &key_pair, - ); - let tx = try_ztx_s!(tx_fut.await); - Ok(tx.into()) - }; - Box::new(fut.boxed().compat()) + ) + .await + ); + Ok(tx.into()) } async fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { diff --git a/mm2src/coins/z_coin/z_balance_streaming.rs b/mm2src/coins/z_coin/z_balance_streaming.rs index 2e4c77df77..5f6d3e590a 100644 --- a/mm2src/coins/z_coin/z_balance_streaming.rs +++ b/mm2src/coins/z_coin/z_balance_streaming.rs @@ -1,5 +1,4 @@ use crate::common::Future01CompatExt; -use crate::hd_wallet::AsyncMutex; use crate::z_coin::ZCoin; use crate::{MarketCoinOps, MmCoin}; @@ -9,10 +8,11 @@ use common::log::{error, info}; use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; use futures::channel::oneshot; use futures::channel::oneshot::{Receiver, Sender}; +use futures::lock::Mutex as AsyncMutex; use futures_util::StreamExt; use mm2_core::mm_ctx::MmArc; use mm2_event_stream::behaviour::{EventBehaviour, EventInitStatus}; -use mm2_event_stream::{Event, EventStreamConfiguration}; +use mm2_event_stream::{ErrorEventName, Event, EventName, EventStreamConfiguration}; use std::sync::Arc; pub type ZBalanceEventSender = UnboundedSender<()>; @@ -20,8 +20,9 @@ pub type ZBalanceEventHandler = Arc>>; #[async_trait] impl EventBehaviour for ZCoin { - const EVENT_NAME: &'static str = "COIN_BALANCE"; - const ERROR_EVENT_NAME: &'static str = "COIN_BALANCE_ERROR"; + fn event_name() -> EventName { EventName::CoinBalance } + + fn error_event_name() -> ErrorEventName { ErrorEventName::CoinBalanceError } async fn handle(self, _interval: f64, tx: Sender) { const RECEIVER_DROPPED_MSG: &str = "Receiver is dropped, which should never happen."; @@ -65,7 +66,7 @@ impl EventBehaviour for ZCoin { }); ctx.stream_channel_controller - .broadcast(Event::new(Self::EVENT_NAME.to_string(), payload.to_string())) + .broadcast(Event::new(Self::event_name().to_string(), payload.to_string())) .await; }, Err(err) => { @@ -75,7 +76,7 @@ impl EventBehaviour for ZCoin { return ctx .stream_channel_controller .broadcast(Event::new( - format!("{}:{}", Self::ERROR_EVENT_NAME, ticker), + format!("{}:{}", Self::error_event_name(), ticker), e.to_string(), )) .await; @@ -85,10 +86,10 @@ impl EventBehaviour for ZCoin { } async fn spawn_if_active(self, config: &EventStreamConfiguration) -> EventInitStatus { - if let Some(event) = config.get_event(Self::EVENT_NAME) { + if let Some(event) = config.get_event(&Self::event_name()) { info!( "{} event is activated for {} address {}. `stream_interval_seconds`({}) has no effect on this.", - Self::EVENT_NAME, + Self::event_name(), self.ticker(), self.my_z_address_encoded(), event.stream_interval_seconds @@ -96,8 +97,11 @@ impl EventBehaviour for ZCoin { let (tx, rx): (Sender, Receiver) = oneshot::channel(); let fut = self.clone().handle(event.stream_interval_seconds, tx); - let settings = - AbortSettings::info_on_abort(format!("{} event is stopped for {}.", Self::EVENT_NAME, self.ticker())); + let settings = AbortSettings::info_on_abort(format!( + "{} event is stopped for {}.", + Self::event_name(), + self.ticker() + )); self.spawner().spawn_with_settings(fut, settings); rx.await.unwrap_or_else(|e| { diff --git a/mm2src/coins/z_coin/z_coin_native_tests.rs b/mm2src/coins/z_coin/z_coin_native_tests.rs index 9da9b2a41b..85bcdc7c8d 100644 --- a/mm2src/coins/z_coin/z_coin_native_tests.rs +++ b/mm2src/coins/z_coin/z_coin_native_tests.rs @@ -131,10 +131,7 @@ fn zombie_coin_send_and_spend_maker_payment() { swap_unique_data: pk_data.as_slice(), watcher_reward: false, }; - let spend_tx = coin - .send_taker_spends_maker_payment(spends_payment_args) - .wait() - .unwrap(); + let spend_tx = block_on(coin.send_taker_spends_maker_payment(spends_payment_args)).unwrap(); log!("spend tx {}", hex::encode(spend_tx.tx_hash().0)); } diff --git a/mm2src/coins_activation/Cargo.toml b/mm2src/coins_activation/Cargo.toml index e9fb215c1b..fb83a2ad7d 100644 --- a/mm2src/coins_activation/Cargo.toml +++ b/mm2src/coins_activation/Cargo.toml @@ -9,6 +9,7 @@ doctest = false [features] enable-solana = [] default = [] +for-tests = [] [dependencies] async-trait = "0.1" diff --git a/mm2src/coins_activation/src/bch_with_tokens_activation.rs b/mm2src/coins_activation/src/bch_with_tokens_activation.rs index 4d55bb5168..ebda8efcba 100644 --- a/mm2src/coins_activation/src/bch_with_tokens_activation.rs +++ b/mm2src/coins_activation/src/bch_with_tokens_activation.rs @@ -1,3 +1,5 @@ +use crate::context::CoinsActivationContext; +use crate::platform_coin_with_tokens::InitPlatformCoinWithTokensTask; use crate::platform_coin_with_tokens::*; use crate::prelude::*; use crate::slp_token_activation::SlpActivationRequest; @@ -19,6 +21,7 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_event_stream::EventStreamConfiguration; use mm2_number::BigDecimal; +use rpc_task::RpcTaskHandleShared; use serde_derive::{Deserialize, Serialize}; use serde_json::Value as Json; use std::collections::{HashMap, HashSet}; @@ -130,6 +133,10 @@ impl TxHistory for BchWithTokensActivationRequest { fn tx_history(&self) -> bool { self.platform_request.utxo_params.tx_history } } +impl ActivationRequestInfo for BchWithTokensActivationRequest { + fn is_hw_policy(&self) -> bool { self.platform_request.utxo_params.is_hw_policy() } +} + pub struct BchProtocolInfo { slp_prefix: String, } @@ -146,7 +153,7 @@ impl TryFromCoinProtocol for BchProtocolInfo { } } -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, Clone)] pub struct BchWithTokensActivationResult { current_block: u64, bch_addresses_infos: HashMap>, @@ -167,7 +174,7 @@ impl CurrentBlock for BchWithTokensActivationResult { fn current_block(&self) -> u64 { self.current_block } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum BchWithTokensActivationError { PlatformCoinCreationError { ticker: String, @@ -203,12 +210,16 @@ impl From for BchWithTokensActivationError { } #[async_trait] -impl PlatformWithTokensActivationOps for BchCoin { +impl PlatformCoinWithTokensActivationOps for BchCoin { type ActivationRequest = BchWithTokensActivationRequest; type PlatformProtocolInfo = BchProtocolInfo; type ActivationResult = BchWithTokensActivationResult; type ActivationError = BchWithTokensActivationError; + type InProgressStatus = InitPlatformCoinWithTokensInProgressStatus; + type AwaitingStatus = InitPlatformCoinWithTokensAwaitingStatus; + type UserAction = InitPlatformCoinWithTokensUserAction; + async fn enable_platform_coin( ctx: MmArc, ticker: String, @@ -266,28 +277,30 @@ impl PlatformWithTokensActivationOps for BchCoin { async fn get_activation_result( &self, + _task_handle: Option>>, activation_request: &Self::ActivationRequest, _nft_global: &Option, ) -> Result> { let current_block = self.as_ref().rpc_client.get_block_count().compat().await?; - let my_address = self.as_ref().derivation_method.single_addr_or_err()?; + let my_address = self.as_ref().derivation_method.single_addr_or_err().await?; let my_slp_address = self .get_my_slp_address() + .await .map_to_mm(BchWithTokensActivationError::Internal)? .encode() .map_to_mm(BchWithTokensActivationError::Internal)?; let pubkey = self.my_public_key()?.to_string(); let mut bch_address_info = CoinAddressInfo { - derivation_method: DerivationMethod::Iguana, + derivation_method: self.as_ref().derivation_method.to_response().await?, pubkey: pubkey.clone(), balances: None, tickers: None, }; let mut slp_address_info = CoinAddressInfo { - derivation_method: DerivationMethod::Iguana, + derivation_method: self.as_ref().derivation_method.to_response().await?, pubkey: pubkey.clone(), balances: None, tickers: None, @@ -306,7 +319,7 @@ impl PlatformWithTokensActivationOps for BchCoin { }); } - let bch_unspents = self.bch_unspents_for_display(my_address).await?; + let bch_unspents = self.bch_unspents_for_display(&my_address).await?; bch_address_info.balances = Some(bch_unspents.platform_balance(self.decimals())); drop_mutability!(bch_address_info); @@ -346,4 +359,10 @@ impl PlatformWithTokensActivationOps for BchCoin { ) -> Result<(), MmError> { Ok(()) } + + fn rpc_task_manager( + _activation_ctx: &CoinsActivationContext, + ) -> &InitPlatformCoinWithTokensTaskManagerShared { + unimplemented!() + } } diff --git a/mm2src/coins_activation/src/context.rs b/mm2src/coins_activation/src/context.rs index 26835ef076..a77f98e4ee 100644 --- a/mm2src/coins_activation/src/context.rs +++ b/mm2src/coins_activation/src/context.rs @@ -1,3 +1,5 @@ +use crate::eth_with_token_activation::EthTaskManagerShared; +use crate::init_erc20_token_activation::Erc20TokenTaskManagerShared; #[cfg(not(target_arch = "wasm32"))] use crate::lightning_activation::LightningTaskManagerShared; use crate::utxo_activation::{QtumTaskManagerShared, UtxoStandardTaskManagerShared}; @@ -10,6 +12,8 @@ pub struct CoinsActivationContext { pub(crate) init_utxo_standard_task_manager: UtxoStandardTaskManagerShared, pub(crate) init_qtum_task_manager: QtumTaskManagerShared, pub(crate) init_z_coin_task_manager: ZcoinTaskManagerShared, + pub(crate) init_eth_task_manager: EthTaskManagerShared, + pub(crate) init_erc20_token_task_manager: Erc20TokenTaskManagerShared, #[cfg(not(target_arch = "wasm32"))] pub(crate) init_lightning_task_manager: LightningTaskManagerShared, } @@ -22,6 +26,8 @@ impl CoinsActivationContext { init_utxo_standard_task_manager: RpcTaskManager::new_shared(), init_qtum_task_manager: RpcTaskManager::new_shared(), init_z_coin_task_manager: RpcTaskManager::new_shared(), + init_eth_task_manager: RpcTaskManager::new_shared(), + init_erc20_token_task_manager: RpcTaskManager::new_shared(), #[cfg(not(target_arch = "wasm32"))] init_lightning_task_manager: RpcTaskManager::new_shared(), }) diff --git a/mm2src/coins_activation/src/erc20_token_activation.rs b/mm2src/coins_activation/src/erc20_token_activation.rs index 173092c65d..664f2c22fd 100644 --- a/mm2src/coins_activation/src/erc20_token_activation.rs +++ b/mm2src/coins_activation/src/erc20_token_activation.rs @@ -1,13 +1,14 @@ use crate::{prelude::{TryFromCoinProtocol, TryPlatformCoinFromMmCoinEnum}, token::{EnableTokenError, TokenActivationOps, TokenProtocolParams}}; use async_trait::async_trait; +use coins::eth::display_eth_address; use coins::eth::v2_activation::{EthTokenActivationParams, EthTokenProtocol, NftProtocol, NftProviderEnum}; use coins::nft::nft_structs::NftInfo; use coins::{eth::{v2_activation::{Erc20Protocol, EthTokenActivationError}, valid_addr_from_str, EthCoin}, - CoinBalance, CoinProtocol, MarketCoinOps, MmCoin, MmCoinEnum}; + CoinBalance, CoinProtocol, CoinWithDerivationMethod, MarketCoinOps, MmCoin, MmCoinEnum}; use common::Future01CompatExt; -use mm2_err_handle::prelude::MmError; +use mm2_err_handle::prelude::*; use serde::Serialize; use std::collections::HashMap; @@ -40,6 +41,7 @@ impl From for EnableTokenError { | EthTokenActivationError::Transport(e) | EthTokenActivationError::ClientConnectionFailed(e) => EnableTokenError::Transport(e), EthTokenActivationError::InvalidPayload(e) => EnableTokenError::InvalidPayload(e), + EthTokenActivationError::UnexpectedDerivationMethod(e) => EnableTokenError::UnexpectedDerivationMethod(e), } } } @@ -124,10 +126,10 @@ impl TokenActivationOps for EthCoin { EthTokenActivationParams::Erc20(erc20_init_params) => match protocol_conf { EthTokenProtocol::Erc20(erc20_protocol) => { let token = platform_coin - .initialize_erc20_token(erc20_init_params, erc20_protocol, ticker) + .initialize_erc20_token(erc20_init_params, erc20_protocol, ticker.clone()) .await?; - let address = token.my_address()?; + let address = display_eth_address(&token.derivation_method().single_addr_or_err().await?); let token_contract_address = token.erc20_token_address().ok_or_else(|| { EthTokenActivationError::InternalError("Token contract address is missing".to_string()) })?; diff --git a/mm2src/coins_activation/src/eth_with_token_activation.rs b/mm2src/coins_activation/src/eth_with_token_activation.rs index 9e0b75ebb4..fb0792704e 100644 --- a/mm2src/coins_activation/src/eth_with_token_activation.rs +++ b/mm2src/coins_activation/src/eth_with_token_activation.rs @@ -1,42 +1,51 @@ -use crate::{platform_coin_with_tokens::{EnablePlatformCoinWithTokensError, GetPlatformBalance, - InitTokensAsMmCoinsError, PlatformWithTokensActivationOps, RegisterTokenInfo, - TokenActivationParams, TokenActivationRequest, TokenAsMmCoinInitializer, - TokenInitializer, TokenOf}, - prelude::*}; +use crate::context::CoinsActivationContext; +use crate::platform_coin_with_tokens::{platform_coin_xpub_extractor_rpc_statuses, EnablePlatformCoinWithTokensError, + GetPlatformBalance, InitPlatformCoinWithTokensAwaitingStatus, + InitPlatformCoinWithTokensInProgressStatus, + InitPlatformCoinWithTokensTaskManagerShared, + InitPlatformCoinWithTokensUserAction, InitTokensAsMmCoinsError, + PlatformCoinWithTokensActivationOps, RegisterTokenInfo, TokenActivationParams, + TokenActivationRequest, TokenAsMmCoinInitializer, TokenInitializer, TokenOf}; +use crate::prelude::*; use async_trait::async_trait; -use coins::eth::v2_activation::{NftActivationRequest, NftProviderEnum}; -use coins::eth::EthPrivKeyBuildPolicy; +use coins::coin_balance::{CoinBalanceReport, EnableCoinBalanceOps}; +use coins::eth::v2_activation::{eth_coin_from_conf_and_request_v2, Erc20Protocol, Erc20TokenActivationRequest, + EthActivationV2Error, EthActivationV2Request, EthPrivKeyActivationPolicy}; +use coins::eth::v2_activation::{EthTokenActivationError, NftActivationRequest, NftProviderEnum}; +use coins::eth::{Erc20TokenInfo, EthCoin, EthCoinType, EthPrivKeyBuildPolicy}; +use coins::hd_wallet::RpcTaskXPubExtractor; +use coins::my_tx_history_v2::TxHistoryStorage; use coins::nft::nft_structs::NftInfo; -use coins::{eth::v2_activation::EthPrivKeyActivationPolicy, MmCoinEnum}; -use coins::{eth::{v2_activation::{eth_coin_from_conf_and_request_v2, Erc20Protocol, Erc20TokenActivationRequest, - EthActivationV2Error, EthActivationV2Request, EthTokenActivationError}, - Erc20TokenInfo, EthCoin, EthCoinType}, - my_tx_history_v2::TxHistoryStorage, - CoinBalance, CoinProtocol, MarketCoinOps, MmCoin}; +use coins::{CoinBalance, CoinBalanceMap, CoinProtocol, CoinWithDerivationMethod, DerivationMethod, MarketCoinOps, + MmCoin, MmCoinEnum}; + +use crate::platform_coin_with_tokens::InitPlatformCoinWithTokensTask; use common::Future01CompatExt; use common::{drop_mutability, true_f}; +use crypto::HwRpcError; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_event_stream::EventStreamConfiguration; #[cfg(target_arch = "wasm32")] use mm2_metamask::MetamaskRpcError; use mm2_number::BigDecimal; +use rpc_task::RpcTaskHandleShared; use serde::{Deserialize, Serialize}; use serde_json::Value as Json; use std::collections::{HashMap, HashSet}; +pub type EthTaskManagerShared = InitPlatformCoinWithTokensTaskManagerShared; + impl From for EnablePlatformCoinWithTokensError { fn from(err: EthActivationV2Error) -> Self { match err { EthActivationV2Error::InvalidPayload(e) | EthActivationV2Error::InvalidSwapContractAddr(e) | EthActivationV2Error::InvalidFallbackSwapContract(e) - | EthActivationV2Error::ErrorDeserializingDerivationPath(e) => { - EnablePlatformCoinWithTokensError::InvalidPayload(e) - }, - #[cfg(target_arch = "wasm32")] - EthActivationV2Error::ExpectedRpcChainId => { - EnablePlatformCoinWithTokensError::InvalidPayload(err.to_string()) + | EthActivationV2Error::ErrorDeserializingDerivationPath(e) + | EthActivationV2Error::InvalidPathToAddress(e) => EnablePlatformCoinWithTokensError::InvalidPayload(e), + EthActivationV2Error::ChainIdNotSet => { + EnablePlatformCoinWithTokensError::Internal("`chain_id` is not set in coin config".to_string()) }, EthActivationV2Error::ActivationFailed { ticker, error } => { EnablePlatformCoinWithTokensError::PlatformCoinCreationError { ticker, error } @@ -53,12 +62,29 @@ impl From for EnablePlatformCoinWithTokensError { EthActivationV2Error::FailedSpawningBalanceEvents(e) => { EnablePlatformCoinWithTokensError::FailedSpawningBalanceEvents(e) }, + EthActivationV2Error::HDWalletStorageError(e) => EnablePlatformCoinWithTokensError::Internal(e), #[cfg(target_arch = "wasm32")] EthActivationV2Error::MetamaskError(metamask) => { EnablePlatformCoinWithTokensError::Transport(metamask.to_string()) }, EthActivationV2Error::InternalError(e) => EnablePlatformCoinWithTokensError::Internal(e), EthActivationV2Error::Transport(e) => EnablePlatformCoinWithTokensError::Transport(e), + EthActivationV2Error::UnexpectedDerivationMethod(e) => { + EnablePlatformCoinWithTokensError::UnexpectedDerivationMethod(e.to_string()) + }, + EthActivationV2Error::HwContextNotInitialized => { + EnablePlatformCoinWithTokensError::Internal("Hardware wallet is not initalised".to_string()) + }, + EthActivationV2Error::CoinDoesntSupportTrezor => { + EnablePlatformCoinWithTokensError::Internal("Coin does not support Trezor wallet".to_string()) + }, + EthActivationV2Error::TaskTimedOut { .. } => { + EnablePlatformCoinWithTokensError::Internal("Coin activation timed out".to_string()) + }, + EthActivationV2Error::HwError(e) => EnablePlatformCoinWithTokensError::Internal(e.to_string()), + EthActivationV2Error::InvalidHardwareWalletCall => EnablePlatformCoinWithTokensError::Internal( + "Hardware wallet must be used within rpc task manager".to_string(), + ), } } } @@ -88,6 +114,9 @@ impl From for InitTokensAsMmCoinsError { }, EthTokenActivationError::InvalidPayload(e) => InitTokensAsMmCoinsError::InvalidPayload(e), EthTokenActivationError::Transport(e) => InitTokensAsMmCoinsError::Transport(e), + EthTokenActivationError::UnexpectedDerivationMethod(e) => { + InitTokensAsMmCoinsError::UnexpectedDerivationMethod(e) + }, } } } @@ -138,6 +167,10 @@ impl TxHistory for EthWithTokensActivationRequest { fn tx_history(&self) -> bool { false } } +impl ActivationRequestInfo for EthWithTokensActivationRequest { + fn is_hw_policy(&self) -> bool { self.platform_request.priv_key_policy.is_hw_policy() } +} + impl TokenOf for EthCoin { type PlatformCoin = EthCoin; } @@ -156,39 +189,80 @@ impl RegisterTokenInfo for EthCoin { } } -/// Represents the result of activating an Ethereum-based coin along with its associated tokens (ERC20 and NFTs). -/// -/// This structure provides a snapshot of the relevant activation data, including the current blockchain block, -/// information about Ethereum addresses and their balances, ERC-20 token balances, and a summary of NFT ownership. -#[derive(Serialize)] -pub struct EthWithTokensActivationResult { +/// Activation result for activating an EVM-based coin along with its associated tokens (ERC20 and NFTs) for Iguana wallets. +#[derive(Serialize, Clone)] +pub struct IguanaEthWithTokensActivationResult { current_block: u64, eth_addresses_infos: HashMap>, erc20_addresses_infos: HashMap>, nfts_infos: HashMap, } +/// Activation result for activating an EVM-based coin along with its associated tokens (ERC20 and NFTs) for HD wallets. +#[derive(Serialize, Clone)] +pub struct HDEthWithTokensActivationResult { + current_block: u64, + ticker: String, + wallet_balance: CoinBalanceReport, + // Todo: Move to wallet_balance when implementing HDWallet for NFTs + nfts_infos: HashMap, +} + +/// Represents the result of activating an Ethereum-based coin along with its associated tokens (ERC20 and NFTs). +/// +/// This structure provides a snapshot of the relevant activation data, including the current blockchain block, +/// information about Ethereum addresses and their balances, ERC-20 token balances, and a summary of NFT ownership. +#[derive(Serialize, Clone)] +#[serde(untagged)] +pub enum EthWithTokensActivationResult { + Iguana(IguanaEthWithTokensActivationResult), + HD(HDEthWithTokensActivationResult), +} + impl GetPlatformBalance for EthWithTokensActivationResult { fn get_platform_balance(&self) -> Option { - self.eth_addresses_infos - .iter() - .fold(Some(BigDecimal::from(0)), |total, (_, addr_info)| { - total.and_then(|t| addr_info.balances.as_ref().map(|b| t + b.get_total())) - }) + match self { + EthWithTokensActivationResult::Iguana(result) => result + .eth_addresses_infos + .iter() + .fold(Some(BigDecimal::from(0)), |total, (_, addr_info)| { + total.and_then(|t| addr_info.balances.as_ref().map(|b| t + b.get_total())) + }), + EthWithTokensActivationResult::HD(result) => result + .wallet_balance + .to_addresses_total_balances(&result.ticker) + .iter() + .fold(None, |maybe_total, (_, maybe_balance)| { + match (maybe_total, maybe_balance) { + (Some(total), Some(balance)) => Some(total + balance), + (None, Some(balance)) => Some(balance.clone()), + (total, None) => total, + } + }), + } } } impl CurrentBlock for EthWithTokensActivationResult { - fn current_block(&self) -> u64 { self.current_block } + fn current_block(&self) -> u64 { + match self { + EthWithTokensActivationResult::Iguana(result) => result.current_block, + EthWithTokensActivationResult::HD(result) => result.current_block, + } + } } #[async_trait] -impl PlatformWithTokensActivationOps for EthCoin { +impl PlatformCoinWithTokensActivationOps for EthCoin { type ActivationRequest = EthWithTokensActivationRequest; type PlatformProtocolInfo = EthCoinType; type ActivationResult = EthWithTokensActivationResult; type ActivationError = EthActivationV2Error; + type InProgressStatus = InitPlatformCoinWithTokensInProgressStatus; + type AwaitingStatus = InitPlatformCoinWithTokensAwaitingStatus; + type UserAction = InitPlatformCoinWithTokensUserAction; + async fn enable_platform_coin( ctx: MmArc, ticker: String, @@ -244,6 +318,7 @@ impl PlatformWithTokensActivationOps for EthCoin { async fn get_activation_result( &self, + task_handle: Option>>, activation_request: &Self::ActivationRequest, nft_global: &Option, ) -> Result> { @@ -253,64 +328,105 @@ impl PlatformWithTokensActivationOps for EthCoin { .await .map_err(EthActivationV2Error::InternalError)?; - let my_address = self.my_address()?; - let pubkey = self.get_public_key()?; - - let mut eth_address_info = CoinAddressInfo { - derivation_method: DerivationMethod::Iguana, - pubkey: pubkey.clone(), - balances: None, - tickers: None, - }; - - let mut erc20_address_info = CoinAddressInfo { - derivation_method: DerivationMethod::Iguana, - pubkey, - balances: None, - tickers: None, - }; - let nfts_map = if let Some(MmCoinEnum::EthCoin(nft_global)) = nft_global { nft_global.nfts_infos.lock().await.clone() } else { Default::default() }; - if !activation_request.get_balances { - drop_mutability!(eth_address_info); - let tickers: HashSet<_> = self.get_erc_tokens_infos().into_keys().collect(); - erc20_address_info.tickers = Some(tickers); - drop_mutability!(erc20_address_info); - - return Ok(EthWithTokensActivationResult { - current_block, - eth_addresses_infos: HashMap::from([(my_address.clone(), eth_address_info)]), - erc20_addresses_infos: HashMap::from([(my_address, erc20_address_info)]), - nfts_infos: nfts_map, - }); + match self.derivation_method() { + DerivationMethod::SingleAddress(my_address) => { + let pubkey = self.get_public_key().await?; + let mut eth_address_info = CoinAddressInfo { + derivation_method: self.derivation_method().to_response().await?, + pubkey: pubkey.clone(), + balances: None, + tickers: None, + }; + let mut erc20_address_info = CoinAddressInfo { + derivation_method: self.derivation_method().to_response().await?, + pubkey, + balances: None, + tickers: None, + }; + // Todo: make get_balances work with HDWallet if it's needed + if !activation_request.get_balances { + drop_mutability!(eth_address_info); + let tickers: HashSet<_> = self.get_erc_tokens_infos().into_keys().collect(); + erc20_address_info.tickers = Some(tickers); + drop_mutability!(erc20_address_info); + + return Ok(EthWithTokensActivationResult::Iguana( + IguanaEthWithTokensActivationResult { + current_block, + eth_addresses_infos: HashMap::from([(my_address.to_string(), eth_address_info)]), + erc20_addresses_infos: HashMap::from([(my_address.to_string(), erc20_address_info)]), + nfts_infos: nfts_map, + }, + )); + } + + let eth_balance = self + .my_balance() + .compat() + .await + .map_err(|e| EthActivationV2Error::CouldNotFetchBalance(e.to_string()))?; + eth_address_info.balances = Some(eth_balance); + drop_mutability!(eth_address_info); + + let token_balances = self + .get_tokens_balance_list() + .await + .map_err(|e| EthActivationV2Error::CouldNotFetchBalance(e.to_string()))?; + erc20_address_info.balances = Some(token_balances); + drop_mutability!(erc20_address_info); + + Ok(EthWithTokensActivationResult::Iguana( + IguanaEthWithTokensActivationResult { + current_block, + eth_addresses_infos: HashMap::from([(my_address.to_string(), eth_address_info)]), + erc20_addresses_infos: HashMap::from([(my_address.to_string(), erc20_address_info)]), + nfts_infos: nfts_map, + }, + )) + }, + DerivationMethod::HDWallet(_) => { + let xpub_extractor = if self.is_trezor() { + let ctx = MmArc::from_weak(&self.ctx).ok_or(EthActivationV2Error::InvalidHardwareWalletCall)?; + let task_handle = task_handle.ok_or_else(|| { + EthActivationV2Error::InternalError( + "Hardware wallet must be accessed under task manager".to_string(), + ) + })?; + Some( + RpcTaskXPubExtractor::new_trezor_extractor( + &ctx, + task_handle, + platform_coin_xpub_extractor_rpc_statuses(), + CoinProtocol::ETH, + ) + .map_err(|_| MmError::new(EthActivationV2Error::HwError(HwRpcError::NotInitialized)))?, + ) + } else { + None + }; + + let wallet_balance = self + .enable_coin_balance( + xpub_extractor, + activation_request.platform_request.enable_params.clone(), + &activation_request.platform_request.path_to_address, + ) + .await?; + + Ok(EthWithTokensActivationResult::HD(HDEthWithTokensActivationResult { + current_block, + ticker: self.ticker().to_string(), + wallet_balance, + nfts_infos: nfts_map, + })) + }, } - - let eth_balance = self - .my_balance() - .compat() - .await - .map_err(|e| EthActivationV2Error::CouldNotFetchBalance(e.to_string()))?; - eth_address_info.balances = Some(eth_balance); - drop_mutability!(eth_address_info); - - let token_balances = self - .get_tokens_balance_list() - .await - .map_err(|e| EthActivationV2Error::CouldNotFetchBalance(e.to_string()))?; - erc20_address_info.balances = Some(token_balances); - drop_mutability!(erc20_address_info); - - Ok(EthWithTokensActivationResult { - current_block, - eth_addresses_infos: HashMap::from([(my_address.clone(), eth_address_info)]), - erc20_addresses_infos: HashMap::from([(my_address, erc20_address_info)]), - nfts_infos: nfts_map, - }) } fn start_history_background_fetching( @@ -327,6 +443,12 @@ impl PlatformWithTokensActivationOps for EthCoin { ) -> Result<(), MmError> { Ok(()) } + + fn rpc_task_manager( + activation_ctx: &CoinsActivationContext, + ) -> &InitPlatformCoinWithTokensTaskManagerShared { + &activation_ctx.init_eth_task_manager + } } fn eth_priv_key_build_policy( @@ -342,5 +464,6 @@ fn eth_priv_key_build_policy( .or_mm_err(|| EthActivationV2Error::MetamaskError(MetamaskRpcError::MetamaskCtxNotInitialized))?; Ok(EthPrivKeyBuildPolicy::Metamask(metamask_ctx)) }, + EthPrivKeyActivationPolicy::Trezor => Ok(EthPrivKeyBuildPolicy::Trezor), } } diff --git a/mm2src/coins_activation/src/init_erc20_token_activation.rs b/mm2src/coins_activation/src/init_erc20_token_activation.rs new file mode 100644 index 0000000000..5bc4e665ff --- /dev/null +++ b/mm2src/coins_activation/src/init_erc20_token_activation.rs @@ -0,0 +1,183 @@ +use crate::context::CoinsActivationContext; +use crate::init_token::{token_xpub_extractor_rpc_statuses, InitTokenActivationOps, InitTokenActivationResult, + InitTokenAwaitingStatus, InitTokenError, InitTokenInProgressStatus, InitTokenTaskHandleShared, + InitTokenTaskManagerShared, InitTokenUserAction}; +use async_trait::async_trait; +use coins::coin_balance::{EnableCoinBalanceError, EnableCoinBalanceOps}; +use coins::eth::v2_activation::{Erc20Protocol, EthTokenActivationError, InitErc20TokenActivationRequest}; +use coins::eth::EthCoin; +use coins::hd_wallet::RpcTaskXPubExtractor; +use coins::{MarketCoinOps, MmCoin, RegisterCoinError}; +use common::Future01CompatExt; +use crypto::HwRpcError; +use derive_more::Display; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::mm_error::MmError; +use mm2_err_handle::prelude::*; +use rpc_task::RpcTaskError; +use ser_error_derive::SerializeErrorType; +use serde_derive::Serialize; +use std::time::Duration; + +pub type Erc20TokenTaskManagerShared = InitTokenTaskManagerShared; + +#[derive(Clone, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum InitErc20Error { + #[display(fmt = "{}", _0)] + HwError(HwRpcError), + #[display(fmt = "Initialization task has timed out {:?}", duration)] + TaskTimedOut { duration: Duration }, + #[display(fmt = "Token {} is activated already", ticker)] + TokenIsAlreadyActivated { ticker: String }, + #[display(fmt = "Error on token {} creation: {}", ticker, error)] + TokenCreationError { ticker: String, error: String }, + #[display(fmt = "Could not fetch balance: {}", _0)] + CouldNotFetchBalance(String), + #[display(fmt = "Transport error: {}", _0)] + Transport(String), + #[display(fmt = "Internal error: {}", _0)] + Internal(String), +} + +impl From for InitTokenError { + fn from(e: InitErc20Error) -> Self { + match e { + InitErc20Error::HwError(hw) => InitTokenError::HwError(hw), + InitErc20Error::TaskTimedOut { duration } => InitTokenError::TaskTimedOut { duration }, + InitErc20Error::TokenIsAlreadyActivated { ticker } => InitTokenError::TokenIsAlreadyActivated { ticker }, + InitErc20Error::TokenCreationError { ticker, error } => { + InitTokenError::TokenCreationError { ticker, error } + }, + InitErc20Error::CouldNotFetchBalance(error) => InitTokenError::CouldNotFetchBalance(error), + InitErc20Error::Transport(transport) => InitTokenError::Transport(transport), + InitErc20Error::Internal(internal) => InitTokenError::Internal(internal), + } + } +} + +impl From for InitErc20Error { + fn from(e: EthTokenActivationError) -> Self { + match e { + EthTokenActivationError::InternalError(_) | EthTokenActivationError::UnexpectedDerivationMethod(_) => { + InitErc20Error::Internal(e.to_string()) + }, + EthTokenActivationError::ClientConnectionFailed(_) + | EthTokenActivationError::CouldNotFetchBalance(_) + | EthTokenActivationError::InvalidPayload(_) + | EthTokenActivationError::Transport(_) => InitErc20Error::Transport(e.to_string()), + } + } +} + +impl From for InitErc20Error { + fn from(err: RegisterCoinError) -> InitErc20Error { + match err { + RegisterCoinError::CoinIsInitializedAlready { coin } => { + InitErc20Error::TokenIsAlreadyActivated { ticker: coin } + }, + RegisterCoinError::Internal(e) => InitErc20Error::Internal(e), + } + } +} + +impl From for InitErc20Error { + fn from(rpc_err: RpcTaskError) -> Self { + match rpc_err { + RpcTaskError::Timeout(duration) => InitErc20Error::TaskTimedOut { duration }, + internal_error => InitErc20Error::Internal(internal_error.to_string()), + } + } +} + +impl From for InitErc20Error { + fn from(e: EnableCoinBalanceError) -> Self { + match e { + EnableCoinBalanceError::NewAddressDerivingError(err) => InitErc20Error::Internal(err.to_string()), + EnableCoinBalanceError::NewAccountCreationError(err) => InitErc20Error::Internal(err.to_string()), + EnableCoinBalanceError::BalanceError(err) => InitErc20Error::CouldNotFetchBalance(err.to_string()), + } + } +} + +#[async_trait] +impl InitTokenActivationOps for EthCoin { + type ActivationRequest = InitErc20TokenActivationRequest; + type ProtocolInfo = Erc20Protocol; + type ActivationResult = InitTokenActivationResult; + type ActivationError = InitErc20Error; + type InProgressStatus = InitTokenInProgressStatus; + type AwaitingStatus = InitTokenAwaitingStatus; + type UserAction = InitTokenUserAction; + + fn rpc_task_manager(activation_ctx: &CoinsActivationContext) -> &Erc20TokenTaskManagerShared { + &activation_ctx.init_erc20_token_task_manager + } + + async fn init_token( + ticker: String, + platform_coin: Self::PlatformCoin, + activation_request: &Self::ActivationRequest, + protocol_conf: Self::ProtocolInfo, + _task_handle: InitTokenTaskHandleShared, + ) -> Result> { + let token = platform_coin + .initialize_erc20_token(activation_request.clone().into(), protocol_conf, ticker) + .await?; + + Ok(token) + } + + // Todo: similar to utxo_activation a common method for getting activation result can be made, needed when more protocols that have tokens are supported + async fn get_activation_result( + &self, + ctx: MmArc, + protocol_conf: Self::ProtocolInfo, + task_handle: InitTokenTaskHandleShared, + activation_request: &Self::ActivationRequest, + ) -> Result> { + let ticker = self.ticker().to_owned(); + let current_block = self + .current_block() + .compat() + .await + .map_to_mm(EthTokenActivationError::Transport)?; + + let xpub_extractor = if self.is_trezor() { + Some( + RpcTaskXPubExtractor::new_trezor_extractor( + &ctx, + task_handle.clone(), + token_xpub_extractor_rpc_statuses(), + protocol_conf.into(), + ) + .mm_err(|_| InitErc20Error::HwError(HwRpcError::NotInitialized))?, + ) + } else { + None + }; + + task_handle.update_in_progress_status(InitTokenInProgressStatus::RequestingWalletBalance)?; + let wallet_balance = self + .enable_coin_balance( + xpub_extractor, + activation_request.enable_params.clone(), + &activation_request.path_to_address, + ) + .await?; + task_handle.update_in_progress_status(InitTokenInProgressStatus::ActivatingCoin)?; + + let token_contract_address = self + .erc20_token_address() + .ok_or_else(|| EthTokenActivationError::InternalError("Token contract address is missing".to_string()))?; + + Ok(InitTokenActivationResult { + ticker, + platform_coin: self.platform_ticker().to_owned(), + token_contract_address: format!("{:#02x}", token_contract_address), + current_block, + required_confirmations: self.required_confirmations(), + wallet_balance, + }) + } +} diff --git a/mm2src/coins_activation/src/init_token.rs b/mm2src/coins_activation/src/init_token.rs new file mode 100644 index 0000000000..dbc03b1754 --- /dev/null +++ b/mm2src/coins_activation/src/init_token.rs @@ -0,0 +1,366 @@ +use crate::context::CoinsActivationContext; +use crate::platform_coin_with_tokens::{RegisterTokenInfo, TokenOf}; +use crate::prelude::{coin_conf_with_protocol, CoinConfWithProtocolError, CurrentBlock, TryFromCoinProtocol, + TryPlatformCoinFromMmCoinEnum}; +use crate::token::TokenProtocolParams; +use async_trait::async_trait; +use coins::coin_balance::CoinBalanceReport; +use coins::{lp_coinfind, lp_coinfind_or_err, CoinBalanceMap, CoinProtocol, CoinsContext, MmCoinEnum, RegisterCoinError}; +use common::{log, HttpStatusCode, StatusCode, SuccessResponse}; +use crypto::hw_rpc_task::{HwConnectStatuses, HwRpcTaskAwaitingStatus, HwRpcTaskUserAction}; +use crypto::HwRpcError; +use derive_more::Display; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::mm_error::{MmError, MmResult, NotEqual, NotMmError}; +use mm2_err_handle::prelude::*; +use rpc_task::rpc_common::{CancelRpcTaskError, CancelRpcTaskRequest, InitRpcTaskResponse, RpcTaskStatusError, + RpcTaskStatusRequest, RpcTaskUserActionError, RpcTaskUserActionRequest}; +use rpc_task::{RpcTask, RpcTaskError, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, + RpcTaskTypes, TaskId}; +use ser_error_derive::SerializeErrorType; +use serde_derive::{Deserialize, Serialize}; +use std::time::Duration; + +pub type InitTokenResponse = InitRpcTaskResponse; +pub type InitTokenStatusRequest = RpcTaskStatusRequest; +pub type InitTokenUserActionRequest = RpcTaskUserActionRequest; +pub type InitTokenTaskManagerShared = RpcTaskManagerShared>; +pub type InitTokenTaskHandleShared = RpcTaskHandleShared>; + +pub type InitTokenAwaitingStatus = HwRpcTaskAwaitingStatus; +pub type InitTokenUserAction = HwRpcTaskUserAction; +pub type InitTokenStatusError = RpcTaskStatusError; +pub type InitTokenUserActionError = RpcTaskUserActionError; +pub type CancelInitTokenError = CancelRpcTaskError; + +/// Request for the `init_token` RPC command. +#[derive(Debug, Deserialize, Clone)] +pub struct InitTokenReq { + ticker: String, + activation_params: T, +} + +/// Trait for the initializing a token using the task manager. +#[async_trait] +pub trait InitTokenActivationOps: Into + TokenOf + Clone + Send + Sync + 'static { + type ActivationRequest: Clone + Send + Sync; + type ProtocolInfo: TokenProtocolParams + TryFromCoinProtocol + Clone + Send + Sync; + type ActivationResult: serde::Serialize + Clone + CurrentBlock + Send + Sync; + type ActivationError: From + + Into + + NotEqual + + SerMmErrorType + + Clone + + Send + + Sync; + type InProgressStatus: InitTokenInitialStatus + Clone + Send + Sync; + type AwaitingStatus: Clone + Send + Sync; + type UserAction: NotMmError + Send + Sync; + + /// Getter for the token initialization task manager. + fn rpc_task_manager(activation_ctx: &CoinsActivationContext) -> &InitTokenTaskManagerShared; + + /// Activates a token and returns the activated token instance. + async fn init_token( + ticker: String, + platform_coin: Self::PlatformCoin, + activation_request: &Self::ActivationRequest, + protocol_conf: Self::ProtocolInfo, + task_handle: InitTokenTaskHandleShared, + ) -> Result>; + + /// Returns the result of the token activation. + async fn get_activation_result( + &self, + ctx: MmArc, + token_protocol: Self::ProtocolInfo, + task_handle: InitTokenTaskHandleShared, + activation_request: &Self::ActivationRequest, + ) -> Result>; +} + +/// Implementation of the init token RPC command. +pub async fn init_token( + ctx: MmArc, + request: InitTokenReq, +) -> MmResult +where + Token: InitTokenActivationOps + Send + Sync + 'static, + Token::InProgressStatus: InitTokenInitialStatus, + InitTokenError: From, + (Token::ActivationError, InitTokenError): NotEqual, +{ + if let Ok(Some(_)) = lp_coinfind(&ctx, &request.ticker).await { + return MmError::err(InitTokenError::TokenIsAlreadyActivated { ticker: request.ticker }); + } + + let (_, token_protocol): (_, Token::ProtocolInfo) = coin_conf_with_protocol(&ctx, &request.ticker)?; + + let platform_coin = lp_coinfind_or_err(&ctx, token_protocol.platform_coin_ticker()) + .await + .mm_err(|_| InitTokenError::PlatformCoinIsNotActivated(token_protocol.platform_coin_ticker().to_owned()))?; + + let platform_coin = + Token::PlatformCoin::try_from_mm_coin(platform_coin).or_mm_err(|| InitTokenError::UnsupportedPlatformCoin { + platform_coin_ticker: token_protocol.platform_coin_ticker().into(), + token_ticker: request.ticker.clone(), + })?; + + let coins_act_ctx = CoinsActivationContext::from_ctx(&ctx).map_to_mm(InitTokenError::Internal)?; + let spawner = ctx.spawner(); + let task = InitTokenTask:: { + ctx, + request, + token_protocol, + platform_coin, + }; + let task_manager = Token::rpc_task_manager(&coins_act_ctx); + + let task_id = RpcTaskManager::spawn_rpc_task(task_manager, &spawner, task) + .mm_err(|e| InitTokenError::Internal(e.to_string()))?; + + Ok(InitTokenResponse { task_id }) +} + +/// Implementation of the init token status RPC command. +pub async fn init_token_status( + ctx: MmArc, + req: InitTokenStatusRequest, +) -> MmResult< + RpcTaskStatus, + InitTokenStatusError, +> +where + InitTokenError: From, +{ + let coins_act_ctx = CoinsActivationContext::from_ctx(&ctx).map_to_mm(InitTokenStatusError::Internal)?; + let mut task_manager = Token::rpc_task_manager(&coins_act_ctx) + .lock() + .map_to_mm(|poison| InitTokenStatusError::Internal(poison.to_string()))?; + task_manager + .task_status(req.task_id, req.forget_if_finished) + .or_mm_err(|| InitTokenStatusError::NoSuchTask(req.task_id)) + .map(|rpc_task| rpc_task.map_err(InitTokenError::from)) +} + +/// Implementation of the init token user action RPC command. +pub async fn init_token_user_action( + ctx: MmArc, + req: InitTokenUserActionRequest, +) -> MmResult { + let coins_act_ctx = CoinsActivationContext::from_ctx(&ctx).map_to_mm(InitTokenUserActionError::Internal)?; + let mut task_manager = Token::rpc_task_manager(&coins_act_ctx) + .lock() + .map_to_mm(|poison| InitTokenUserActionError::Internal(poison.to_string()))?; + task_manager.on_user_action(req.task_id, req.user_action)?; + Ok(SuccessResponse::new()) +} + +/// Implementation of the cancel init token RPC command. +pub async fn cancel_init_token( + ctx: MmArc, + req: CancelRpcTaskRequest, +) -> MmResult { + let coins_act_ctx = CoinsActivationContext::from_ctx(&ctx).map_to_mm(CancelInitTokenError::Internal)?; + let mut task_manager = Standalone::rpc_task_manager(&coins_act_ctx) + .lock() + .map_to_mm(|poison| CancelInitTokenError::Internal(poison.to_string()))?; + task_manager.cancel_task(req.task_id)?; + Ok(SuccessResponse::new()) +} + +/// A struct that contains the info needed by the task that initializes the token. +#[derive(Clone)] +pub struct InitTokenTask { + ctx: MmArc, + request: InitTokenReq, + token_protocol: Token::ProtocolInfo, + platform_coin: Token::PlatformCoin, +} + +impl RpcTaskTypes for InitTokenTask { + type Item = Token::ActivationResult; + type Error = Token::ActivationError; + type InProgressStatus = Token::InProgressStatus; + type AwaitingStatus = Token::AwaitingStatus; + type UserAction = Token::UserAction; +} + +#[async_trait] +impl RpcTask for InitTokenTask +where + Token: InitTokenActivationOps, +{ + fn initial_status(&self) -> Self::InProgressStatus { + ::initial_status() + } + + /// Try to disable the coin in case if we managed to register it already. + async fn cancel(self) { + if let Ok(c_ctx) = CoinsContext::from_ctx(&self.ctx) { + if let Ok(Some(coin)) = lp_coinfind(&self.ctx, &self.request.ticker).await { + c_ctx.remove_coin(coin).await; + }; + }; + } + + async fn run(&mut self, task_handle: RpcTaskHandleShared) -> Result> { + let ticker = self.request.ticker.clone(); + let token = Token::init_token( + ticker.clone(), + self.platform_coin.clone(), + &self.request.activation_params, + self.token_protocol.clone(), + task_handle.clone(), + ) + .await?; + + let activation_result = token + .get_activation_result( + self.ctx.clone(), + self.token_protocol.clone(), + task_handle, + &self.request.activation_params, + ) + .await?; + log::info!("{} current block {}", ticker, activation_result.current_block()); + + let coins_ctx = CoinsContext::from_ctx(&self.ctx).unwrap(); + coins_ctx.add_token(token.clone().into()).await?; + + self.platform_coin.register_token_info(&token); + + Ok(activation_result) + } +} + +/// Response for the init token RPC command. +#[derive(Clone, Serialize)] +pub struct InitTokenActivationResult { + pub ticker: String, + pub platform_coin: String, + pub token_contract_address: String, + pub current_block: u64, + pub required_confirmations: u64, + pub wallet_balance: CoinBalanceReport, +} + +impl CurrentBlock for InitTokenActivationResult { + fn current_block(&self) -> u64 { self.current_block } +} + +/// Trait for the initial status of the token initialization task. +pub trait InitTokenInitialStatus { + fn initial_status() -> Self; +} + +/// Status of the token initialization task. +#[derive(Clone, Serialize)] +pub enum InitTokenInProgressStatus { + ActivatingCoin, + TemporaryError(String), + RequestingWalletBalance, + Finishing, + /// This status doesn't require the user to send `UserAction`, + /// but it tells the user that he should confirm/decline an address on his device. + WaitingForTrezorToConnect, + FollowHwDeviceInstructions, +} + +impl InitTokenInitialStatus for InitTokenInProgressStatus { + fn initial_status() -> Self { InitTokenInProgressStatus::ActivatingCoin } +} + +pub(crate) fn token_xpub_extractor_rpc_statuses( +) -> HwConnectStatuses { + HwConnectStatuses { + on_connect: InitTokenInProgressStatus::WaitingForTrezorToConnect, + on_connected: InitTokenInProgressStatus::ActivatingCoin, + on_connection_failed: InitTokenInProgressStatus::Finishing, + on_button_request: InitTokenInProgressStatus::FollowHwDeviceInstructions, + on_pin_request: InitTokenAwaitingStatus::EnterTrezorPin, + on_passphrase_request: InitTokenAwaitingStatus::EnterTrezorPassphrase, + on_ready: InitTokenInProgressStatus::ActivatingCoin, + } +} + +#[derive(Clone, Debug, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum InitTokenError { + #[display(fmt = "No such task '{}'", _0)] + NoSuchTask(TaskId), + #[display(fmt = "Initialization task has timed out {:?}", duration)] + TaskTimedOut { duration: Duration }, + #[display(fmt = "Token {} is activated already", ticker)] + TokenIsAlreadyActivated { ticker: String }, + #[display(fmt = "Token {} config is not found", _0)] + TokenConfigIsNotFound(String), + #[display(fmt = "Token {} protocol parsing failed: {}", ticker, error)] + TokenProtocolParseError { ticker: String, error: String }, + #[display(fmt = "Unexpected platform protocol {:?} for {}", protocol, ticker)] + UnexpectedTokenProtocol { ticker: String, protocol: CoinProtocol }, + #[display(fmt = "Error on platform coin {} creation: {}", ticker, error)] + TokenCreationError { ticker: String, error: String }, + #[display(fmt = "Could not fetch balance: {}", _0)] + CouldNotFetchBalance(String), + #[display(fmt = "Platform coin {} is not activated", _0)] + PlatformCoinIsNotActivated(String), + #[display(fmt = "{} is not a platform coin for token {}", platform_coin_ticker, token_ticker)] + UnsupportedPlatformCoin { + platform_coin_ticker: String, + token_ticker: String, + }, + #[display(fmt = "{}", _0)] + HwError(HwRpcError), + #[display(fmt = "Transport error: {}", _0)] + Transport(String), + #[display(fmt = "Internal error: {}", _0)] + Internal(String), +} + +impl From for InitTokenError { + fn from(e: CoinConfWithProtocolError) -> Self { + match e { + CoinConfWithProtocolError::ConfigIsNotFound(error) => InitTokenError::TokenConfigIsNotFound(error), + CoinConfWithProtocolError::CoinProtocolParseError { ticker, err } => { + InitTokenError::TokenProtocolParseError { + ticker, + error: err.to_string(), + } + }, + CoinConfWithProtocolError::UnexpectedProtocol { ticker, protocol } => { + InitTokenError::UnexpectedTokenProtocol { ticker, protocol } + }, + } + } +} + +impl From for InitTokenError { + fn from(e: RpcTaskError) -> Self { + match e { + RpcTaskError::NoSuchTask(task_id) => InitTokenError::NoSuchTask(task_id), + RpcTaskError::Timeout(duration) => InitTokenError::TaskTimedOut { duration }, + rpc_internal => InitTokenError::Internal(rpc_internal.to_string()), + } + } +} + +impl HttpStatusCode for InitTokenError { + fn status_code(&self) -> StatusCode { + match self { + InitTokenError::NoSuchTask(_) + | InitTokenError::TokenIsAlreadyActivated { .. } + | InitTokenError::TokenConfigIsNotFound { .. } + | InitTokenError::TokenProtocolParseError { .. } + | InitTokenError::UnexpectedTokenProtocol { .. } + | InitTokenError::TokenCreationError { .. } + | InitTokenError::PlatformCoinIsNotActivated(_) => StatusCode::BAD_REQUEST, + InitTokenError::TaskTimedOut { .. } => StatusCode::REQUEST_TIMEOUT, + InitTokenError::HwError(_) => StatusCode::GONE, + InitTokenError::CouldNotFetchBalance(_) + | InitTokenError::UnsupportedPlatformCoin { .. } + | InitTokenError::Transport(_) + | InitTokenError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} diff --git a/mm2src/coins_activation/src/lib.rs b/mm2src/coins_activation/src/lib.rs index f52bf2f3a7..a1cc200c57 100644 --- a/mm2src/coins_activation/src/lib.rs +++ b/mm2src/coins_activation/src/lib.rs @@ -2,6 +2,8 @@ mod bch_with_tokens_activation; mod context; mod erc20_token_activation; mod eth_with_token_activation; +mod init_erc20_token_activation; +mod init_token; mod l2; #[cfg(not(target_arch = "wasm32"))] mod lightning_activation; mod platform_coin_with_tokens; @@ -30,8 +32,12 @@ mod utxo_activation; pub use utxo_activation::for_tests; mod z_coin_activation; +pub use init_token::{cancel_init_token, init_token, init_token_status, init_token_user_action}; pub use l2::{cancel_init_l2, init_l2, init_l2_status, init_l2_user_action}; -pub use platform_coin_with_tokens::enable_platform_coin_with_tokens; +pub use platform_coin_with_tokens::for_tests as platform_for_tests; +pub use platform_coin_with_tokens::{cancel_init_platform_coin_with_tokens, enable_platform_coin_with_tokens, + init_platform_coin_with_tokens, init_platform_coin_with_tokens_status, + init_platform_coin_with_tokens_user_action}; pub use standalone_coin::{cancel_init_standalone_coin, init_standalone_coin, init_standalone_coin_status, init_standalone_coin_user_action, InitStandaloneCoinReq, InitStandaloneCoinStatusRequest}; pub use token::enable_token; diff --git a/mm2src/coins_activation/src/platform_coin_with_tokens.rs b/mm2src/coins_activation/src/platform_coin_with_tokens.rs index c20dccf302..051dd22fc3 100644 --- a/mm2src/coins_activation/src/platform_coin_with_tokens.rs +++ b/mm2src/coins_activation/src/platform_coin_with_tokens.rs @@ -1,19 +1,40 @@ +use std::time::Duration; + +use crate::context::CoinsActivationContext; use crate::prelude::*; use async_trait::async_trait; use coins::my_tx_history_v2::TxHistoryStorage; use coins::tx_history_storage::{CreateTxHistoryStorageError, TxHistoryStorageBuilder}; -use coins::{lp_coinfind_any, CoinProtocol, CoinsContext, MmCoin, MmCoinEnum, PrivKeyPolicyNotAllowed}; -use common::{log, HttpStatusCode, StatusCode}; +use coins::{lp_coinfind, lp_coinfind_any, CoinProtocol, CoinsContext, MmCoinEnum, PrivKeyPolicyNotAllowed, + UnexpectedDerivationMethod}; +use common::{log, HttpStatusCode, StatusCode, SuccessResponse}; +use crypto::hw_rpc_task::{HwConnectStatuses, HwRpcTaskAwaitingStatus, HwRpcTaskUserAction}; use crypto::CryptoCtxError; use derive_more::Display; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_event_stream::EventStreamConfiguration; use mm2_number::BigDecimal; +use rpc_task::rpc_common::{CancelRpcTaskError, CancelRpcTaskRequest, InitRpcTaskResponse, RpcTaskStatusError, + RpcTaskStatusRequest, RpcTaskUserActionError, RpcTaskUserActionRequest}; +use rpc_task::{RpcTask, RpcTaskError, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, + RpcTaskTypes, TaskId}; use ser_error_derive::SerializeErrorType; use serde_derive::{Deserialize, Serialize}; use serde_json::Value as Json; +pub type InitPlatformCoinWithTokensStatusError = RpcTaskStatusError; +pub type InitPlatformCoinWithTokensUserActionError = RpcTaskUserActionError; +pub type CancelInitPlatformCoinWithTokensError = CancelRpcTaskError; + +pub type InitPlatformCoinWithTokensAwaitingStatus = HwRpcTaskAwaitingStatus; +pub type InitPlatformCoinWithTokensUserAction = HwRpcTaskUserAction; +pub type EnablePlatformCoinWithTokensResponse = InitRpcTaskResponse; +pub type EnablePlatformCoinWithTokensStatusRequest = RpcTaskStatusRequest; +pub type InitPlatformCoinWithTokensUserActionRequest = RpcTaskUserActionRequest; +pub type InitPlatformCoinWithTokensTaskManagerShared = + RpcTaskManagerShared>; + #[derive(Clone, Debug, Deserialize)] pub struct TokenActivationRequest { ticker: String, @@ -22,7 +43,10 @@ pub struct TokenActivationRequest { } pub trait TokenOf: Into { - type PlatformCoin: TryPlatformCoinFromMmCoinEnum + PlatformWithTokensActivationOps + RegisterTokenInfo + Clone; + type PlatformCoin: TryPlatformCoinFromMmCoinEnum + + PlatformCoinWithTokensActivationOps + + RegisterTokenInfo + + Clone; } pub struct TokenActivationParams { @@ -39,7 +63,7 @@ pub trait TokenInitializer { type InitTokensError: NotMmError; fn tokens_requests_from_platform_request( - platform_request: &<::PlatformCoin as PlatformWithTokensActivationOps>::ActivationRequest, + platform_request: &<::PlatformCoin as PlatformCoinWithTokensActivationOps>::ActivationRequest, ) -> Vec>; async fn enable_tokens( @@ -63,8 +87,10 @@ pub trait TokenAsMmCoinInitializer: Send + Sync { } pub enum InitTokensAsMmCoinsError { + TokenAlreadyActivated(String), TokenConfigIsNotFound(String), CouldNotFetchBalance(String), + UnexpectedDerivationMethod(UnexpectedDerivationMethod), Internal(String), TokenProtocolParseError { ticker: String, error: String }, UnexpectedTokenProtocol { ticker: String, protocol: CoinProtocol }, @@ -101,7 +127,7 @@ where (T::InitTokensError, InitTokensAsMmCoinsError): NotEqual, { type PlatformCoin = ::PlatformCoin; - type ActivationRequest = ::ActivationRequest; + type ActivationRequest = ::ActivationRequest; async fn enable_tokens_as_mm_coins( &self, @@ -134,11 +160,21 @@ pub trait GetPlatformBalance { } #[async_trait] -pub trait PlatformWithTokensActivationOps: Into { - type ActivationRequest: Clone + Send + Sync + TxHistory; - type PlatformProtocolInfo: TryFromCoinProtocol; - type ActivationResult: GetPlatformBalance + CurrentBlock; - type ActivationError: NotMmError + std::fmt::Debug; +pub trait PlatformCoinWithTokensActivationOps: Into + Clone + Send + Sync + 'static { + type ActivationRequest: Clone + Send + Sync + TxHistory + ActivationRequestInfo; + type PlatformProtocolInfo: TryFromCoinProtocol + Send; + type ActivationResult: GetPlatformBalance + CurrentBlock + serde::Serialize + Send + Clone + Sync + 'static; + type ActivationError: NotMmError + + std::fmt::Debug + + NotEqual + + Into + + Clone + + Send + + Sync; + + type InProgressStatus: InitPlatformCoinWithTokensInitialStatus + Clone + Send + Sync; + type AwaitingStatus: Clone + Send + Sync; + type UserAction: NotMmError + Send + Sync; /// Initializes the platform coin itself async fn enable_platform_coin( @@ -164,9 +200,12 @@ pub trait PlatformWithTokensActivationOps: Into { async fn get_activation_result( &self, + task_handle: Option>>, activation_request: &Self::ActivationRequest, nft_global: &Option, - ) -> Result>; + ) -> Result> + where + EnablePlatformCoinWithTokensError: From; fn start_history_background_fetching( &self, @@ -179,19 +218,24 @@ pub trait PlatformWithTokensActivationOps: Into { &self, config: &EventStreamConfiguration, ) -> Result<(), MmError>; + + fn rpc_task_manager(activation_ctx: &CoinsActivationContext) -> &InitPlatformCoinWithTokensTaskManagerShared + where + EnablePlatformCoinWithTokensError: From; } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Clone)] pub struct EnablePlatformCoinWithTokensReq { ticker: String, #[serde(flatten)] request: T, } -#[derive(Debug, Display, Serialize, SerializeErrorType)] +#[derive(Debug, Display, Serialize, SerializeErrorType, Clone)] #[serde(tag = "error_type", content = "error_data")] pub enum EnablePlatformCoinWithTokensError { PlatformIsAlreadyActivated(String), + TokenIsAlreadyActivated(String), #[display(fmt = "Platform {} config is not found", _0)] PlatformConfigIsNotFound(String), #[display(fmt = "Platform coin {} protocol parsing failed: {}", ticker, error)] @@ -231,6 +275,14 @@ pub enum EnablePlatformCoinWithTokensError { #[display(fmt = "Failed spawning balance events. Error: {_0}")] FailedSpawningBalanceEvents(String), Internal(String), + #[display(fmt = "No such task '{}'", _0)] + NoSuchTask(TaskId), + #[display(fmt = "Initialization task has timed out {:?}", duration)] + TaskTimedOut { + duration: Duration, + }, + #[display(fmt = "Hardware policy must be activated within task manager")] + UnexpectedDeviceActivationPolicy, } impl From for EnablePlatformCoinWithTokensError { @@ -255,6 +307,9 @@ impl From for EnablePlatformCoinWithTokensError { impl From for EnablePlatformCoinWithTokensError { fn from(err: InitTokensAsMmCoinsError) -> Self { match err { + InitTokensAsMmCoinsError::TokenAlreadyActivated(ticker) => { + EnablePlatformCoinWithTokensError::TokenIsAlreadyActivated(ticker) + }, InitTokensAsMmCoinsError::TokenConfigIsNotFound(ticker) => { EnablePlatformCoinWithTokensError::TokenConfigIsNotFound(ticker) }, @@ -269,6 +324,9 @@ impl From for EnablePlatformCoinWithTokensError { EnablePlatformCoinWithTokensError::Transport(e) }, InitTokensAsMmCoinsError::InvalidPayload(e) => EnablePlatformCoinWithTokensError::InvalidPayload(e), + InitTokensAsMmCoinsError::UnexpectedDerivationMethod(e) => { + EnablePlatformCoinWithTokensError::UnexpectedDerivationMethod(e.to_string()) + }, } } } @@ -285,6 +343,16 @@ impl From for EnablePlatformCoinWithTokensError { fn from(e: CryptoCtxError) -> Self { EnablePlatformCoinWithTokensError::Internal(e.to_string()) } } +impl From for EnablePlatformCoinWithTokensError { + fn from(e: RpcTaskError) -> Self { + match e { + RpcTaskError::NoSuchTask(task_id) => EnablePlatformCoinWithTokensError::NoSuchTask(task_id), + RpcTaskError::Timeout(duration) => EnablePlatformCoinWithTokensError::TaskTimedOut { duration }, + rpc_internal => EnablePlatformCoinWithTokensError::Internal(rpc_internal.to_string()), + } + } +} + impl HttpStatusCode for EnablePlatformCoinWithTokensError { fn status_code(&self) -> StatusCode { match self { @@ -293,13 +361,17 @@ impl HttpStatusCode for EnablePlatformCoinWithTokensError { | EnablePlatformCoinWithTokensError::PlatformCoinCreationError { .. } | EnablePlatformCoinWithTokensError::PrivKeyPolicyNotAllowed(_) | EnablePlatformCoinWithTokensError::UnexpectedDerivationMethod(_) - | EnablePlatformCoinWithTokensError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + | EnablePlatformCoinWithTokensError::Internal(_) + | EnablePlatformCoinWithTokensError::TaskTimedOut { .. } => StatusCode::INTERNAL_SERVER_ERROR, EnablePlatformCoinWithTokensError::PlatformIsAlreadyActivated(_) + | EnablePlatformCoinWithTokensError::TokenIsAlreadyActivated(_) | EnablePlatformCoinWithTokensError::PlatformConfigIsNotFound(_) | EnablePlatformCoinWithTokensError::TokenConfigIsNotFound(_) | EnablePlatformCoinWithTokensError::UnexpectedPlatformProtocol { .. } | EnablePlatformCoinWithTokensError::InvalidPayload { .. } | EnablePlatformCoinWithTokensError::AtLeastOneNodeRequired(_) + | EnablePlatformCoinWithTokensError::NoSuchTask(_) + | EnablePlatformCoinWithTokensError::UnexpectedDeviceActivationPolicy | EnablePlatformCoinWithTokensError::FailedSpawningBalanceEvents(_) | EnablePlatformCoinWithTokensError::UnexpectedTokenProtocol { .. } => StatusCode::BAD_REQUEST, EnablePlatformCoinWithTokensError::Transport(_) => StatusCode::BAD_GATEWAY, @@ -310,10 +382,11 @@ impl HttpStatusCode for EnablePlatformCoinWithTokensError { pub async fn re_enable_passive_platform_coin_with_tokens( ctx: MmArc, platform_coin: Platform, + task_handle: Option>>, req: EnablePlatformCoinWithTokensReq, ) -> Result> where - Platform: PlatformWithTokensActivationOps + MmCoin + Clone, + Platform: PlatformCoinWithTokensActivationOps + Clone, EnablePlatformCoinWithTokensError: From, (Platform::ActivationError, EnablePlatformCoinWithTokensError): NotEqual, { @@ -325,7 +398,9 @@ where let nft_global = platform_coin.enable_global_nft(&req.request).await?; - let activation_result = platform_coin.get_activation_result(&req.request, &nft_global).await?; + let activation_result = platform_coin + .get_activation_result(task_handle, &req.request, &nft_global) + .await?; log::info!("{} current block {}", req.ticker, activation_result.current_block()); let coins_ctx = CoinsContext::from_ctx(&ctx).unwrap(); @@ -342,14 +417,30 @@ pub async fn enable_platform_coin_with_tokens( req: EnablePlatformCoinWithTokensReq, ) -> Result> where - Platform: PlatformWithTokensActivationOps + MmCoin + Clone, + Platform: PlatformCoinWithTokensActivationOps, + EnablePlatformCoinWithTokensError: From, + (Platform::ActivationError, EnablePlatformCoinWithTokensError): NotEqual, +{ + if req.request.is_hw_policy() { + return MmError::err(EnablePlatformCoinWithTokensError::UnexpectedDeviceActivationPolicy); + } + enable_platform_coin_with_tokens_impl::(ctx, None, req).await +} + +pub async fn enable_platform_coin_with_tokens_impl( + ctx: MmArc, + task_handle: Option>>, + req: EnablePlatformCoinWithTokensReq, +) -> Result> +where + Platform: PlatformCoinWithTokensActivationOps + Clone, EnablePlatformCoinWithTokensError: From, (Platform::ActivationError, EnablePlatformCoinWithTokensError): NotEqual, { if let Ok(Some(coin)) = lp_coinfind_any(&ctx, &req.ticker).await { if !coin.is_available() { if let Some(platform_coin) = Platform::try_from_mm_coin(coin.inner) { - return re_enable_passive_platform_coin_with_tokens(ctx, platform_coin, req).await; + return re_enable_passive_platform_coin_with_tokens(ctx, platform_coin, task_handle, req).await; } } @@ -377,7 +468,9 @@ where let nft_global = platform_coin.enable_global_nft(&req.request).await?; - let activation_result = platform_coin.get_activation_result(&req.request, &nft_global).await?; + let activation_result = platform_coin + .get_activation_result(task_handle, &req.request, &nft_global) + .await?; log::info!("{} current block {}", req.ticker, activation_result.current_block()); if req.request.tx_history() { @@ -400,3 +493,214 @@ where Ok(activation_result) } + +/// A struct that contains the info needed by the task that initializes a platform coin with tokens. +pub struct InitPlatformCoinWithTokensTask { + ctx: MmArc, + request: EnablePlatformCoinWithTokensReq, +} + +impl RpcTaskTypes for InitPlatformCoinWithTokensTask { + type Item = Platform::ActivationResult; + // Using real type here because enable_platform_coin_with_tokens_impl fn, which implements RpcTask::run common logic, creates such errors + type Error = EnablePlatformCoinWithTokensError; + type InProgressStatus = Platform::InProgressStatus; + type AwaitingStatus = Platform::AwaitingStatus; + type UserAction = Platform::UserAction; +} + +#[async_trait] +impl RpcTask for InitPlatformCoinWithTokensTask +where + Platform: PlatformCoinWithTokensActivationOps + Clone, + EnablePlatformCoinWithTokensError: From<::ActivationError>, +{ + fn initial_status(&self) -> Self::InProgressStatus { + ::initial_status() + } + + /// Try to disable the coin in case if we managed to register it already. + async fn cancel(self) {} + + async fn run(&mut self, task_handle: RpcTaskHandleShared) -> Result> { + enable_platform_coin_with_tokens_impl::(self.ctx.clone(), Some(task_handle), self.request.clone()) + .await + } +} + +/// Trait for the initial status of the task that initializes a platform coin with tokens. +pub trait InitPlatformCoinWithTokensInitialStatus { + fn initial_status() -> Self; +} + +/// The status of the task that initializes a platform coin with tokens. +#[derive(Clone, Serialize)] +pub enum InitPlatformCoinWithTokensInProgressStatus { + ActivatingCoin, + SyncingBlockHeaders { + current_scanned_block: u64, + last_block: u64, + }, + TemporaryError(String), + RequestingWalletBalance, + Finishing, + /// This status doesn't require the user to send `UserAction`, + /// but it tells the user that he should confirm/decline an address on his device. + WaitingForTrezorToConnect, + FollowHwDeviceInstructions, +} + +impl InitPlatformCoinWithTokensInitialStatus for InitPlatformCoinWithTokensInProgressStatus { + fn initial_status() -> Self { InitPlatformCoinWithTokensInProgressStatus::ActivatingCoin } +} + +/// Implementation of the init platform coin with tokens RPC command. +pub async fn init_platform_coin_with_tokens( + ctx: MmArc, + request: EnablePlatformCoinWithTokensReq, +) -> MmResult +where + Platform: PlatformCoinWithTokensActivationOps + Send + Sync + 'static + Clone, + Platform::InProgressStatus: InitPlatformCoinWithTokensInitialStatus, + EnablePlatformCoinWithTokensError: From, + (Platform::ActivationError, EnablePlatformCoinWithTokensError): NotEqual, +{ + if let Ok(Some(_)) = lp_coinfind(&ctx, &request.ticker).await { + return MmError::err(EnablePlatformCoinWithTokensError::PlatformIsAlreadyActivated( + request.ticker, + )); + } + + let coins_act_ctx = + CoinsActivationContext::from_ctx(&ctx).map_to_mm(EnablePlatformCoinWithTokensError::Internal)?; + let spawner = ctx.spawner(); + let task = InitPlatformCoinWithTokensTask:: { ctx, request }; + let task_manager = Platform::rpc_task_manager(&coins_act_ctx); + + let task_id = RpcTaskManager::spawn_rpc_task(task_manager, &spawner, task) + .mm_err(|e| EnablePlatformCoinWithTokensError::Internal(e.to_string()))?; + + Ok(EnablePlatformCoinWithTokensResponse { task_id }) +} + +/// Implementation of the init platform coin with tokens status RPC command. +pub async fn init_platform_coin_with_tokens_status( + ctx: MmArc, + req: EnablePlatformCoinWithTokensStatusRequest, +) -> MmResult< + RpcTaskStatus< + Platform::ActivationResult, + EnablePlatformCoinWithTokensError, + Platform::InProgressStatus, + Platform::AwaitingStatus, + >, + InitPlatformCoinWithTokensStatusError, +> +where + EnablePlatformCoinWithTokensError: From, +{ + let coins_act_ctx = + CoinsActivationContext::from_ctx(&ctx).map_to_mm(InitPlatformCoinWithTokensStatusError::Internal)?; + let mut task_manager = Platform::rpc_task_manager(&coins_act_ctx) + .lock() + .map_to_mm(|poison| InitPlatformCoinWithTokensStatusError::Internal(poison.to_string()))?; + task_manager + .task_status(req.task_id, req.forget_if_finished) + .or_mm_err(|| InitPlatformCoinWithTokensStatusError::NoSuchTask(req.task_id)) + .map(|rpc_task| rpc_task.map_err(|e| e)) +} + +/// Implementation of the init platform coin with tokens user action RPC command. +pub async fn init_platform_coin_with_tokens_user_action( + ctx: MmArc, + req: InitPlatformCoinWithTokensUserActionRequest, +) -> MmResult +where + EnablePlatformCoinWithTokensError: From, +{ + let coins_act_ctx = + CoinsActivationContext::from_ctx(&ctx).map_to_mm(InitPlatformCoinWithTokensUserActionError::Internal)?; + let mut task_manager = Platform::rpc_task_manager(&coins_act_ctx) + .lock() + .map_to_mm(|poison| InitPlatformCoinWithTokensUserActionError::Internal(poison.to_string()))?; + task_manager.on_user_action(req.task_id, req.user_action)?; + Ok(SuccessResponse::new()) +} + +/// Implementation of the cancel init platform coin with tokens RPC command. +pub async fn cancel_init_platform_coin_with_tokens( + ctx: MmArc, + req: CancelRpcTaskRequest, +) -> MmResult +where + EnablePlatformCoinWithTokensError: From, +{ + let coins_act_ctx = + CoinsActivationContext::from_ctx(&ctx).map_to_mm(CancelInitPlatformCoinWithTokensError::Internal)?; + let mut task_manager = Platform::rpc_task_manager(&coins_act_ctx) + .lock() + .map_to_mm(|poison| CancelInitPlatformCoinWithTokensError::Internal(poison.to_string()))?; + task_manager.cancel_task(req.task_id)?; + Ok(SuccessResponse::new()) +} + +pub(crate) fn platform_coin_xpub_extractor_rpc_statuses( +) -> HwConnectStatuses { + HwConnectStatuses { + on_connect: InitPlatformCoinWithTokensInProgressStatus::WaitingForTrezorToConnect, + on_connected: InitPlatformCoinWithTokensInProgressStatus::ActivatingCoin, + on_connection_failed: InitPlatformCoinWithTokensInProgressStatus::Finishing, + on_button_request: InitPlatformCoinWithTokensInProgressStatus::FollowHwDeviceInstructions, + on_pin_request: InitPlatformCoinWithTokensAwaitingStatus::EnterTrezorPin, + on_passphrase_request: InitPlatformCoinWithTokensAwaitingStatus::EnterTrezorPassphrase, + on_ready: InitPlatformCoinWithTokensInProgressStatus::ActivatingCoin, + } +} + +pub mod for_tests { + use common::{executor::Timer, now_ms, wait_until_ms}; + use mm2_core::mm_ctx::MmArc; + use mm2_err_handle::prelude::MmResult; + use rpc_task::RpcTaskStatus; + + use super::{init_platform_coin_with_tokens, init_platform_coin_with_tokens_status, + EnablePlatformCoinWithTokensError, EnablePlatformCoinWithTokensReq, + EnablePlatformCoinWithTokensStatusRequest, InitPlatformCoinWithTokensInitialStatus, NotEqual, + PlatformCoinWithTokensActivationOps}; + + /// test helper to activate platform coin with waiting for the result + pub async fn init_platform_coin_with_tokens_loop( + ctx: MmArc, + request: EnablePlatformCoinWithTokensReq, + ) -> MmResult + where + Platform: PlatformCoinWithTokensActivationOps + Clone + Send + Sync + 'static, + Platform::InProgressStatus: InitPlatformCoinWithTokensInitialStatus, + EnablePlatformCoinWithTokensError: From, + (Platform::ActivationError, EnablePlatformCoinWithTokensError): NotEqual, + { + let init_result = init_platform_coin_with_tokens::(ctx.clone(), request) + .await + .unwrap(); + let timeout = wait_until_ms(150000); + loop { + if now_ms() > timeout { + panic!("init_standalone_coin timed out"); + } + let status_req = EnablePlatformCoinWithTokensStatusRequest { + task_id: init_result.task_id, + forget_if_finished: true, + }; + let status_res = init_platform_coin_with_tokens_status::(ctx.clone(), status_req).await; + if let Ok(status) = status_res { + match status { + RpcTaskStatus::Ok(result) => break Ok(result), + RpcTaskStatus::Error(e) => break Err(e), + _ => Timer::sleep(1.).await, + } + } else { + panic!("could not get init_standalone_coin status"); + } + } + } +} diff --git a/mm2src/coins_activation/src/prelude.rs b/mm2src/coins_activation/src/prelude.rs index 967a4cae68..bcb4137d8f 100644 --- a/mm2src/coins_activation/src/prelude.rs +++ b/mm2src/coins_activation/src/prelude.rs @@ -1,7 +1,7 @@ use coins::nft::nft_structs::{Chain, ConvertChain}; use coins::utxo::UtxoActivationParams; use coins::z_coin::ZcoinActivationParams; -use coins::{coin_conf, CoinBalance, CoinProtocol, MmCoinEnum}; +use coins::{coin_conf, CoinBalance, CoinProtocol, DerivationMethodResponse, MmCoinEnum}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::BigDecimal; @@ -29,19 +29,9 @@ pub trait GetAddressesBalances { fn get_addresses_balances(&self) -> HashMap; } -#[derive(Clone, Debug, Serialize)] -#[serde(tag = "type", content = "data")] -pub enum DerivationMethod { - /// Legacy iguana's privkey derivation, used by default - Iguana, - /// HD wallet derivation path, String is temporary here - #[allow(dead_code)] - HDWallet(String), -} - #[derive(Clone, Debug, Serialize)] pub struct CoinAddressInfo { - pub(crate) derivation_method: DerivationMethod, + pub(crate) derivation_method: DerivationMethodResponse, pub(crate) pubkey: String, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) balances: Option, @@ -110,3 +100,18 @@ pub fn coin_conf_with_protocol( })?; Ok((conf, protocol)) } + +/// A trait to be implemented for coin activation requests to determine some information about the request. +pub trait ActivationRequestInfo { + /// Checks if the activation request is for a hardware wallet. + fn is_hw_policy(&self) -> bool; +} + +impl ActivationRequestInfo for UtxoActivationParams { + fn is_hw_policy(&self) -> bool { self.priv_key_policy.is_hw_policy() } +} + +#[cfg(not(target_arch = "wasm32"))] +impl ActivationRequestInfo for ZcoinActivationParams { + fn is_hw_policy(&self) -> bool { false } // TODO: fix when device policy is added +} diff --git a/mm2src/coins_activation/src/solana_with_tokens_activation.rs b/mm2src/coins_activation/src/solana_with_tokens_activation.rs index 2ba63839da..f411995779 100644 --- a/mm2src/coins_activation/src/solana_with_tokens_activation.rs +++ b/mm2src/coins_activation/src/solana_with_tokens_activation.rs @@ -1,7 +1,11 @@ +use crate::context::CoinsActivationContext; use crate::platform_coin_with_tokens::{EnablePlatformCoinWithTokensError, GetPlatformBalance, - InitTokensAsMmCoinsError, PlatformWithTokensActivationOps, RegisterTokenInfo, - TokenActivationParams, TokenActivationRequest, TokenAsMmCoinInitializer, - TokenInitializer, TokenOf}; + InitPlatformCoinWithTokensAwaitingStatus, + InitPlatformCoinWithTokensInProgressStatus, InitPlatformCoinWithTokensTask, + InitPlatformCoinWithTokensTaskManagerShared, + InitPlatformCoinWithTokensUserAction, InitTokensAsMmCoinsError, + PlatformCoinWithTokensActivationOps, RegisterTokenInfo, TokenActivationParams, + TokenActivationRequest, TokenAsMmCoinInitializer, TokenInitializer, TokenOf}; use crate::prelude::*; use crate::prelude::{CoinAddressInfo, TokenBalances, TryFromCoinProtocol, TxHistory}; use crate::spl_token_activation::SplActivationRequest; @@ -10,8 +14,8 @@ use coins::coin_errors::MyAddressError; use coins::my_tx_history_v2::TxHistoryStorage; use coins::solana::solana_coin_with_policy; use coins::solana::spl::{SplProtocolConf, SplTokenCreationError}; -use coins::{BalanceError, CoinBalance, CoinProtocol, MarketCoinOps, MmCoinEnum, PrivKeyBuildPolicy, - SolanaActivationParams, SolanaCoin, SplToken}; +use coins::{BalanceError, CoinBalance, CoinProtocol, DerivationMethodResponse, MarketCoinOps, MmCoinEnum, + PrivKeyBuildPolicy, SolanaActivationParams, SolanaCoin, SplToken}; use common::Future01CompatExt; use common::{drop_mutability, true_f}; use crypto::CryptoCtxError; @@ -20,6 +24,7 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_event_stream::EventStreamConfiguration; use mm2_number::BigDecimal; +use rpc_task::RpcTaskHandleShared; use serde_derive::{Deserialize, Serialize}; use serde_json::Value as Json; use std::collections::HashMap; @@ -90,7 +95,11 @@ impl TxHistory for SolanaWithTokensActivationRequest { fn tx_history(&self) -> bool { false } } -#[derive(Debug, Serialize)] +impl ActivationRequestInfo for SolanaWithTokensActivationRequest { + fn is_hw_policy(&self) -> bool { false } // TODO: fix when device policy is added +} + +#[derive(Debug, Serialize, Clone)] pub struct SolanaWithTokensActivationResult { current_block: u64, solana_addresses_infos: HashMap>, @@ -111,7 +120,7 @@ impl CurrentBlock for SolanaWithTokensActivationResult { fn current_block(&self) -> u64 { self.current_block } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum SolanaWithTokensActivationError { PlatformCoinCreationError { ticker: String, error: String }, UnableToRetrieveMyAddress(String), @@ -177,12 +186,16 @@ impl From for InitTokensAsMmCoinsError { } #[async_trait] -impl PlatformWithTokensActivationOps for SolanaCoin { +impl PlatformCoinWithTokensActivationOps for SolanaCoin { type ActivationRequest = SolanaWithTokensActivationRequest; type PlatformProtocolInfo = SolanaProtocolInfo; type ActivationResult = SolanaWithTokensActivationResult; type ActivationError = SolanaWithTokensActivationError; + type InProgressStatus = InitPlatformCoinWithTokensInProgressStatus; + type AwaitingStatus = InitPlatformCoinWithTokensAwaitingStatus; + type UserAction = InitPlatformCoinWithTokensUserAction; + async fn enable_platform_coin( ctx: MmArc, ticker: String, @@ -229,6 +242,7 @@ impl PlatformWithTokensActivationOps for SolanaCoin { async fn get_activation_result( &self, + _task_handle: Option>>, activation_request: &Self::ActivationRequest, _nft_global: &Option, ) -> Result> { @@ -241,14 +255,14 @@ impl PlatformWithTokensActivationOps for SolanaCoin { let my_address = self.my_address()?; let mut solana_address_info = CoinAddressInfo { - derivation_method: DerivationMethod::Iguana, + derivation_method: DerivationMethodResponse::Iguana, pubkey: my_address.clone(), balances: None, tickers: None, }; let mut spl_address_info = CoinAddressInfo { - derivation_method: DerivationMethod::Iguana, + derivation_method: DerivationMethodResponse::Iguana, pubkey: my_address.clone(), balances: None, tickers: None, @@ -304,4 +318,10 @@ impl PlatformWithTokensActivationOps for SolanaCoin { ) -> Result<(), MmError> { Ok(()) } + + fn rpc_task_manager( + _activation_ctx: &CoinsActivationContext, + ) -> &InitPlatformCoinWithTokensTaskManagerShared { + unimplemented!() + } } diff --git a/mm2src/coins_activation/src/tendermint_with_assets_activation.rs b/mm2src/coins_activation/src/tendermint_with_assets_activation.rs index 01b944a0b8..521e6776d0 100644 --- a/mm2src/coins_activation/src/tendermint_with_assets_activation.rs +++ b/mm2src/coins_activation/src/tendermint_with_assets_activation.rs @@ -1,9 +1,14 @@ +use crate::context::CoinsActivationContext; use crate::platform_coin_with_tokens::{EnablePlatformCoinWithTokensError, GetPlatformBalance, - InitTokensAsMmCoinsError, PlatformWithTokensActivationOps, RegisterTokenInfo, - TokenActivationParams, TokenActivationRequest, TokenAsMmCoinInitializer, - TokenInitializer, TokenOf}; + InitPlatformCoinWithTokensAwaitingStatus, + InitPlatformCoinWithTokensInProgressStatus, InitPlatformCoinWithTokensTask, + InitPlatformCoinWithTokensTaskManagerShared, + InitPlatformCoinWithTokensUserAction, InitTokensAsMmCoinsError, + PlatformCoinWithTokensActivationOps, RegisterTokenInfo, TokenActivationParams, + TokenActivationRequest, TokenAsMmCoinInitializer, TokenInitializer, TokenOf}; use crate::prelude::*; use async_trait::async_trait; +use coins::hd_wallet::HDPathAccountToAddressId; use coins::my_tx_history_v2::TxHistoryStorage; use coins::tendermint::tendermint_tx_history_v2::tendermint_history_loop; use coins::tendermint::{tendermint_priv_key_policy, TendermintCoin, TendermintCommons, TendermintConf, @@ -12,12 +17,12 @@ use coins::tendermint::{tendermint_priv_key_policy, TendermintCoin, TendermintCo use coins::{CoinBalance, CoinProtocol, MarketCoinOps, MmCoin, MmCoinEnum, PrivKeyBuildPolicy}; use common::executor::{AbortSettings, SpawnAbortable}; use common::{true_f, Future01CompatExt}; -use crypto::StandardHDCoinAddress; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_event_stream::behaviour::{EventBehaviour, EventInitStatus}; use mm2_event_stream::EventStreamConfiguration; use mm2_number::BigDecimal; +use rpc_task::RpcTaskHandleShared; use serde::{Deserialize, Serialize}; use serde_json::Value as Json; use std::collections::{HashMap, HashSet}; @@ -42,13 +47,17 @@ pub struct TendermintActivationParams { pub get_balances: bool, /// /account'/change/address_index`. #[serde(default)] - pub path_to_address: StandardHDCoinAddress, + pub path_to_address: HDPathAccountToAddressId, } impl TxHistory for TendermintActivationParams { fn tx_history(&self) -> bool { self.tx_history } } +impl ActivationRequestInfo for TendermintActivationParams { + fn is_hw_policy(&self) -> bool { false } // TODO: fix when device policy is added +} + struct TendermintTokenInitializer { platform_coin: TendermintCoin, } @@ -128,7 +137,7 @@ impl From for InitTokensAsMmCoinsError { } } -#[derive(Serialize)] +#[derive(Serialize, Clone)] pub struct TendermintActivationResult { ticker: String, address: String, @@ -159,12 +168,16 @@ impl From for EnablePlatformCoinWithTokensError { } #[async_trait] -impl PlatformWithTokensActivationOps for TendermintCoin { +impl PlatformCoinWithTokensActivationOps for TendermintCoin { type ActivationRequest = TendermintActivationParams; type PlatformProtocolInfo = TendermintProtocolInfo; type ActivationResult = TendermintActivationResult; type ActivationError = TendermintInitError; + type InProgressStatus = InitPlatformCoinWithTokensInProgressStatus; + type AwaitingStatus = InitPlatformCoinWithTokensAwaitingStatus; + type UserAction = InitPlatformCoinWithTokensUserAction; + async fn enable_platform_coin( ctx: MmArc, ticker: String, @@ -226,6 +239,7 @@ impl PlatformWithTokensActivationOps for TendermintCoin { async fn get_activation_result( &self, + _task_handle: Option>>, activation_request: &Self::ActivationRequest, _nft_global: &Option, ) -> Result> { @@ -252,7 +266,7 @@ impl PlatformWithTokensActivationOps for TendermintCoin { }); } - let balances = self.all_balances().await.mm_err(|e| TendermintInitError { + let balances = self.get_all_balances().await.mm_err(|e| TendermintInitError { ticker: self.ticker().to_owned(), kind: TendermintInitErrorKind::RpcError(e.to_string()), })?; @@ -305,4 +319,10 @@ impl PlatformWithTokensActivationOps for TendermintCoin { } Ok(()) } + + fn rpc_task_manager( + _activation_ctx: &CoinsActivationContext, + ) -> &InitPlatformCoinWithTokensTaskManagerShared { + unimplemented!() + } } diff --git a/mm2src/coins_activation/src/utxo_activation/common_impl.rs b/mm2src/coins_activation/src/utxo_activation/common_impl.rs index 7f9a69faa5..8e3d071025 100644 --- a/mm2src/coins_activation/src/utxo_activation/common_impl.rs +++ b/mm2src/coins_activation/src/utxo_activation/common_impl.rs @@ -4,14 +4,14 @@ use crate::utxo_activation::init_utxo_standard_statuses::{UtxoStandardAwaitingSt UtxoStandardUserAction}; use crate::utxo_activation::utxo_standard_activation_result::UtxoStandardActivationResult; use coins::coin_balance::EnableCoinBalanceOps; -use coins::hd_pubkey::RpcTaskXPubExtractor; +use coins::hd_wallet::RpcTaskXPubExtractor; use coins::my_tx_history_v2::TxHistoryStorage; use coins::utxo::utxo_tx_history_v2::{utxo_history_loop, UtxoTxHistoryOps}; use coins::utxo::{UtxoActivationParams, UtxoCoinFields}; -use coins::{CoinFutSpawner, MarketCoinOps, PrivKeyActivationPolicy, PrivKeyBuildPolicy}; +use coins::{CoinBalance, CoinFutSpawner, MarketCoinOps, PrivKeyActivationPolicy, PrivKeyBuildPolicy}; use common::executor::{AbortSettings, SpawnAbortable}; use crypto::hw_rpc_task::HwConnectStatuses; -use crypto::CryptoCtxError; +use crypto::{CryptoCtxError, HwRpcError}; use futures::compat::Future01CompatExt; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; @@ -31,7 +31,7 @@ where InProgressStatus = UtxoStandardInProgressStatus, AwaitingStatus = UtxoStandardAwaitingStatus, UserAction = UtxoStandardUserAction, - > + EnableCoinBalanceOps + > + EnableCoinBalanceOps + MarketCoinOps, { let ticker = coin.ticker().to_owned(); @@ -41,13 +41,26 @@ where .await .map_to_mm(InitUtxoStandardError::Transport)?; - // Construct an Xpub extractor without checking if the MarketMaker supports HD wallet ops. - // [`EnableCoinBalanceOps::enable_coin_balance`] won't just use `xpub_extractor` - // if the coin has been initialized with an Iguana priv key. - let xpub_extractor = RpcTaskXPubExtractor::new_unchecked(ctx, task_handle.clone(), xpub_extractor_rpc_statuses()); + let xpub_extractor = if coin.is_trezor() { + Some( + RpcTaskXPubExtractor::new_trezor_extractor( + ctx, + task_handle.clone(), + xpub_extractor_rpc_statuses(), + coins::CoinProtocol::UTXO, + ) + .mm_err(|_| InitUtxoStandardError::HwError(HwRpcError::NotInitialized))?, + ) + } else { + None + }; task_handle.update_in_progress_status(UtxoStandardInProgressStatus::RequestingWalletBalance)?; let wallet_balance = coin - .enable_coin_balance(&xpub_extractor, activation_params.enable_params.clone()) + .enable_coin_balance( + xpub_extractor, + activation_params.enable_params.clone(), + &activation_params.path_to_address, + ) .await .mm_err(|enable_err| InitUtxoStandardError::from_enable_coin_balance_err(enable_err, ticker.clone()))?; task_handle.update_in_progress_status(UtxoStandardInProgressStatus::ActivatingCoin)?; @@ -60,8 +73,7 @@ where Ok(result) } -pub(crate) fn xpub_extractor_rpc_statuses( -) -> HwConnectStatuses { +fn xpub_extractor_rpc_statuses() -> HwConnectStatuses { HwConnectStatuses { on_connect: UtxoStandardInProgressStatus::WaitingForTrezorToConnect, on_connected: UtxoStandardInProgressStatus::ActivatingCoin, diff --git a/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_activation_error.rs b/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_activation_error.rs index 47894c2a6f..4cc7c7a5fd 100644 --- a/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_activation_error.rs +++ b/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_activation_error.rs @@ -1,6 +1,6 @@ use crate::standalone_coin::InitStandaloneCoinError; use coins::coin_balance::EnableCoinBalanceError; -use coins::hd_wallet::{NewAccountCreatingError, NewAddressDerivingError}; +use coins::hd_wallet::{NewAccountCreationError, NewAddressDerivingError}; use coins::tx_history_storage::CreateTxHistoryStorageError; use coins::utxo::utxo_builder::UtxoCoinBuildError; use coins::{BalanceError, RegisterCoinError}; @@ -83,7 +83,7 @@ impl InitUtxoStandardError { EnableCoinBalanceError::NewAddressDerivingError(addr) => { Self::from_new_address_deriving_error(addr, ticker) }, - EnableCoinBalanceError::NewAccountCreatingError(acc) => Self::from_new_account_err(acc, ticker), + EnableCoinBalanceError::NewAccountCreationError(acc) => Self::from_new_account_err(acc, ticker), EnableCoinBalanceError::BalanceError(balance) => Self::from_balance_err(balance, ticker), } } @@ -95,11 +95,11 @@ impl InitUtxoStandardError { } } - fn from_new_account_err(new_acc_err: NewAccountCreatingError, ticker: String) -> Self { + fn from_new_account_err(new_acc_err: NewAccountCreationError, ticker: String) -> Self { match new_acc_err { - NewAccountCreatingError::RpcTaskError(rpc) => Self::from(rpc), - NewAccountCreatingError::HardwareWalletError(hw_err) => Self::from_hw_err(hw_err, ticker), - NewAccountCreatingError::Internal(internal) => InitUtxoStandardError::Internal(internal), + NewAccountCreationError::RpcTaskError(rpc) => Self::from(rpc), + NewAccountCreationError::HardwareWalletError(hw_err) => Self::from_hw_err(hw_err, ticker), + NewAccountCreationError::Internal(internal) => InitUtxoStandardError::Internal(internal), other => InitUtxoStandardError::CoinCreationError { ticker, error: other.to_string(), diff --git a/mm2src/coins_activation/src/utxo_activation/utxo_standard_activation_result.rs b/mm2src/coins_activation/src/utxo_activation/utxo_standard_activation_result.rs index aa54ec3698..d9f67ae9b5 100644 --- a/mm2src/coins_activation/src/utxo_activation/utxo_standard_activation_result.rs +++ b/mm2src/coins_activation/src/utxo_activation/utxo_standard_activation_result.rs @@ -1,5 +1,6 @@ use crate::prelude::{CurrentBlock, GetAddressesBalances}; use coins::coin_balance::CoinBalanceReport; +use coins::CoinBalance; use mm2_number::BigDecimal; use serde_derive::Serialize; use std::collections::HashMap; @@ -8,7 +9,7 @@ use std::collections::HashMap; pub struct UtxoStandardActivationResult { pub ticker: String, pub current_block: u64, - pub wallet_balance: CoinBalanceReport, + pub wallet_balance: CoinBalanceReport, } impl CurrentBlock for UtxoStandardActivationResult { @@ -17,6 +18,10 @@ impl CurrentBlock for UtxoStandardActivationResult { impl GetAddressesBalances for UtxoStandardActivationResult { fn get_addresses_balances(&self) -> HashMap { - self.wallet_balance.to_addresses_total_balances() + self.wallet_balance + .to_addresses_total_balances(&self.ticker) + .into_iter() + .map(|(address, balance)| (address, balance.unwrap_or_default())) + .collect() } } diff --git a/mm2src/coins_activation/src/z_coin_activation.rs b/mm2src/coins_activation/src/z_coin_activation.rs index dc8c062a84..70da5c4eae 100644 --- a/mm2src/coins_activation/src/z_coin_activation.rs +++ b/mm2src/coins_activation/src/z_coin_activation.rs @@ -9,7 +9,7 @@ use coins::my_tx_history_v2::TxHistoryStorage; use coins::tx_history_storage::CreateTxHistoryStorageError; use coins::z_coin::{z_coin_from_conf_and_params, BlockchainScanStopped, FirstSyncBlock, SyncStatus, ZCoin, ZCoinBuildError, ZcoinActivationParams, ZcoinProtocolInfo}; -use coins::{BalanceError, CoinProtocol, MarketCoinOps, PrivKeyBuildPolicy, RegisterCoinError}; +use coins::{BalanceError, CoinBalance, CoinProtocol, MarketCoinOps, PrivKeyBuildPolicy, RegisterCoinError}; use crypto::hw_rpc_task::{HwRpcTaskAwaitingStatus, HwRpcTaskUserAction}; use crypto::CryptoCtxError; use derive_more::Display; @@ -43,7 +43,7 @@ pub type ZcoinUserAction = HwRpcTaskUserAction; pub struct ZcoinActivationResult { pub ticker: String, pub current_block: u64, - pub wallet_balance: CoinBalanceReport, + pub wallet_balance: CoinBalanceReport, pub first_sync_block: FirstSyncBlock, } @@ -53,7 +53,11 @@ impl CurrentBlock for ZcoinActivationResult { impl GetAddressesBalances for ZcoinActivationResult { fn get_addresses_balances(&self) -> HashMap { - self.wallet_balance.to_addresses_total_balances() + self.wallet_balance + .to_addresses_total_balances(&self.ticker) + .into_iter() + .map(|(address, balance)| (address, balance.unwrap_or_default())) + .collect() } } diff --git a/mm2src/crypto/src/global_hd_ctx.rs b/mm2src/crypto/src/global_hd_ctx.rs index 15cb2cffbb..ad7c2bc63b 100644 --- a/mm2src/crypto/src/global_hd_ctx.rs +++ b/mm2src/crypto/src/global_hd_ctx.rs @@ -1,8 +1,6 @@ use crate::privkey::{bip39_seed_from_passphrase, key_pair_from_secret, PrivKeyError}; -use crate::standard_hd_path::StandardHDCoinAddress; -use crate::{mm2_internal_der_path, Bip32DerPathOps, Bip32Error, CryptoInitError, CryptoInitResult, - StandardHDPathToCoin}; -use bip32::{ChildNumber, ExtendedPrivateKey}; +use crate::{mm2_internal_der_path, Bip32Error, CryptoInitError, CryptoInitResult}; +use bip32::{DerivationPath, ExtendedPrivateKey}; use common::drop_mutability; use keys::{KeyPair, Secret as Secp256k1Secret}; use mm2_err_handle::prelude::*; @@ -10,9 +8,6 @@ use std::ops::Deref; use std::sync::Arc; use zeroize::{Zeroize, ZeroizeOnDrop}; -const HARDENED: bool = true; -const NON_HARDENED: bool = false; - pub(super) type Mm2InternalKeyPair = KeyPair; #[derive(Clone)] @@ -76,27 +71,17 @@ impl GlobalHDAccountCtx { /// * `address_id = HDAccountCtx::hd_account`. /// /// Returns the `secp256k1::Private` Secret 256-bit key - pub fn derive_secp256k1_secret( - &self, - derivation_path: &StandardHDPathToCoin, - path_to_address: &StandardHDCoinAddress, - ) -> MmResult { - derive_secp256k1_secret(self.bip39_secp_priv_key.clone(), derivation_path, path_to_address) + pub fn derive_secp256k1_secret(&self, derivation_path: &DerivationPath) -> MmResult { + derive_secp256k1_secret(self.bip39_secp_priv_key.clone(), derivation_path) } } pub fn derive_secp256k1_secret( bip39_secp_priv_key: ExtendedPrivateKey, - derivation_path: &StandardHDPathToCoin, - path_to_address: &StandardHDCoinAddress, + derivation_path: &DerivationPath, ) -> MmResult { - let mut account_der_path = derivation_path.to_derivation_path(); - account_der_path.push(ChildNumber::new(path_to_address.account, HARDENED).unwrap()); - account_der_path.push(ChildNumber::new(path_to_address.is_change as u32, NON_HARDENED).unwrap()); - account_der_path.push(ChildNumber::new(path_to_address.address_index, NON_HARDENED).unwrap()); - let mut priv_key = bip39_secp_priv_key; - for child in account_der_path { + for child in derivation_path.iter() { priv_key = priv_key.derive_child(child)?; } drop_mutability!(priv_key); diff --git a/mm2src/crypto/src/hw_error.rs b/mm2src/crypto/src/hw_error.rs index e75cf30f81..6631de5204 100644 --- a/mm2src/crypto/src/hw_error.rs +++ b/mm2src/crypto/src/hw_error.rs @@ -91,7 +91,7 @@ impl From for HwError { /// so please extend it if it's required **only**. /// /// Please also note that this enum is fieldless. -#[derive(Clone, Debug, Display, Serialize, PartialEq)] +#[derive(Clone, Debug, Display, Serialize, PartialEq, Deserialize)] pub enum HwRpcError { #[display(fmt = "No Trezor device available")] NoTrezorDeviceAvailable = 0, diff --git a/mm2src/crypto/src/hw_rpc_task.rs b/mm2src/crypto/src/hw_rpc_task.rs index b64cd23a43..96b0c73c03 100644 --- a/mm2src/crypto/src/hw_rpc_task.rs +++ b/mm2src/crypto/src/hw_rpc_task.rs @@ -18,7 +18,7 @@ pub type HwRpcTaskUserActionRequest = RpcTaskUserActionRequest>>>>; #[rustfmt::skip] -pub type StandardHDPathToCoin = +pub type HDPathToCoin = Bip32Child>; #[rustfmt::skip] -pub type StandardHDPathToAccount = +pub type HDPathToAccount = Bip32Child u32 { self.child().child().child().child().value() } } -impl StandardHDPathToCoin { +impl HDPathToCoin { pub fn purpose(&self) -> Bip43Purpose { self.value() } pub fn coin_type(&self) -> u32 { self.child().value() } } -impl StandardHDPathToAccount { +impl HDPathToAccount { pub fn purpose(&self) -> Bip43Purpose { self.value() } pub fn coin_type(&self) -> u32 { self.child().value() } @@ -122,21 +122,6 @@ impl From for Bip32DerPathError { } } -/// A struct that represents a standard HD path from account to address. -/// -/// This is used in coins activation to specify the default address that will be used for swaps. -/// -/// # Attributes -/// * `account`: The account number of the address. -/// * `is_change`: A flag that indicates whether the address is a change address or not. -/// * `address_index`: The index of the address within the account. -#[derive(Clone, Debug, Default, Deserialize, Serialize)] -pub struct StandardHDCoinAddress { - pub account: u32, - pub is_change: bool, - pub address_index: u32, -} - #[derive(Clone, Copy, Debug, Eq, PartialEq, Primitive)] pub enum StandardHDIndex { Purpose = 0, @@ -259,15 +244,15 @@ mod tests { #[test] fn test_display() { - let der_path = StandardHDPathToAccount::from_str("m/44'/141'/1'").unwrap(); + let der_path = HDPathToAccount::from_str("m/44'/141'/1'").unwrap(); let actual = format!("{}", der_path); assert_eq!(actual, "m/44'/141'/1'"); } #[test] fn test_derive() { - let der_path_to_coin = StandardHDPathToCoin::from_str("m/44'/141'").unwrap(); - let der_path_to_account: StandardHDPathToAccount = + let der_path_to_coin = HDPathToCoin::from_str("m/44'/141'").unwrap(); + let der_path_to_account: HDPathToAccount = der_path_to_coin.derive(ChildNumber::new(10, true).unwrap()).unwrap(); assert_eq!( der_path_to_account.to_derivation_path(), @@ -293,7 +278,7 @@ mod tests { #[test] fn test_from_unexpected_child_value() { - let error = StandardHDPathToAccount::from_str("m/44'/141'/0").expect_err("'account_id' is not hardened"); + let error = HDPathToAccount::from_str("m/44'/141'/0").expect_err("'account_id' is not hardened"); assert_eq!(error, Bip32DerPathError::ChildIsNotHardened { child_at: 2 }); let error = StandardHDPathError::from(error); assert_eq!(error, StandardHDPathError::ChildIsNotHardened { diff --git a/mm2src/mm2_event_stream/src/behaviour.rs b/mm2src/mm2_event_stream/src/behaviour.rs index d09424dcdc..ff2cfbefa9 100644 --- a/mm2src/mm2_event_stream/src/behaviour.rs +++ b/mm2src/mm2_event_stream/src/behaviour.rs @@ -1,4 +1,4 @@ -use crate::EventStreamConfiguration; +use crate::{ErrorEventName, EventName, EventStreamConfiguration}; use async_trait::async_trait; use futures::channel::oneshot; @@ -11,11 +11,12 @@ pub enum EventInitStatus { #[async_trait] pub trait EventBehaviour { - /// Unique name of the event. - const EVENT_NAME: &'static str; + /// Returns the unique name of the event as an EventName enum variant. + fn event_name() -> EventName; - /// Name of the error event with default value "ERROR". - const ERROR_EVENT_NAME: &'static str = "ERROR"; + /// Returns the name of the error event as an ErrorEventName enum variant. + /// By default, it returns `ErrorEventName::GenericError,` which shows as "ERROR" in the event stream. + fn error_event_name() -> ErrorEventName { ErrorEventName::GenericError } /// Event handler that is responsible for broadcasting event data to the streaming channels. async fn handle(self, interval: f64, tx: oneshot::Sender); diff --git a/mm2src/mm2_event_stream/src/lib.rs b/mm2src/mm2_event_stream/src/lib.rs index 286867843d..2d0cb6bda0 100644 --- a/mm2src/mm2_event_stream/src/lib.rs +++ b/mm2src/mm2_event_stream/src/lib.rs @@ -1,5 +1,6 @@ use serde::Deserialize; use std::collections::HashMap; +use std::fmt; #[cfg(target_arch = "wasm32")] use std::path::PathBuf; #[cfg(target_arch = "wasm32")] @@ -30,6 +31,44 @@ impl Event { pub fn message(&self) -> &str { &self.message } } +/// Event types streamed to clients through channels like Server-Sent Events (SSE). +#[derive(Deserialize, Eq, Hash, PartialEq)] +pub enum EventName { + /// Indicates a change in the balance of a coin. + CoinBalance, + /// Event triggered at regular intervals to indicate that the system is operational. + HEARTBEAT, + /// Returns p2p network information at a regular interval. + NETWORK, +} + +impl fmt::Display for EventName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::CoinBalance => write!(f, "COIN_BALANCE"), + Self::HEARTBEAT => write!(f, "HEARTBEAT"), + Self::NETWORK => write!(f, "NETWORK"), + } + } +} + +/// Error event types used to indicate various kinds of errors to clients through channels like Server-Sent Events (SSE). +pub enum ErrorEventName { + /// A generic error that doesn't fit any other specific categories. + GenericError, + /// Signifies an error related to fetching or calculating the balance of a coin. + CoinBalanceError, +} + +impl fmt::Display for ErrorEventName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::GenericError => write!(f, "ERROR"), + Self::CoinBalanceError => write!(f, "COIN_BALANCE_ERROR"), + } + } +} + /// Configuration for event streaming #[derive(Deserialize)] pub struct EventStreamConfiguration { @@ -37,7 +76,7 @@ pub struct EventStreamConfiguration { #[serde(default)] pub access_control_allow_origin: String, #[serde(default)] - active_events: HashMap, + active_events: HashMap, /// The path to the worker script for event streaming. #[cfg(target_arch = "wasm32")] #[serde(default = "default_worker_path")] @@ -72,7 +111,9 @@ impl Default for EventStreamConfiguration { impl EventStreamConfiguration { /// Retrieves the configuration for a specific event by its name. #[inline] - pub fn get_event(&self, event_name: &str) -> Option { self.active_events.get(event_name).cloned() } + pub fn get_event(&self, event_name: &EventName) -> Option { + self.active_events.get(event_name).cloned() + } /// Gets the total number of active events in the configuration. #[inline] diff --git a/mm2src/mm2_main/Cargo.toml b/mm2src/mm2_main/Cargo.toml index 5244f03da4..c29f9bec2c 100644 --- a/mm2src/mm2_main/Cargo.toml +++ b/mm2src/mm2_main/Cargo.toml @@ -115,12 +115,13 @@ rcgen = "0.10" rustls = { version = "0.21", default-features = false } rustls-pemfile = "1.0.2" tokio = { version = "1.20", features = ["io-util", "rt-multi-thread", "net"] } -mm2_test_helpers = { path = "../mm2_test_helpers" } [target.'cfg(windows)'.dependencies] winapi = "0.3" [dev-dependencies] +coins = { path = "../coins", features = ["for-tests"] } +coins_activation = { path = "../coins_activation", features = ["for-tests"] } mm2_test_helpers = { path = "../mm2_test_helpers" } mocktopus = "0.8.0" testcontainers = "0.15.0" diff --git a/mm2src/mm2_main/src/heartbeat_event.rs b/mm2src/mm2_main/src/heartbeat_event.rs index 9b4cb0809a..6c4d19d77b 100644 --- a/mm2src/mm2_main/src/heartbeat_event.rs +++ b/mm2src/mm2_main/src/heartbeat_event.rs @@ -4,7 +4,7 @@ use common::{executor::{SpawnFuture, Timer}, use futures::channel::oneshot::{self, Receiver, Sender}; use mm2_core::mm_ctx::MmArc; use mm2_event_stream::{behaviour::{EventBehaviour, EventInitStatus}, - Event, EventStreamConfiguration}; + Event, EventName, EventStreamConfiguration}; pub struct HeartbeatEvent { ctx: MmArc, @@ -16,7 +16,7 @@ impl HeartbeatEvent { #[async_trait] impl EventBehaviour for HeartbeatEvent { - const EVENT_NAME: &'static str = "HEARTBEAT"; + fn event_name() -> EventName { EventName::HEARTBEAT } async fn handle(self, interval: f64, tx: oneshot::Sender) { tx.send(EventInitStatus::Success).unwrap(); @@ -24,7 +24,7 @@ impl EventBehaviour for HeartbeatEvent { loop { self.ctx .stream_channel_controller - .broadcast(Event::new(Self::EVENT_NAME.to_string(), json!({}).to_string())) + .broadcast(Event::new(Self::event_name().to_string(), json!({}).to_string())) .await; Timer::sleep(interval).await; @@ -32,10 +32,10 @@ impl EventBehaviour for HeartbeatEvent { } async fn spawn_if_active(self, config: &EventStreamConfiguration) -> EventInitStatus { - if let Some(event) = config.get_event(Self::EVENT_NAME) { + if let Some(event) = config.get_event(&Self::event_name()) { info!( "{} event is activated with {} seconds interval.", - Self::EVENT_NAME, + Self::event_name(), event.stream_interval_seconds ); diff --git a/mm2src/mm2_main/src/lp_swap.rs b/mm2src/mm2_main/src/lp_swap.rs index c4b7a405a0..2263b0051d 100644 --- a/mm2src/mm2_main/src/lp_swap.rs +++ b/mm2src/mm2_main/src/lp_swap.rs @@ -1835,13 +1835,13 @@ pub fn generate_secret() -> Result<[u8; 32], rand::Error> { mod lp_swap_tests { use super::*; use crate::mm2::lp_native_dex::{fix_directories, init_p2p}; + use coins::hd_wallet::HDPathAccountToAddressId; use coins::utxo::rpc_clients::ElectrumRpcRequest; use coins::utxo::utxo_standard::utxo_standard_coin_with_priv_key; use coins::utxo::{UtxoActivationParams, UtxoRpcMode}; use coins::MarketCoinOps; use coins::PrivKeyActivationPolicy; use common::{block_on, new_uuid}; - use crypto::StandardHDCoinAddress; use mm2_core::mm_ctx::MmCtxBuilder; use mm2_test_helpers::for_tests::{morty_conf, rick_conf, MORTY_ELECTRUM_ADDRS, RICK_ELECTRUM_ADDRS}; @@ -2234,7 +2234,7 @@ mod lp_swap_tests { enable_params: Default::default(), priv_key_policy: PrivKeyActivationPolicy::ContextPrivKey, check_utxo_maturity: None, - path_to_address: StandardHDCoinAddress::default(), + path_to_address: HDPathAccountToAddressId::default(), } } diff --git a/mm2src/mm2_main/src/lp_swap/maker_swap.rs b/mm2src/mm2_main/src/lp_swap/maker_swap.rs index 282d43dc95..fe0b50bd64 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap.rs @@ -1074,18 +1074,24 @@ impl MakerSwap { ])); } - let spend_fut = self.taker_coin.send_maker_spends_taker_payment(SpendPaymentArgs { - other_payment_tx: &self.r().taker_payment.clone().unwrap().tx_hex, + let other_taker_coin_htlc_pub = self.r().other_taker_coin_htlc_pub; + let secret = self.r().data.secret; + let maker_spends_payment_args = SpendPaymentArgs { + other_payment_tx: &self.r().taker_payment.clone().unwrap().tx_hex.clone(), time_lock: self.taker_payment_lock.load(Ordering::Relaxed), - other_pubkey: &*self.r().other_taker_coin_htlc_pub, - secret: &self.r().data.secret.0, + other_pubkey: &*other_taker_coin_htlc_pub, + secret: &secret.0, secret_hash: &self.secret_hash(), - swap_contract_address: &self.r().data.taker_coin_swap_contract_address, + swap_contract_address: &self.r().data.taker_coin_swap_contract_address.clone(), swap_unique_data: &self.unique_swap_data(), watcher_reward: self.r().watcher_reward, - }); + }; + let maybe_spend_tx = self + .taker_coin + .send_maker_spends_taker_payment(maker_spends_payment_args) + .await; - let transaction = match spend_fut.compat().await { + let transaction = match maybe_spend_tx { Ok(t) => t, Err(err) => { if let Some(tx) = err.get_tx() { @@ -1426,7 +1432,6 @@ impl MakerSwap { swap_unique_data: &selfi.unique_swap_data(), watcher_reward, }) - .compat() .await .map_err(|e| ERRL!("{:?}", e)) } @@ -2657,7 +2662,7 @@ mod maker_swap_tests { static mut SEND_MAKER_SPENDS_TAKER_PAYMENT_CALLED: bool = false; TestCoin::send_maker_spends_taker_payment.mock_safe(|_, _| { unsafe { SEND_MAKER_SPENDS_TAKER_PAYMENT_CALLED = true } - MockResult::Return(Box::new(futures01::future::ok(eth_tx_for_test().into()))) + MockResult::Return(Box::pin(futures::future::ready(Ok(eth_tx_for_test().into())))) }); let maker_coin = MmCoinEnum::Test(TestCoin::default()); diff --git a/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs b/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs index b21947792a..25c0ff8344 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs @@ -907,7 +907,7 @@ impl t, Err(err) => { if let Some(tx) = err.get_tx() { @@ -2142,19 +2148,23 @@ impl TakerSwap { let other_maker_coin_htlc_pub = self.r().other_maker_coin_htlc_pub; let secret = self.r().secret.0; let maker_coin_swap_contract_address = self.r().data.maker_coin_swap_contract_address.clone(); + let watcher_reward = self.r().watcher_reward; - let fut = self.maker_coin.send_taker_spends_maker_payment(SpendPaymentArgs { - other_payment_tx: &maker_payment, - time_lock: self.maker_payment_lock.load(Ordering::Relaxed), - other_pubkey: other_maker_coin_htlc_pub.as_slice(), - secret: &secret, - secret_hash: &secret_hash, - swap_contract_address: &maker_coin_swap_contract_address, - swap_unique_data: &unique_data, - watcher_reward: self.r().watcher_reward, - }); + let maybe_spend_tx = self + .maker_coin + .send_taker_spends_maker_payment(SpendPaymentArgs { + other_payment_tx: &maker_payment, + time_lock: self.maker_payment_lock.load(Ordering::Relaxed), + other_pubkey: other_maker_coin_htlc_pub.as_slice(), + secret: &secret, + secret_hash: &secret_hash, + swap_contract_address: &maker_coin_swap_contract_address, + swap_unique_data: &unique_data, + watcher_reward, + }) + .await; - let transaction = match fut.compat().await { + let transaction = match maybe_spend_tx { Ok(t) => t, Err(err) => { if let Some(tx) = err.get_tx() { @@ -2201,7 +2211,7 @@ impl TakerSwap { .await ); - let fut = self.maker_coin.send_taker_spends_maker_payment(SpendPaymentArgs { + let taker_spends_payment_args = SpendPaymentArgs { other_payment_tx: &maker_payment, time_lock: self.maker_payment_lock.load(Ordering::Relaxed), other_pubkey: other_maker_coin_htlc_pub.as_slice(), @@ -2210,9 +2220,13 @@ impl TakerSwap { swap_contract_address: &maker_coin_swap_contract_address, swap_unique_data: &unique_data, watcher_reward: self.r().watcher_reward, - }); + }; + let maybe_spend_tx = self + .maker_coin + .send_taker_spends_maker_payment(taker_spends_payment_args) + .await; - let transaction = match fut.compat().await { + let transaction = match maybe_spend_tx { Ok(t) => t, Err(err) => { if let Some(tx) = err.get_tx() { @@ -2701,7 +2715,7 @@ mod taker_swap_tests { static mut MAKER_PAYMENT_SPEND_CALLED: bool = false; TestCoin::send_taker_spends_maker_payment.mock_safe(|_, _| { unsafe { MAKER_PAYMENT_SPEND_CALLED = true }; - MockResult::Return(Box::new(futures01::future::ok(eth_tx_for_test().into()))) + MockResult::Return(Box::pin(futures::future::ready(Ok(eth_tx_for_test().into())))) }); TestCoin::search_for_swap_tx_spend_other .mock_safe(|_, _| MockResult::Return(Box::pin(futures::future::ready(Ok(None))))); @@ -2804,7 +2818,7 @@ mod taker_swap_tests { static mut MAKER_PAYMENT_SPEND_CALLED: bool = false; TestCoin::send_taker_spends_maker_payment.mock_safe(|_, _| { unsafe { MAKER_PAYMENT_SPEND_CALLED = true }; - MockResult::Return(Box::new(futures01::future::ok(eth_tx_for_test().into()))) + MockResult::Return(Box::pin(futures::future::ready(Ok(eth_tx_for_test().into())))) }); let maker_coin = MmCoinEnum::Test(TestCoin::default()); let taker_coin = MmCoinEnum::Test(TestCoin::default()); @@ -2925,7 +2939,7 @@ mod taker_swap_tests { static mut MAKER_PAYMENT_SPEND_CALLED: bool = false; TestCoin::send_taker_spends_maker_payment.mock_safe(|_, _| { unsafe { MAKER_PAYMENT_SPEND_CALLED = true }; - MockResult::Return(Box::new(futures01::future::ok(eth_tx_for_test().into()))) + MockResult::Return(Box::pin(futures::future::ready(Ok(eth_tx_for_test().into())))) }); let maker_coin = MmCoinEnum::Test(TestCoin::default()); let taker_coin = MmCoinEnum::Test(TestCoin::default()); diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index 1596fc9bc3..89145d05dd 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -41,9 +41,11 @@ use coins::{add_delegation, get_my_address, get_raw_transaction, get_staking_inf not(target_arch = "wasm32") ))] use coins::{SolanaCoin, SplToken}; -use coins_activation::{cancel_init_l2, cancel_init_standalone_coin, enable_platform_coin_with_tokens, enable_token, - init_l2, init_l2_status, init_l2_user_action, init_standalone_coin, - init_standalone_coin_status, init_standalone_coin_user_action}; +use coins_activation::{cancel_init_l2, cancel_init_platform_coin_with_tokens, cancel_init_standalone_coin, + cancel_init_token, enable_platform_coin_with_tokens, enable_token, init_l2, init_l2_status, + init_l2_user_action, init_platform_coin_with_tokens, init_platform_coin_with_tokens_status, + init_platform_coin_with_tokens_user_action, init_standalone_coin, init_standalone_coin_status, + init_standalone_coin_user_action, init_token, init_token_status, init_token_user_action}; use common::log::{error, warn}; use common::HttpStatusCode; use futures::Future as Future03; @@ -254,6 +256,16 @@ async fn rpc_task_dispatcher( "enable_utxo::user_action" => { handle_mmrpc(ctx, request, init_standalone_coin_user_action::).await }, + "enable_eth::cancel" => handle_mmrpc(ctx, request, cancel_init_platform_coin_with_tokens::).await, + "enable_eth::init" => handle_mmrpc(ctx, request, init_platform_coin_with_tokens::).await, + "enable_eth::status" => handle_mmrpc(ctx, request, init_platform_coin_with_tokens_status::).await, + "enable_eth::user_action" => { + handle_mmrpc(ctx, request, init_platform_coin_with_tokens_user_action::).await + }, + "enable_erc20::cancel" => handle_mmrpc(ctx, request, cancel_init_token::).await, + "enable_erc20::init" => handle_mmrpc(ctx, request, init_token::).await, + "enable_erc20::status" => handle_mmrpc(ctx, request, init_token_status::).await, + "enable_erc20::user_action" => handle_mmrpc(ctx, request, init_token_user_action::).await, "get_new_address::cancel" => handle_mmrpc(ctx, request, cancel_get_new_address).await, "get_new_address::init" => handle_mmrpc(ctx, request, init_get_new_address).await, "get_new_address::status" => handle_mmrpc(ctx, request, init_get_new_address_status).await, diff --git a/mm2src/mm2_main/src/wasm_tests.rs b/mm2src/mm2_main/src/wasm_tests.rs index f565757b0c..a43b948ac8 100644 --- a/mm2src/mm2_main/src/wasm_tests.rs +++ b/mm2src/mm2_main/src/wasm_tests.rs @@ -1,18 +1,17 @@ use crate::mm2::lp_init; use common::executor::{spawn, Timer}; use common::log::wasm_log::register_wasm_log; -use crypto::StandardHDCoinAddress; use mm2_core::mm_ctx::MmArc; use mm2_number::BigDecimal; use mm2_rpc::data::legacy::OrderbookResponse; use mm2_test_helpers::electrums::{doc_electrums, marty_electrums}; -use mm2_test_helpers::for_tests::{check_recent_swaps, enable_electrum_json, enable_z_coin_light, morty_conf, - pirate_conf, rick_conf, start_swaps, test_qrc20_history_impl, - wait_for_swaps_finish_and_check_status, MarketMakerIt, Mm2InitPrivKeyPolicy, - Mm2TestConf, Mm2TestConfForSwap, ARRR, MORTY, PIRATE_ELECTRUMS, - PIRATE_LIGHTWALLETD_URLS, RICK}; +use mm2_test_helpers::for_tests::{check_recent_swaps, enable_electrum_json, enable_utxo_v2_electrum, + enable_z_coin_light, morty_conf, pirate_conf, rick_conf, start_swaps, + test_qrc20_history_impl, wait_for_swaps_finish_and_check_status, MarketMakerIt, + Mm2InitPrivKeyPolicy, Mm2TestConf, Mm2TestConfForSwap, ARRR, MORTY, + PIRATE_ELECTRUMS, PIRATE_LIGHTWALLETD_URLS, RICK}; use mm2_test_helpers::get_passphrase; -use mm2_test_helpers::structs::EnableCoinBalance; +use mm2_test_helpers::structs::{Bip44Chain, EnableCoinBalance, HDAccountAddressId}; use serde_json::json; use wasm_bindgen_test::wasm_bindgen_test; @@ -54,17 +53,17 @@ async fn test_mm2_stops_impl( Timer::sleep(2.).await; // Enable coins on Bob side. Print the replies in case we need the address. - let rc = enable_electrum_json(&mm_bob, RICK, true, doc_electrums(), None).await; + let rc = enable_electrum_json(&mm_bob, RICK, true, doc_electrums()).await; log!("enable RICK (bob): {:?}", rc); - let rc = enable_electrum_json(&mm_bob, MORTY, true, marty_electrums(), None).await; + let rc = enable_electrum_json(&mm_bob, MORTY, true, marty_electrums()).await; log!("enable MORTY (bob): {:?}", rc); // Enable coins on Alice side. Print the replies in case we need the address. - let rc = enable_electrum_json(&mm_alice, RICK, true, doc_electrums(), None).await; + let rc = enable_electrum_json(&mm_alice, RICK, true, doc_electrums()).await; log!("enable RICK (bob): {:?}", rc); - let rc = enable_electrum_json(&mm_alice, MORTY, true, marty_electrums(), None).await; + let rc = enable_electrum_json(&mm_alice, MORTY, true, marty_electrums()).await; log!("enable MORTY (bob): {:?}", rc); start_swaps(&mut mm_bob, &mut mm_alice, pairs, maker_price, taker_price, volume).await; @@ -92,25 +91,31 @@ async fn test_qrc20_tx_history() { test_qrc20_history_impl(Some(wasm_start)).awa async fn trade_base_rel_electrum( mut mm_bob: MarketMakerIt, mut mm_alice: MarketMakerIt, - bob_path_to_address: Option, - alice_path_to_address: Option, + bob_path_to_address: Option, + alice_path_to_address: Option, pairs: &[(&'static str, &'static str)], maker_price: f64, taker_price: f64, volume: f64, ) { // Enable coins on Bob side. Print the replies in case we need the address. - let rc = enable_electrum_json(&mm_bob, RICK, true, doc_electrums(), bob_path_to_address.clone()).await; + let rc = enable_utxo_v2_electrum(&mm_bob, "RICK", doc_electrums(), bob_path_to_address.clone(), 60, None).await; log!("enable RICK (bob): {:?}", rc); - - let rc = enable_electrum_json(&mm_bob, MORTY, true, marty_electrums(), bob_path_to_address).await; + let rc = enable_utxo_v2_electrum(&mm_bob, "MORTY", marty_electrums(), bob_path_to_address, 60, None).await; log!("enable MORTY (bob): {:?}", rc); // Enable coins on Alice side. Print the replies in case we need the address. - let rc = enable_electrum_json(&mm_alice, RICK, true, doc_electrums(), alice_path_to_address.clone()).await; + let rc = enable_utxo_v2_electrum( + &mm_alice, + "RICK", + doc_electrums(), + alice_path_to_address.clone(), + 60, + None, + ) + .await; log!("enable RICK (alice): {:?}", rc); - - let rc = enable_electrum_json(&mm_alice, MORTY, true, marty_electrums(), alice_path_to_address).await; + let rc = enable_utxo_v2_electrum(&mm_alice, "MORTY", marty_electrums(), alice_path_to_address, 60, None).await; log!("enable MORTY (alice): {:?}", rc); let uuids = start_swaps(&mut mm_bob, &mut mm_alice, pairs, maker_price, taker_price, volume).await; @@ -174,11 +179,7 @@ async fn trade_test_rick_and_morty() { let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); - let alice_path_to_address = StandardHDCoinAddress { - account: 0, - is_change: false, - address_index: 0, - }; + let alice_path_to_address = HDAccountAddressId::default(); let pairs: &[_] = &[("RICK", "MORTY")]; trade_base_rel_electrum( @@ -220,17 +221,17 @@ async fn trade_v2_test_rick_and_morty() { let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); // use account: 1 to avoid possible UTXO re-usage between trade_v2_test_rick_and_morty and trade_test_rick_and_morty - let bob_path_to_address = StandardHDCoinAddress { - account: 1, - is_change: false, - address_index: 0, + let bob_path_to_address = HDAccountAddressId { + account_id: 1, + chain: Bip44Chain::External, + address_id: 0, }; // use account: 1 to avoid possible UTXO re-usage between trade_v2_test_rick_and_morty and trade_test_rick_and_morty - let alice_path_to_address = StandardHDCoinAddress { - account: 1, - is_change: false, - address_index: 0, + let alice_path_to_address = HDAccountAddressId { + account_id: 1, + chain: Bip44Chain::External, + address_id: 0, }; let pairs: &[_] = &[("RICK", "MORTY")]; diff --git a/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs b/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs index 9b11c46e3e..dc1e55f99f 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs @@ -3,8 +3,8 @@ pub use mm2_number::MmNumber; use mm2_rpc::data::legacy::BalanceResponse; pub use mm2_test_helpers::for_tests::{check_my_swap_status, check_recent_swaps, enable_eth_coin, enable_native, enable_native_bch, erc20_dev_conf, eth_dev_conf, eth_sepolia_conf, - eth_testnet_conf, jst_sepolia_conf, mm_dump, wait_check_stats_swap_status, - MarketMakerIt, MAKER_ERROR_EVENTS, MAKER_SUCCESS_EVENTS, TAKER_ERROR_EVENTS, + jst_sepolia_conf, mm_dump, wait_check_stats_swap_status, MarketMakerIt, + MAKER_ERROR_EVENTS, MAKER_SUCCESS_EVENTS, TAKER_ERROR_EVENTS, TAKER_SUCCESS_EVENTS}; use super::eth_docker_tests::{erc20_contract_checksum, fill_eth, fill_eth_erc20_with_private_key, geth_account, @@ -27,6 +27,7 @@ use crypto::privkey::key_pair_from_seed; use crypto::Secp256k1Secret; use ethabi::Token; use ethereum_types::{H160 as H160Eth, U256}; +use futures::TryFutureExt; use futures01::Future; use http::StatusCode; use keys::{Address, AddressBuilder, AddressHashEnum, AddressPrefix, KeyPair, NetworkAddressPrefixes, @@ -518,7 +519,7 @@ pub fn fill_qrc20_address(coin: &Qrc20Coin, amount: BigDecimal, timeout: u64) { }; let from_addr = get_address_by_label(coin, QTUM_ADDRESS_LABEL); - let to_addr = coin.my_addr_as_contract_addr().unwrap(); + let to_addr = coin.my_addr_as_contract_addr().compat().wait().unwrap(); let satoshis = sat_from_big_decimal(&amount, coin.as_ref().decimals).expect("!sat_from_big_decimal"); let hash = client diff --git a/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs b/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs index 7f061dd21a..c198371f61 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs @@ -14,15 +14,15 @@ use coins::{ConfirmPaymentInput, FoundSwapTxSpend, MarketCoinOps, MmCoin, Refund TransactionEnum, WithdrawRequest}; use common::{block_on, executor::Timer, get_utc_timestamp, now_sec, wait_until_sec}; use crypto::privkey::key_pair_from_seed; -use crypto::{CryptoCtx, KeyPairPolicy, StandardHDCoinAddress}; +use crypto::{CryptoCtx, DerivationPath, KeyPairPolicy}; use futures01::Future; use http::StatusCode; use mm2_number::{BigDecimal, BigRational, MmNumber}; use mm2_test_helpers::for_tests::{check_my_swap_status_amounts, disable_coin, disable_coin_err, enable_eth_coin, - enable_eth_coin_hd, erc20_dev_conf, eth_dev_conf, eth_testnet_conf, - get_locked_amount, kmd_conf, max_maker_vol, mm_dump, mycoin1_conf, mycoin_conf, - set_price, start_swaps, wait_for_swap_contract_negotiation, - wait_for_swap_negotiation_failure, MarketMakerIt, Mm2TestConf}; + enable_eth_with_tokens_v2, erc20_dev_conf, eth_dev_conf, get_locked_amount, + kmd_conf, max_maker_vol, mm_dump, mycoin1_conf, mycoin_conf, set_price, start_swaps, + wait_for_swap_contract_negotiation, wait_for_swap_negotiation_failure, + MarketMakerIt, Mm2TestConf}; use mm2_test_helpers::{get_passphrase, structs::*}; use serde_json::Value as Json; use std::collections::{HashMap, HashSet}; @@ -227,10 +227,7 @@ fn test_search_for_taker_swap_tx_spend_native_was_spent_by_maker() { swap_unique_data: &[], watcher_reward: false, }; - let spend_tx = coin - .send_maker_spends_taker_payment(maker_spends_payment_args) - .wait() - .unwrap(); + let spend_tx = block_on(coin.send_maker_spends_taker_payment(maker_spends_payment_args)).unwrap(); let confirm_payment_input = ConfirmPaymentInput { payment_tx: spend_tx.tx_hex(), @@ -298,10 +295,7 @@ fn test_search_for_maker_swap_tx_spend_native_was_spent_by_taker() { swap_unique_data: &[], watcher_reward: false, }; - let spend_tx = coin - .send_taker_spends_maker_payment(taker_spends_payment_args) - .wait() - .unwrap(); + let spend_tx = block_on(coin.send_taker_spends_maker_payment(taker_spends_payment_args)).unwrap(); let confirm_payment_input = ConfirmPaymentInput { payment_tx: spend_tx.tx_hex(), @@ -2151,17 +2145,11 @@ fn test_get_max_taker_vol_with_kmd() { log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); - let electrum = block_on(enable_electrum( - &mm_alice, - "KMD", - false, - &[ - "electrum1.cipig.net:10001", - "electrum2.cipig.net:10001", - "electrum3.cipig.net:10001", - ], - None, - )); + let electrum = block_on(enable_electrum(&mm_alice, "KMD", false, &[ + "electrum1.cipig.net:10001", + "electrum2.cipig.net:10001", + "electrum3.cipig.net:10001", + ])); log!("{:?}", electrum); let rc = block_on(mm_alice.rpc(&json!({ "userpass": mm_alice.userpass, @@ -2969,8 +2957,8 @@ fn test_utxo_merge() { block_on(mm_bob.wait_for_log(4., |log| log.contains("UTXO merge successful for coin MYCOIN, tx_hash"))).unwrap(); thread::sleep(Duration::from_secs(2)); - let (unspents, _) = - block_on(coin.get_unspent_ordered_list(coin.as_ref().derivation_method.unwrap_single_addr())).unwrap(); + let address = block_on(coin.as_ref().derivation_method.unwrap_single_addr()); + let (unspents, _) = block_on(coin.get_unspent_ordered_list(&address)).unwrap(); assert_eq!(unspents.len(), 1); } @@ -3023,8 +3011,8 @@ fn test_utxo_merge_max_merge_at_once() { block_on(mm_bob.wait_for_log(4., |log| log.contains("UTXO merge successful for coin MYCOIN, tx_hash"))).unwrap(); thread::sleep(Duration::from_secs(2)); - let (unspents, _) = - block_on(coin.get_unspent_ordered_list(coin.as_ref().derivation_method.unwrap_single_addr())).unwrap(); + let address = block_on(coin.as_ref().derivation_method.unwrap_single_addr()); + let (unspents, _) = block_on(coin.get_unspent_ordered_list(&address)).unwrap(); // 4 utxos are merged of 5 so the resulting unspents len must be 2 assert_eq!(unspents.len(), 2); } @@ -3388,7 +3376,7 @@ fn test_match_utxo_with_eth_taker_sell() { generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), alice_priv_key); generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), bob_priv_key); - let coins = json!([mycoin_conf(1000), eth_testnet_conf()]); + let coins = json!([mycoin_conf(1000), eth_dev_conf()]); let mut mm_bob = MarketMakerIt::start( json!({ @@ -3465,7 +3453,7 @@ fn test_match_utxo_with_eth_taker_buy() { generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), alice_priv_key); generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), bob_priv_key); - let coins = json!([mycoin_conf(1000), eth_testnet_conf()]); + let coins = json!([mycoin_conf(1000), eth_dev_conf()]); let mut mm_bob = MarketMakerIt::start( json!({ "gui": "nogui", @@ -3693,7 +3681,7 @@ fn test_enable_eth_coin_with_token_without_balance() { false, )); - let enable_eth_with_tokens: RpcV2Response = + let enable_eth_with_tokens: RpcV2Response = serde_json::from_value(enable_eth_with_tokens).unwrap(); let (_, eth_balance) = enable_eth_with_tokens @@ -3902,7 +3890,7 @@ fn test_trade_base_rel_mycoin_mycoin1_coins() { trade_base_rel(("MYCOIN", "MYCOI fn withdraw_and_send( mm: &MarketMakerIt, coin: &str, - from: Option, + from: Option, to: &str, from_addr: &str, expected_bal_change: &str, @@ -4050,19 +4038,20 @@ fn test_withdraw_and_send_hd_eth_erc20() { panic!("Expected 'KeyPairPolicy::GlobalHDAccount'"); }; + let swap_contract = format!("0x{}", hex::encode(swap_contract())); + // Withdraw from HD account 0, change address 0, index 1 - let mut path_to_address = StandardHDCoinAddress { - account: 0, - is_change: false, - address_index: 1, + let mut path_to_address = HDAccountAddressId { + account_id: 0, + chain: Bip44Chain::External, + address_id: 1, }; + let path_to_addr_str = "/0'/0/1"; + let path_to_coin: String = serde_json::from_value(eth_dev_conf()["derivation_path"].clone()).unwrap(); + let derivation_path = path_to_coin.clone() + path_to_addr_str; + let derivation_path = DerivationPath::from_str(&derivation_path).unwrap(); // Get the private key associated with this account and fill it with eth and erc20 token. - let priv_key = hd_acc - .derive_secp256k1_secret( - &serde_json::from_value(eth_dev_conf()["derivation_path"].clone()).unwrap(), - &path_to_address, - ) - .unwrap(); + let priv_key = hd_acc.derive_secp256k1_secret(&derivation_path).unwrap(); fill_eth_erc20_with_private_key(priv_key); let coins = json!([eth_dev_conf(), erc20_dev_conf(&erc20_contract_checksum())]); @@ -4073,31 +4062,33 @@ fn test_withdraw_and_send_hd_eth_erc20() { let (_mm_dump_log, _mm_dump_dashboard) = mm_hd.mm_dump(); log!("Alice log path: {}", mm_hd.log_path.display()); - let swap_contract = format!("0x{}", hex::encode(swap_contract())); - - let eth_enable = block_on(enable_eth_coin_hd( + let eth_enable = block_on(enable_eth_with_tokens_v2( &mm_hd, "ETH", - &[GETH_RPC_URL], + &["ERC20DEV"], &swap_contract, - Some(path_to_address.clone()), - )); - - let erc20_enable = block_on(enable_eth_coin_hd( - &mm_hd, - "ERC20DEV", &[GETH_RPC_URL], - &swap_contract, + 60, Some(path_to_address.clone()), )); - + let activation_result = match eth_enable { + EthWithTokensActivationResult::HD(hd) => hd, + _ => panic!("Expected EthWithTokensActivationResult::HD"), + }; + let balance = match activation_result.wallet_balance { + EnableCoinBalanceMap::HD(hd) => hd, + _ => panic!("Expected EnableCoinBalance::HD"), + }; + let account = balance.accounts.get(0).expect("Expected account at index 0"); assert_eq!( - eth_enable["address"].as_str().unwrap(), + account.addresses[1].address, "0xDe841899aB4A22E23dB21634e54920aDec402397" ); + assert_eq!(account.addresses[1].balance.len(), 2); + assert_eq!(account.addresses[1].balance.get("ETH").unwrap().spendable, 100.into()); assert_eq!( - erc20_enable["address"].as_str().unwrap(), - "0xDe841899aB4A22E23dB21634e54920aDec402397" + account.addresses[1].balance.get("ERC20DEV").unwrap().spendable, + 100.into() ); withdraw_and_send( @@ -4105,7 +4096,7 @@ fn test_withdraw_and_send_hd_eth_erc20() { "ETH", Some(path_to_address.clone()), "0x4b2d0d6c2c785217457B69B922A2A9cEA98f71E9", - eth_enable["address"].as_str().unwrap(), + &account.addresses[1].address, "-0.001", 0.001, ); @@ -4115,13 +4106,13 @@ fn test_withdraw_and_send_hd_eth_erc20() { "ERC20DEV", Some(path_to_address.clone()), "0x4b2d0d6c2c785217457B69B922A2A9cEA98f71E9", - erc20_enable["address"].as_str().unwrap(), + &account.addresses[1].address, "-0.001", 0.001, ); // Change the address index, the withdrawal should fail. - path_to_address.address_index = 0; + path_to_address.address_id = 0; let withdraw = block_on(mm_hd.rpc(&json! ({ "mmrpc": "2.0", @@ -4139,12 +4130,10 @@ fn test_withdraw_and_send_hd_eth_erc20() { assert!(!withdraw.0.is_success(), "!withdraw: {}", withdraw.1); // But if we fill it, we should be able to withdraw. - let priv_key = hd_acc - .derive_secp256k1_secret( - &serde_json::from_value(eth_dev_conf()["derivation_path"].clone()).unwrap(), - &path_to_address, - ) - .unwrap(); + let path_to_addr_str = "/0'/0/0"; + let derivation_path = path_to_coin + path_to_addr_str; + let derivation_path = DerivationPath::from_str(&derivation_path).unwrap(); + let priv_key = hd_acc.derive_secp256k1_secret(&derivation_path).unwrap(); fill_eth_erc20_with_private_key(priv_key); let withdraw = block_on(mm_hd.rpc(&json! ({ @@ -4543,7 +4532,7 @@ fn test_set_price_conf_settings() { .display_priv_key() .unwrap(); - let coins = json!([eth_dev_conf(),{"coin":"ERC20DEV","name":"erc20dev","protocol":{"type":"ERC20","protocol_data":{"platform":"ETH","contract_address":erc20_contract_checksum()}},"required_confirmations":2},]); + let coins = json!([eth_dev_conf(),{"coin":"ERC20DEV","name":"erc20dev","protocol":{"type":"ERC20","protocol_data":{"platform":"ETH","contract_address":erc20_contract_checksum()}},"required_confirmations":2,"chain_id": 1337},]); let conf = Mm2TestConf::seednode(&private_key_str, &coins); let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); @@ -4616,7 +4605,7 @@ fn test_buy_conf_settings() { .display_priv_key() .unwrap(); - let coins = json!([eth_dev_conf(),{"coin":"ERC20DEV","name":"erc20dev","protocol":{"type":"ERC20","protocol_data":{"platform":"ETH","contract_address":erc20_contract_checksum()}},"required_confirmations":2},]); + let coins = json!([eth_dev_conf(),{"coin":"ERC20DEV","name":"erc20dev","protocol":{"type":"ERC20","protocol_data":{"platform":"ETH","contract_address":erc20_contract_checksum()}},"required_confirmations":2,"chain_id": 1337},]); let conf = Mm2TestConf::seednode(&private_key_str, &coins); let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); @@ -4689,7 +4678,7 @@ fn test_sell_conf_settings() { .display_priv_key() .unwrap(); - let coins = json!([eth_dev_conf(),{"coin":"ERC20DEV","name":"erc20dev","protocol":{"type":"ERC20","protocol_data":{"platform":"ETH","contract_address":erc20_contract_checksum()}},"required_confirmations":2},]); + let coins = json!([eth_dev_conf(),{"coin":"ERC20DEV","name":"erc20dev","protocol":{"type":"ERC20","protocol_data":{"platform":"ETH","contract_address":erc20_contract_checksum()}},"required_confirmations":2,"chain_id": 1337},]); let conf = Mm2TestConf::seednode(&private_key_str, &coins); let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); @@ -5273,107 +5262,117 @@ fn test_enable_eth_erc20_coins_with_enable_hd() { let swap_contract = format!("0x{}", hex::encode(swap_contract())); // Withdraw from HD account 0, change address 0, index 0 - let path_to_address = StandardHDCoinAddress { - account: 0, - is_change: false, - address_index: 0, - }; + let path_to_address = HDAccountAddressId::default(); let conf = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); let mm_hd = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); let (_mm_dump_log, _mm_dump_dashboard) = mm_hd.mm_dump(); log!("Alice log path: {}", mm_hd.log_path.display()); - let eth_enable = block_on(enable_eth_coin_hd( + let eth_enable = block_on(enable_eth_with_tokens_v2( &mm_hd, "ETH", - &[GETH_RPC_URL], + &["ERC20DEV"], &swap_contract, - Some(path_to_address.clone()), - )); - assert_eq!( - eth_enable["address"].as_str().unwrap(), - "0x1737F1FaB40c6Fd3dc729B51C0F97DB3297CCA93" - ); - - let erc20_enable = block_on(enable_eth_coin_hd( - &mm_hd, - "ERC20DEV", &[GETH_RPC_URL], - &swap_contract, + 60, Some(path_to_address), )); + let activation_result = match eth_enable { + EthWithTokensActivationResult::HD(hd) => hd, + _ => panic!("Expected EthWithTokensActivationResult::HD"), + }; + let balance = match activation_result.wallet_balance { + EnableCoinBalanceMap::HD(hd) => hd, + _ => panic!("Expected EnableCoinBalance::HD"), + }; + let account = balance.accounts.get(0).expect("Expected account at index 0"); assert_eq!( - erc20_enable["address"].as_str().unwrap(), + account.addresses[0].address, "0x1737F1FaB40c6Fd3dc729B51C0F97DB3297CCA93" ); + assert_eq!(account.addresses[0].balance.len(), 2); + assert!(account.addresses[0].balance.contains_key("ETH")); + assert!(account.addresses[0].balance.contains_key("ERC20DEV")); - // Withdraw from HD account 0, change address 0, index 1 - let path_to_address = StandardHDCoinAddress { - account: 0, - is_change: false, - address_index: 1, + block_on(mm_hd.stop()).unwrap(); + + // Enable HD account 0, change address 0, index 1 + let path_to_address = HDAccountAddressId { + account_id: 0, + chain: Bip44Chain::External, + address_id: 1, }; let conf = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); let mm_hd = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); let (_mm_dump_log, _mm_dump_dashboard) = mm_hd.mm_dump(); log!("Alice log path: {}", mm_hd.log_path.display()); - let eth_enable = block_on(enable_eth_coin_hd( + let eth_enable = block_on(enable_eth_with_tokens_v2( &mm_hd, "ETH", - &[GETH_RPC_URL], + &["ERC20DEV"], &swap_contract, - Some(path_to_address.clone()), - )); - assert_eq!( - eth_enable["address"].as_str().unwrap(), - "0xDe841899aB4A22E23dB21634e54920aDec402397" - ); - let erc20_enable = block_on(enable_eth_coin_hd( - &mm_hd, - "ERC20DEV", &[GETH_RPC_URL], - &swap_contract, + 60, Some(path_to_address), )); + let activation_result = match eth_enable { + EthWithTokensActivationResult::HD(hd) => hd, + _ => panic!("Expected EthWithTokensActivationResult::HD"), + }; + let balance = match activation_result.wallet_balance { + EnableCoinBalanceMap::HD(hd) => hd, + _ => panic!("Expected EnableCoinBalance::HD"), + }; + let account = balance.accounts.get(0).expect("Expected account at index 0"); assert_eq!( - erc20_enable["address"].as_str().unwrap(), + account.addresses[1].address, "0xDe841899aB4A22E23dB21634e54920aDec402397" ); + assert_eq!(account.addresses[0].balance.len(), 2); + assert!(account.addresses[0].balance.contains_key("ETH")); + assert!(account.addresses[0].balance.contains_key("ERC20DEV")); - // Withdraw from HD account 77, change address 0, index 7 - let path_to_address = StandardHDCoinAddress { - account: 77, - is_change: false, - address_index: 7, + block_on(mm_hd.stop()).unwrap(); + + // Enable HD account 77, change address 0, index 7 + let path_to_address = HDAccountAddressId { + account_id: 77, + chain: Bip44Chain::External, + address_id: 7, }; let conf = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); let mm_hd = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); let (_mm_dump_log, _mm_dump_dashboard) = mm_hd.mm_dump(); log!("Alice log path: {}", mm_hd.log_path.display()); - let eth_enable = block_on(enable_eth_coin_hd( + let eth_enable = block_on(enable_eth_with_tokens_v2( &mm_hd, "ETH", - &[GETH_RPC_URL], + &["ERC20DEV"], &swap_contract, - Some(path_to_address.clone()), - )); - assert_eq!( - eth_enable["address"].as_str().unwrap(), - "0xa420a4DBd8C50e6240014Db4587d2ec8D0cE0e6B" - ); - let erc20_enable = block_on(enable_eth_coin_hd( - &mm_hd, - "ERC20DEV", &[GETH_RPC_URL], - &swap_contract, + 60, Some(path_to_address), )); + let activation_result = match eth_enable { + EthWithTokensActivationResult::HD(hd) => hd, + _ => panic!("Expected EthWithTokensActivationResult::HD"), + }; + let balance = match activation_result.wallet_balance { + EnableCoinBalanceMap::HD(hd) => hd, + _ => panic!("Expected EnableCoinBalance::HD"), + }; + let account = balance.accounts.get(0).expect("Expected account at index 0"); assert_eq!( - erc20_enable["address"].as_str().unwrap(), + account.addresses[7].address, "0xa420a4DBd8C50e6240014Db4587d2ec8D0cE0e6B" ); + assert_eq!(account.addresses[0].balance.len(), 2); + assert!(account.addresses[0].balance.contains_key("ETH")); + assert!(account.addresses[0].balance.contains_key("ERC20DEV")); + + block_on(mm_hd.stop()).unwrap(); } fn request_and_check_orderbook_depth(mm_alice: &MarketMakerIt) { diff --git a/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs b/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs index f209e9afc2..f7f5cdb5c9 100644 --- a/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs @@ -5,10 +5,10 @@ use super::docker_tests_common::{random_secp256k1_secret, ERC1155_TEST_ABI, ERC7 use bitcrypto::{dhash160, sha256}; use coins::eth::{checksum_address, eth_addr_to_hex, eth_coin_from_conf_and_request, EthCoin, ERC20_ABI}; use coins::nft::nft_structs::{Chain, ContractType, NftInfo}; -use coins::{CoinProtocol, ConfirmPaymentInput, FoundSwapTxSpend, MakerNftSwapOpsV2, MarketCoinOps, NftSwapInfo, - ParseCoinAssocTypes, PrivKeyBuildPolicy, RefundPaymentArgs, SearchForSwapTxSpendInput, - SendNftMakerPaymentArgs, SendPaymentArgs, SpendNftMakerPaymentArgs, SpendPaymentArgs, SwapOps, - SwapTxTypeWithSecretHash, ToBytes, Transaction, ValidateNftMakerPaymentArgs}; +use coins::{CoinProtocol, CoinWithDerivationMethod, ConfirmPaymentInput, DerivationMethod, FoundSwapTxSpend, + MakerNftSwapOpsV2, MarketCoinOps, NftSwapInfo, ParseCoinAssocTypes, PrivKeyBuildPolicy, RefundPaymentArgs, + SearchForSwapTxSpendInput, SendNftMakerPaymentArgs, SendPaymentArgs, SpendNftMakerPaymentArgs, + SpendPaymentArgs, SwapOps, SwapTxTypeWithSecretHash, ToBytes, Transaction, ValidateNftMakerPaymentArgs}; use common::{block_on, now_sec}; use crypto::Secp256k1Secret; use ethereum_types::U256; @@ -244,8 +244,13 @@ pub fn eth_coin_with_random_privkey_using_urls(swap_contract_address: Address, u )) .unwrap(); + let my_address = match eth_coin.derivation_method() { + DerivationMethod::SingleAddress(addr) => *addr, + _ => panic!("Expected single address"), + }; + // 100 ETH - fill_eth(eth_coin.my_address, U256::from(10).pow(U256::from(20))); + fill_eth(my_address, U256::from(10).pow(U256::from(20))); eth_coin } @@ -278,10 +283,15 @@ pub fn erc20_coin_with_random_privkey(swap_contract_address: Address) -> EthCoin )) .unwrap(); + let my_address = match erc20_coin.derivation_method() { + DerivationMethod::SingleAddress(addr) => *addr, + _ => panic!("Expected single address"), + }; + // 1 ETH - fill_eth(erc20_coin.my_address, U256::from(10).pow(U256::from(18))); + fill_eth(my_address, U256::from(10).pow(U256::from(18))); // 100 tokens (it has 8 decimals) - fill_erc20(erc20_coin.my_address, U256::from(10000000000u64)); + fill_erc20(my_address, U256::from(10000000000u64)); erc20_coin } @@ -315,16 +325,17 @@ pub fn global_nft_with_random_privkey(swap_contract_address: Address, nft_type: )) .unwrap(); - fill_eth(global_nft.my_address, U256::from(10).pow(U256::from(20))); + let my_address = block_on(global_nft.my_addr()); + fill_eth(my_address, U256::from(10).pow(U256::from(20))); if let Some(nft_type) = nft_type { match nft_type { TestNftType::Erc1155 { token_id, amount } => { - mint_erc1155(global_nft.my_address, U256::from(token_id), U256::from(amount)); + mint_erc1155(my_address, U256::from(token_id), U256::from(amount)); block_on(fill_erc1155_info(&global_nft, token_id, amount)); }, TestNftType::Erc721 { token_id } => { - mint_erc721(global_nft.my_address, U256::from(token_id)); + mint_erc721(my_address, U256::from(token_id)); block_on(fill_erc721_info(&global_nft, token_id)); }, } @@ -351,9 +362,10 @@ pub fn fill_eth_erc20_with_private_key(priv_key: Secp256k1Secret) { PrivKeyBuildPolicy::IguanaPrivKey(priv_key), )) .unwrap(); + let my_address = block_on(eth_coin.derivation_method().single_addr_or_err()).unwrap(); // 100 ETH - fill_eth(eth_coin.my_address, U256::from(10).pow(U256::from(20))); + fill_eth(my_address, U256::from(10).pow(U256::from(20))); let erc20_conf = erc20_dev_conf(&erc20_contract_checksum()); let req = json!({ @@ -363,7 +375,7 @@ pub fn fill_eth_erc20_with_private_key(priv_key: Secp256k1Secret) { "swap_contract_address": swap_contract(), }); - let erc20_coin = block_on(eth_coin_from_conf_and_request( + let _erc20_coin = block_on(eth_coin_from_conf_and_request( &MM_CTX, "ERC20DEV", &erc20_conf, @@ -377,7 +389,7 @@ pub fn fill_eth_erc20_with_private_key(priv_key: Secp256k1Secret) { .unwrap(); // 100 tokens (it has 8 decimals) - fill_erc20(erc20_coin.my_address, U256::from(10000000000u64)); + fill_erc20(my_address, U256::from(10000000000u64)); } #[test] @@ -499,10 +511,7 @@ fn send_and_spend_eth_maker_payment() { swap_unique_data: &[], watcher_reward: false, }; - let payment_spend = taker_eth_coin - .send_taker_spends_maker_payment(spend_args) - .wait() - .unwrap(); + let payment_spend = block_on(taker_eth_coin.send_taker_spends_maker_payment(spend_args)).unwrap(); log!("Payment spend tx hash {:02x}", payment_spend.tx_hash()); let confirm_input = ConfirmPaymentInput { @@ -652,10 +661,7 @@ fn send_and_spend_erc20_maker_payment() { swap_unique_data: &[], watcher_reward: false, }; - let payment_spend = taker_erc20_coin - .send_taker_spends_maker_payment(spend_args) - .wait() - .unwrap(); + let payment_spend = block_on(taker_erc20_coin.send_taker_spends_maker_payment(spend_args)).unwrap(); log!("Payment spend tx hash {:02x}", payment_spend.tx_hash()); let confirm_input = ConfirmPaymentInput { @@ -768,7 +774,8 @@ fn send_and_spend_erc721_maker_payment() { taker_global_nft.wait_for_confirmations(confirm_input).wait().unwrap(); let new_owner = erc712_owner(U256::from(2)); - assert_eq!(new_owner, taker_global_nft.my_address); + let my_address = block_on(taker_global_nft.my_addr()); + assert_eq!(new_owner, my_address); } #[test] @@ -848,7 +855,8 @@ fn send_and_spend_erc1155_maker_payment() { }; taker_global_nft.wait_for_confirmations(confirm_input).wait().unwrap(); - let balance = erc1155_balance(taker_global_nft.my_address, U256::from(4)); + let my_address = block_on(taker_global_nft.my_addr()); + let balance = erc1155_balance(my_address, U256::from(4)); assert_eq!(balance, U256::from(3)); } @@ -856,12 +864,13 @@ fn send_and_spend_erc1155_maker_payment() { fn test_nonce_several_urls() { // Use one working and one failing URL. let coin = eth_coin_with_random_privkey_using_urls(swap_contract(), &[GETH_RPC_URL, "http://127.0.0.1:0"]); - let (old_nonce, _) = coin.clone().get_addr_nonce(coin.my_address).wait().unwrap(); + let my_address = block_on(coin.derivation_method().single_addr_or_err()).unwrap(); + let (old_nonce, _) = coin.clone().get_addr_nonce(my_address).wait().unwrap(); // Send a payment to increase the nonce. - coin.send_to_address(coin.my_address, 200000000.into()).wait().unwrap(); + coin.send_to_address(my_address, 200000000.into()).wait().unwrap(); - let (new_nonce, _) = coin.clone().get_addr_nonce(coin.my_address).wait().unwrap(); + let (new_nonce, _) = coin.get_addr_nonce(my_address).wait().unwrap(); assert_eq!(old_nonce + 1, new_nonce); } @@ -869,16 +878,14 @@ fn test_nonce_several_urls() { fn test_nonce_lock() { use crate::common::Future01CompatExt; use futures::future::join_all; - use mm2_test_helpers::for_tests::wait_for_log; let coin = eth_coin_with_random_privkey(swap_contract()); - let futures = (0..5).map(|_| coin.send_to_address(coin.my_address, 200000000.into()).compat()); + let my_address = block_on(coin.derivation_method().single_addr_or_err()).unwrap(); + let futures = (0..5).map(|_| coin.send_to_address(my_address, 200000000.into()).compat()); let results = block_on(join_all(futures)); // make sure all transactions are successful for result in results { result.unwrap(); } - - block_on(wait_for_log(&MM_CTX, 1.1, |line| line.contains("get_addr_nonce…"))).unwrap(); } diff --git a/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs b/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs index 3f32f85a4e..857141e2d9 100644 --- a/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs @@ -236,10 +236,7 @@ fn test_taker_spends_maker_payment() { swap_unique_data: &[], watcher_reward: false, }; - let spend = taker_coin - .send_taker_spends_maker_payment(taker_spends_payment_args) - .wait() - .unwrap(); + let spend = block_on(taker_coin.send_taker_spends_maker_payment(taker_spends_payment_args)).unwrap(); let spend_tx_hash = spend.tx_hash(); let spend_tx_hex = spend.tx_hex(); log!("Taker spends tx: {:?}", spend_tx_hash); @@ -341,10 +338,7 @@ fn test_maker_spends_taker_payment() { swap_unique_data: &[], watcher_reward: false, }; - let spend = maker_coin - .send_maker_spends_taker_payment(maker_spends_payment_args) - .wait() - .unwrap(); + let spend = block_on(maker_coin.send_maker_spends_taker_payment(maker_spends_payment_args)).unwrap(); let spend_tx_hash = spend.tx_hash(); let spend_tx_hex = spend.tx_hex(); log!("Maker spends tx: {:?}", spend_tx_hash); @@ -618,10 +612,7 @@ fn test_search_for_swap_tx_spend_taker_spent() { swap_unique_data: &[], watcher_reward: false, }; - let spend = taker_coin - .send_taker_spends_maker_payment(taker_spends_payment_args) - .wait() - .unwrap(); + let spend = block_on(taker_coin.send_taker_spends_maker_payment(taker_spends_payment_args)).unwrap(); let spend_tx_hash = spend.tx_hash(); let spend_tx_hex = spend.tx_hex(); log!("Taker spends tx: {:?}", spend_tx_hash); @@ -864,10 +855,7 @@ fn test_wait_for_tx_spend() { swap_unique_data: &[], watcher_reward: false, }; - let spend = taker_coin - .send_taker_spends_maker_payment(taker_spends_payment_args) - .wait() - .unwrap(); + let spend = block_on(taker_coin.send_taker_spends_maker_payment(taker_spends_payment_args)).unwrap(); unsafe { SPEND_TX = Some(spend) } }); diff --git a/mm2src/mm2_main/tests/docker_tests/swap_proto_v2_tests.rs b/mm2src/mm2_main/tests/docker_tests/swap_proto_v2_tests.rs index 5ecb0af243..fbc632aa47 100644 --- a/mm2src/mm2_main/tests/docker_tests/swap_proto_v2_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/swap_proto_v2_tests.rs @@ -333,7 +333,7 @@ fn send_and_spend_taker_payment_dex_fee_burn() { time_lock: 0, maker_secret_hash, maker_pub, - maker_address: maker_coin.my_addr(), + maker_address: &block_on(maker_coin.my_addr()), taker_pub, dex_fee_pub: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee, @@ -436,7 +436,7 @@ fn send_and_spend_taker_payment_standard_dex_fee() { time_lock: 0, maker_secret_hash, maker_pub, - maker_address: maker_coin.my_addr(), + maker_address: &block_on(maker_coin.my_addr()), taker_pub, dex_fee_pub: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee, diff --git a/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs b/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs index 635b14d439..22019abdb1 100644 --- a/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs @@ -21,9 +21,9 @@ use mm2_main::mm2::lp_swap::{dex_fee_amount, dex_fee_amount_from_taker_coin, gen REFUND_TEST_FAILURE_LOG, TAKER_PAYMENT_REFUND_SENT_LOG, WATCHER_MESSAGE_SENT_LOG}; use mm2_number::BigDecimal; use mm2_number::MmNumber; -use mm2_test_helpers::for_tests::{enable_eth_coin, erc20_dev_conf, eth_dev_conf, eth_jst_testnet_conf, - eth_testnet_conf, mm_dump, my_balance, my_swap_status, mycoin1_conf, mycoin_conf, - start_swaps, wait_for_swaps_finish_and_check_status, MarketMakerIt, Mm2TestConf, +use mm2_test_helpers::for_tests::{enable_eth_coin, erc20_dev_conf, eth_dev_conf, eth_jst_testnet_conf, mm_dump, + my_balance, my_swap_status, mycoin1_conf, mycoin_conf, start_swaps, + wait_for_swaps_finish_and_check_status, MarketMakerIt, Mm2TestConf, DEFAULT_RPC_PASSWORD}; use mm2_test_helpers::get_passphrase; use mm2_test_helpers::structs::WatcherConf; @@ -271,9 +271,9 @@ fn check_actual_events(mm_alice: &MarketMakerIt, uuid: &str, expected_events: &[ status_response } -fn run_taker_node(coins: &Value, envs: &[(&str, &str)]) -> (MarketMakerIt, Mm2TestConf) { +fn run_taker_node(coins: &Value, envs: &[(&str, &str)], seednodes: &[&str]) -> (MarketMakerIt, Mm2TestConf) { let privkey = hex::encode(random_secp256k1_secret()); - let conf = Mm2TestConf::seednode(&format!("0x{}", privkey), coins); + let conf = Mm2TestConf::light_node(&format!("0x{}", privkey), coins, seednodes); let mm = block_on(MarketMakerIt::start_with_envs( conf.conf.clone(), conf.rpc_password.clone(), @@ -312,7 +312,11 @@ fn restart_taker_and_wait_until(conf: &Mm2TestConf, envs: &[(&str, &str)], wait_ fn run_maker_node(coins: &Value, envs: &[(&str, &str)], seednodes: &[&str]) -> MarketMakerIt { let privkey = hex::encode(random_secp256k1_secret()); - let conf = Mm2TestConf::light_node(&format!("0x{}", privkey), coins, seednodes); + let conf = if seednodes.is_empty() { + Mm2TestConf::seednode(&format!("0x{}", privkey), coins) + } else { + Mm2TestConf::light_node(&format!("0x{}", privkey), coins, seednodes) + }; let mm = block_on(MarketMakerIt::start_with_envs( conf.conf.clone(), conf.rpc_password, @@ -360,9 +364,11 @@ fn run_watcher_node( #[test] fn test_taker_saves_the_swap_as_successful_after_restart_panic_at_wait_for_taker_payment_spend() { let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let mut mm_bob = run_maker_node(&coins, &[], &[]); let (mut mm_alice, mut alice_conf) = - run_taker_node(&coins, &[("TAKER_FAIL_AT", "wait_for_taker_payment_spend_panic")]); - let mut mm_bob = run_maker_node(&coins, &[], &[&mm_alice.ip.to_string()]); + run_taker_node(&coins, &[("TAKER_FAIL_AT", "wait_for_taker_payment_spend_panic")], &[ + &mm_bob.ip.to_string(), + ]); let watcher_conf = WatcherConf { wait_taker_payment: 0., @@ -370,7 +376,7 @@ fn test_taker_saves_the_swap_as_successful_after_restart_panic_at_wait_for_taker refund_start_factor: 1.5, search_interval: 1.0, }; - let mut mm_watcher = run_watcher_node(&coins, &[], &[&mm_alice.ip.to_string()], watcher_conf); + let mut mm_watcher = run_watcher_node(&coins, &[], &[&mm_bob.ip.to_string()], watcher_conf); let uuids = block_on(start_swaps( &mut mm_bob, @@ -407,13 +413,20 @@ fn test_taker_saves_the_swap_as_successful_after_restart_panic_at_wait_for_taker "Finished", ]; check_actual_events(&mm_alice, &uuids[0], &expected_events); + + block_on(mm_alice.stop()).unwrap(); + block_on(mm_watcher.stop()).unwrap(); + block_on(mm_bob.stop()).unwrap(); } #[test] fn test_taker_saves_the_swap_as_successful_after_restart_panic_at_maker_payment_spend() { let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let (mut mm_alice, mut alice_conf) = run_taker_node(&coins, &[("TAKER_FAIL_AT", "maker_payment_spend_panic")]); - let mut mm_bob = run_maker_node(&coins, &[], &[&mm_alice.ip.to_string()]); + let mut mm_bob = run_maker_node(&coins, &[], &[]); + let (mut mm_alice, mut alice_conf) = + run_taker_node(&coins, &[("TAKER_FAIL_AT", "maker_payment_spend_panic")], &[&mm_bob + .ip + .to_string()]); let watcher_conf = WatcherConf { wait_taker_payment: 0., @@ -421,7 +434,7 @@ fn test_taker_saves_the_swap_as_successful_after_restart_panic_at_maker_payment_ refund_start_factor: 1.5, search_interval: 1.0, }; - let mut mm_watcher = run_watcher_node(&coins, &[], &[&mm_alice.ip.to_string()], watcher_conf); + let mut mm_watcher = run_watcher_node(&coins, &[], &[&mm_bob.ip.to_string()], watcher_conf); let uuids = block_on(start_swaps( &mut mm_bob, @@ -463,11 +476,16 @@ fn test_taker_saves_the_swap_as_successful_after_restart_panic_at_maker_payment_ #[test] fn test_taker_saves_the_swap_as_finished_after_restart_taker_payment_refunded_panic_at_wait_for_taker_payment_spend() { let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let (mut mm_alice, mut alice_conf) = run_taker_node(&coins, &[ - ("USE_TEST_LOCKTIME", ""), - ("TAKER_FAIL_AT", "wait_for_taker_payment_spend_panic"), - ]); - let mut mm_bob = run_maker_node(&coins, &[("USE_TEST_LOCKTIME", "")], &[&mm_alice.ip.to_string()]); + let mm_seednode = run_maker_node(&coins, &[("USE_TEST_LOCKTIME", "")], &[]); + let mut mm_bob = run_maker_node(&coins, &[("USE_TEST_LOCKTIME", "")], &[&mm_seednode.ip.to_string()]); + let (mut mm_alice, mut alice_conf) = run_taker_node( + &coins, + &[ + ("USE_TEST_LOCKTIME", ""), + ("TAKER_FAIL_AT", "wait_for_taker_payment_spend_panic"), + ], + &[&mm_seednode.ip.to_string()], + ); let watcher_conf = WatcherConf { wait_taker_payment: 0., @@ -478,7 +496,7 @@ fn test_taker_saves_the_swap_as_finished_after_restart_taker_payment_refunded_pa let mut mm_watcher = run_watcher_node( &coins, &[("USE_TEST_LOCKTIME", "")], - &[&mm_alice.ip.to_string()], + &[&mm_seednode.ip.to_string()], watcher_conf, ); @@ -522,16 +540,25 @@ fn test_taker_saves_the_swap_as_finished_after_restart_taker_payment_refunded_pa "Finished", ]; check_actual_events(&mm_alice, &uuids[0], &expected_events); + + block_on(mm_alice.stop()).unwrap(); + block_on(mm_watcher.stop()).unwrap(); + block_on(mm_seednode.stop()).unwrap(); } #[test] fn test_taker_saves_the_swap_as_finished_after_restart_taker_payment_refunded_panic_at_taker_payment_refund() { let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let (mut mm_alice, mut alice_conf) = run_taker_node(&coins, &[ - ("USE_TEST_LOCKTIME", ""), - ("TAKER_FAIL_AT", "taker_payment_refund_panic"), - ]); - let mut mm_bob = run_maker_node(&coins, &[("USE_TEST_LOCKTIME", "")], &[&mm_alice.ip.to_string()]); + let mm_seednode = run_maker_node(&coins, &[("USE_TEST_LOCKTIME", "")], &[]); + let mut mm_bob = run_maker_node(&coins, &[("USE_TEST_LOCKTIME", "")], &[&mm_seednode.ip.to_string()]); + let (mut mm_alice, mut alice_conf) = run_taker_node( + &coins, + &[ + ("USE_TEST_LOCKTIME", ""), + ("TAKER_FAIL_AT", "taker_payment_refund_panic"), + ], + &[&mm_seednode.ip.to_string()], + ); let watcher_conf = WatcherConf { wait_taker_payment: 0., @@ -542,7 +569,7 @@ fn test_taker_saves_the_swap_as_finished_after_restart_taker_payment_refunded_pa let mut mm_watcher = run_watcher_node( &coins, &[("USE_TEST_LOCKTIME", "")], - &[&mm_alice.ip.to_string()], + &[&mm_seednode.ip.to_string()], watcher_conf, ); @@ -589,13 +616,17 @@ fn test_taker_saves_the_swap_as_finished_after_restart_taker_payment_refunded_pa "Finished", ]; check_actual_events(&mm_alice, &uuids[0], &expected_events); + + block_on(mm_alice.stop()).unwrap(); + block_on(mm_watcher.stop()).unwrap(); + block_on(mm_seednode.stop()).unwrap(); } #[test] fn test_taker_completes_swap_after_restart() { let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let (mut mm_alice, mut alice_conf) = run_taker_node(&coins, &[]); - let mut mm_bob = run_maker_node(&coins, &[], &[&mm_alice.ip.to_string()]); + let mut mm_bob = run_maker_node(&coins, &[], &[]); + let (mut mm_alice, mut alice_conf) = run_taker_node(&coins, &[], &[&mm_bob.ip.to_string()]); let uuids = block_on(start_swaps( &mut mm_bob, @@ -630,6 +661,9 @@ fn test_taker_completes_swap_after_restart() { 2., 25., )); + + block_on(mm_alice.stop()).unwrap(); + block_on(mm_bob.stop()).unwrap(); } #[test] @@ -1019,7 +1053,7 @@ fn test_watcher_waits_for_taker_eth() { #[test] #[ignore] fn test_two_watchers_spend_maker_payment_eth_erc20() { - let coins = json!([eth_testnet_conf(), eth_jst_testnet_conf()]); + let coins = json!([eth_dev_conf(), eth_jst_testnet_conf()]); let alice_passphrase = String::from("spice describe gravity federal blast come thank unfair canal monkey style afraid"); diff --git a/mm2src/mm2_main/tests/integration_tests_common/mod.rs b/mm2src/mm2_main/tests/integration_tests_common/mod.rs index 7ec7fcdf6e..13393bd8d1 100644 --- a/mm2src/mm2_main/tests/integration_tests_common/mod.rs +++ b/mm2src/mm2_main/tests/integration_tests_common/mod.rs @@ -2,14 +2,13 @@ use common::executor::Timer; use common::log::LogLevel; use common::{block_on, log, now_ms, wait_until_ms}; use crypto::privkey::key_pair_from_seed; -use crypto::StandardHDCoinAddress; use mm2_main::mm2::{lp_main, LpMainParams}; use mm2_rpc::data::legacy::CoinInitResponse; use mm2_test_helpers::electrums::{doc_electrums, marty_electrums}; -use mm2_test_helpers::for_tests::{enable_native as enable_native_impl, init_utxo_electrum, init_utxo_status, - MarketMakerIt}; - -use mm2_test_helpers::structs::{InitTaskResult, InitUtxoStatus, RpcV2Response, UtxoStandardActivationResult}; +use mm2_test_helpers::for_tests::{create_new_account_status, enable_native as enable_native_impl, + init_create_new_account, MarketMakerIt}; +use mm2_test_helpers::structs::{CreateNewAccountStatus, HDAccountAddressId, HDAccountBalance, InitTaskResult, + RpcV2Response}; use serde_json::{self as json, Value as Json}; use std::collections::HashMap; use std::env::var; @@ -34,16 +33,10 @@ pub fn test_mm_start_impl() { } /// Ideally, this function should be replaced everywhere with `enable_electrum_json`. -pub async fn enable_electrum( - mm: &MarketMakerIt, - coin: &str, - tx_history: bool, - urls: &[&str], - path_to_address: Option, -) -> CoinInitResponse { +pub async fn enable_electrum(mm: &MarketMakerIt, coin: &str, tx_history: bool, urls: &[&str]) -> CoinInitResponse { use mm2_test_helpers::for_tests::enable_electrum as enable_electrum_impl; - let value = enable_electrum_impl(mm, coin, tx_history, urls, path_to_address).await; + let value = enable_electrum_impl(mm, coin, tx_history, urls).await; json::from_value(value).unwrap() } @@ -52,11 +45,10 @@ pub async fn enable_electrum_json( coin: &str, tx_history: bool, servers: Vec, - path_to_address: Option, ) -> CoinInitResponse { use mm2_test_helpers::for_tests::enable_electrum_json as enable_electrum_impl; - let value = enable_electrum_impl(mm, coin, tx_history, servers, path_to_address).await; + let value = enable_electrum_impl(mm, coin, tx_history, servers).await; json::from_value(value).unwrap() } @@ -64,7 +56,7 @@ pub async fn enable_native( mm: &MarketMakerIt, coin: &str, urls: &[&str], - path_to_address: Option, + path_to_address: Option, ) -> CoinInitResponse { let value = enable_native_impl(mm, coin, urls, path_to_address).await; json::from_value(value).unwrap() @@ -72,59 +64,25 @@ pub async fn enable_native( pub async fn enable_coins_rick_morty_electrum(mm: &MarketMakerIt) -> HashMap<&'static str, CoinInitResponse> { let mut replies = HashMap::new(); - replies.insert( - "RICK", - enable_electrum_json(mm, "RICK", false, doc_electrums(), None).await, - ); + replies.insert("RICK", enable_electrum_json(mm, "RICK", false, doc_electrums()).await); replies.insert( "MORTY", - enable_electrum_json(mm, "MORTY", false, marty_electrums(), None).await, + enable_electrum_json(mm, "MORTY", false, marty_electrums()).await, ); replies } -pub async fn enable_utxo_v2_electrum( - mm: &MarketMakerIt, - coin: &str, - servers: Vec, - timeout: u64, - priv_key_policy: Option<&str>, -) -> UtxoStandardActivationResult { - let init = init_utxo_electrum(mm, coin, servers, priv_key_policy).await; - let init: RpcV2Response = json::from_value(init).unwrap(); - let timeout = wait_until_ms(timeout * 1000); - - loop { - if now_ms() > timeout { - panic!("{} initialization timed out", coin); - } - - let status = init_utxo_status(mm, init.result.task_id).await; - let status: RpcV2Response = json::from_value(status).unwrap(); - log!("init_utxo_status: {:?}", status); - match status.result { - InitUtxoStatus::Ok(result) => break result, - InitUtxoStatus::Error(e) => panic!("{} initialization error {:?}", coin, e), - _ => Timer::sleep(1.).await, - } - } -} - pub async fn enable_coins_eth_electrum( mm: &MarketMakerIt, eth_urls: &[&str], - path_to_address: Option, ) -> HashMap<&'static str, CoinInitResponse> { let mut replies = HashMap::new(); - replies.insert( - "RICK", - enable_electrum_json(mm, "RICK", false, doc_electrums(), path_to_address.clone()).await, - ); + replies.insert("RICK", enable_electrum_json(mm, "RICK", false, doc_electrums()).await); replies.insert( "MORTY", - enable_electrum_json(mm, "MORTY", false, marty_electrums(), path_to_address.clone()).await, + enable_electrum_json(mm, "MORTY", false, marty_electrums()).await, ); - replies.insert("ETH", enable_native(mm, "ETH", eth_urls, path_to_address.clone()).await); + replies.insert("ETH", enable_native(mm, "ETH", eth_urls, None).await); replies } @@ -135,3 +93,29 @@ pub fn addr_from_enable<'a>(enable_response: &'a HashMap<&str, CoinInitResponse> pub fn rmd160_from_passphrase(passphrase: &str) -> [u8; 20] { key_pair_from_seed(passphrase).unwrap().public().address_hash().take() } + +pub async fn create_new_account( + mm: &MarketMakerIt, + coin: &str, + account_id: Option, + timeout: u64, +) -> HDAccountBalance { + let init = init_create_new_account(mm, coin, account_id).await; + let init: RpcV2Response = json::from_value(init).unwrap(); + let timeout = wait_until_ms(timeout * 1000); + + loop { + if now_ms() > timeout { + panic!("{} initialization timed out", coin); + } + + let status = create_new_account_status(mm, init.result.task_id).await; + let status: RpcV2Response = json::from_value(status).unwrap(); + log!("create_new_account_status: {:?}", status); + match status.result { + CreateNewAccountStatus::Ok(result) => break result, + CreateNewAccountStatus::Error(e) => panic!("{} initialization error {:?}", coin, e), + _ => Timer::sleep(1.).await, + } + } +} diff --git a/mm2src/mm2_main/tests/mm2_tests/bch_and_slp_tests.rs b/mm2src/mm2_main/tests/mm2_tests/bch_and_slp_tests.rs index 59e61647f9..4614bfdafb 100644 --- a/mm2src/mm2_main/tests/mm2_tests/bch_and_slp_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/bch_and_slp_tests.rs @@ -1,13 +1,12 @@ use common::custom_futures::repeatable::{Ready, Retry}; use common::{block_on, log, repeatable}; -use crypto::StandardHDCoinAddress; use http::StatusCode; use itertools::Itertools; use mm2_test_helpers::for_tests::{disable_coin, electrum_servers_rpc, enable_bch_with_tokens, enable_slp, my_tx_history_v2, sign_message, tbch_for_slp_conf, tbch_usdf_conf, verify_message, MarketMakerIt, Mm2TestConf, UtxoRpcMode, T_BCH_ELECTRUMS}; -use mm2_test_helpers::structs::{EnableBchWithTokensResponse, RpcV2Response, SignatureResponse, StandardHistoryV2Res, - UtxoFeeDetails, VerificationResponse}; +use mm2_test_helpers::structs::{Bip44Chain, EnableBchWithTokensResponse, HDAccountAddressId, RpcV2Response, + SignatureResponse, StandardHistoryV2Res, UtxoFeeDetails, VerificationResponse}; use serde_json::{self as json, json, Value as Json}; use std::env; use std::thread; @@ -607,7 +606,9 @@ fn test_sign_verify_message_slp() { } /// Tested via [Electron-Cash-SLP](https://github.com/simpleledger/Electron-Cash-SLP). +// Todo: Ignored until enable_bch_with_tokens is implemented for HD wallet using task manager. #[test] +#[ignore] #[cfg(not(target_arch = "wasm32"))] fn test_bch_and_slp_with_enable_hd() { const TX_HISTORY: bool = false; @@ -615,11 +616,7 @@ fn test_bch_and_slp_with_enable_hd() { let coins = json!([tbch_for_slp_conf(), tbch_usdf_conf()]); // HD account 0 and change 0 and address_index 0 - let path_to_address = StandardHDCoinAddress { - account: 0, - is_change: false, - address_index: 0, - }; + let path_to_address = HDAccountAddressId::default(); let conf_0 = Mm2TestConf::seednode_with_hd_account(BIP39_PASSPHRASE, &coins); let mm_hd_0 = MarketMakerIt::start(conf_0.conf, conf_0.rpc_password, None).unwrap(); @@ -651,10 +648,10 @@ fn test_bch_and_slp_with_enable_hd() { assert_eq!(slp_addr, "slptest:qpylzql7gzh6yctm7uslsz5qufl44gk2tsfnl7m9uj"); // HD account 0 and change 0 and address_index 1 - let path_to_address = StandardHDCoinAddress { - account: 0, - is_change: false, - address_index: 1, + let path_to_address = HDAccountAddressId { + account_id: 0, + chain: Bip44Chain::External, + address_id: 1, }; let conf_1 = Mm2TestConf::seednode_with_hd_account(BIP39_PASSPHRASE, &coins); let mm_hd_1 = MarketMakerIt::start(conf_1.conf, conf_1.rpc_password, None).unwrap(); @@ -687,10 +684,10 @@ fn test_bch_and_slp_with_enable_hd() { assert_eq!(slp_addr, "slptest:qpyhwc7shd5hlul8zg0snmaptaa9q9yc4q9uzddky0"); // HD account 7 and change 1 and address_index 77 - let path_to_address = StandardHDCoinAddress { - account: 7, - is_change: true, - address_index: 77, + let path_to_address = HDAccountAddressId { + account_id: 7, + chain: Bip44Chain::Internal, + address_id: 77, }; let conf_1 = Mm2TestConf::seednode_with_hd_account(BIP39_PASSPHRASE, &coins); let mm_hd_1 = MarketMakerIt::start(conf_1.conf, conf_1.rpc_password, None).unwrap(); diff --git a/mm2src/mm2_main/tests/mm2_tests/best_orders_tests.rs b/mm2src/mm2_main/tests/mm2_tests/best_orders_tests.rs index 80b751b2ff..8daaac6e16 100644 --- a/mm2src/mm2_main/tests/mm2_tests/best_orders_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/best_orders_tests.rs @@ -37,8 +37,8 @@ fn test_best_orders_v2_exclude_mine() { .unwrap(); thread::sleep(Duration::from_secs(2)); - let _ = block_on(enable_electrum(&mm_bob, "RICK", false, DOC_ELECTRUM_ADDRS, None)); - let _ = block_on(enable_electrum(&mm_bob, "MORTY", false, MARTY_ELECTRUM_ADDRS, None)); + let _ = block_on(enable_electrum(&mm_bob, "RICK", false, DOC_ELECTRUM_ADDRS)); + let _ = block_on(enable_electrum(&mm_bob, "MORTY", false, MARTY_ELECTRUM_ADDRS)); let bob_orders = [ ("RICK", "MORTY", "0.9", "0.9", None), ("RICK", "MORTY", "0.8", "0.9", None), @@ -80,8 +80,8 @@ fn test_best_orders_v2_exclude_mine() { .unwrap(); thread::sleep(Duration::from_secs(2)); - let _ = block_on(enable_electrum(&mm_alice, "RICK", false, DOC_ELECTRUM_ADDRS, None)); - let _ = block_on(enable_electrum(&mm_alice, "MORTY", false, MARTY_ELECTRUM_ADDRS, None)); + let _ = block_on(enable_electrum(&mm_alice, "RICK", false, DOC_ELECTRUM_ADDRS)); + let _ = block_on(enable_electrum(&mm_alice, "MORTY", false, MARTY_ELECTRUM_ADDRS)); let alice_orders = [("RICK", "MORTY", "0.85", "1", None)]; let mut alice_order_ids = BTreeSet::::new(); for (base, rel, price, volume, min_volume) in alice_orders.iter() { @@ -319,7 +319,7 @@ fn test_best_orders_address_and_confirmations() { let enable_tbtc_res: CoinInitResponse = json::from_str(&electrum.1).unwrap(); let tbtc_segwit_address = enable_tbtc_res.address; - let enable_rick_res = block_on(enable_electrum(&mm_bob, "RICK", false, DOC_ELECTRUM_ADDRS, None)); + let enable_rick_res = block_on(enable_electrum(&mm_bob, "RICK", false, DOC_ELECTRUM_ADDRS)); log!("enable RICK: {:?}", enable_rick_res); let rick_address = enable_rick_res.address; @@ -464,10 +464,10 @@ fn best_orders_must_return_duplicate_for_orderbook_tickers() { let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); log!("Bob log path: {}", mm_bob.log_path.display()); - let t_btc_bob = block_on(enable_electrum(&mm_bob, "tBTC", false, TBTC_ELECTRUMS, None)); + let t_btc_bob = block_on(enable_electrum(&mm_bob, "tBTC", false, TBTC_ELECTRUMS)); log!("Bob enable tBTC: {:?}", t_btc_bob); - let rick_bob = block_on(enable_electrum(&mm_bob, "RICK", false, DOC_ELECTRUM_ADDRS, None)); + let rick_bob = block_on(enable_electrum(&mm_bob, "RICK", false, DOC_ELECTRUM_ADDRS)); log!("Bob enable RICK: {:?}", rick_bob); // issue sell request on Bob side by setting base/rel price @@ -617,7 +617,7 @@ fn zhtlc_best_orders() { log!("bob_zombie_cache_path {}", bob_zombie_cache_path.display()); std::fs::copy("./mm2src/coins/for_tests/ZOMBIE_CACHE.db", bob_zombie_cache_path).unwrap(); - block_on(enable_electrum_json(&mm_bob, "RICK", false, doc_electrums(), None)); + block_on(enable_electrum_json(&mm_bob, "RICK", false, doc_electrums())); block_on(enable_z_coin(&mm_bob, "ZOMBIE")); let set_price_json = json!({ diff --git a/mm2src/mm2_main/tests/mm2_tests/eth_tests.rs b/mm2src/mm2_main/tests/mm2_tests/eth_tests.rs index 470a8b2b48..600848f3b6 100644 --- a/mm2src/mm2_main/tests/mm2_tests/eth_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/eth_tests.rs @@ -1,6 +1,6 @@ use common::block_on; use http::StatusCode; -use mm2_test_helpers::for_tests::{eth_testnet_conf, get_passphrase, MarketMakerIt, Mm2TestConf, ETH_SEPOLIA_NODE, +use mm2_test_helpers::for_tests::{eth_sepolia_conf, get_passphrase, MarketMakerIt, Mm2TestConf, ETH_SEPOLIA_NODES, ETH_SEPOLIA_SWAP_CONTRACT}; use serde_json::{json, Value as Json}; use std::str::FromStr; @@ -9,10 +9,10 @@ use std::str::FromStr; #[cfg(not(target_arch = "wasm32"))] fn test_sign_eth_transaction() { let passphrase = get_passphrase(&".env.client", "BOB_PASSPHRASE").unwrap(); - let coins = json!([eth_testnet_conf()]); + let coins = json!([eth_sepolia_conf()]); let conf = Mm2TestConf::seednode(&passphrase, &coins); let mm = block_on(MarketMakerIt::start_async(conf.conf, conf.rpc_password, None)).unwrap(); - block_on(enable_eth(&mm, "ETH", ETH_SEPOLIA_NODE)); + block_on(enable_eth(&mm, "ETH", ETH_SEPOLIA_NODES)); let signed_tx = block_on(call_sign_eth_transaction( &mm, "ETH", diff --git a/mm2src/mm2_main/tests/mm2_tests/iris_swap.rs b/mm2src/mm2_main/tests/mm2_tests/iris_swap.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/mm2src/mm2_main/tests/mm2_tests/lightning_tests.rs b/mm2src/mm2_main/tests/mm2_tests/lightning_tests.rs index 5e69a89771..4fde665bd6 100644 --- a/mm2src/mm2_main/tests/mm2_tests/lightning_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/lightning_tests.rs @@ -120,13 +120,7 @@ fn start_lightning_nodes(enable_0_confs: bool) -> (MarketMakerIt, MarketMakerIt, let (_dump_log, _dump_dashboard) = mm_node_1.mm_dump(); log!("Node 1 log path: {}", mm_node_1.log_path.display()); - let electrum = block_on(enable_electrum( - &mm_node_1, - "tBTC-TEST-segwit", - false, - T_BTC_ELECTRUMS, - None, - )); + let electrum = block_on(enable_electrum(&mm_node_1, "tBTC-TEST-segwit", false, T_BTC_ELECTRUMS)); log!("Node 1 tBTC address: {}", electrum.address); let enable_lightning_1 = block_on(enable_lightning(&mm_node_1, "tBTC-TEST-lightning", 600)); @@ -150,13 +144,7 @@ fn start_lightning_nodes(enable_0_confs: bool) -> (MarketMakerIt, MarketMakerIt, let (_dump_log, _dump_dashboard) = mm_node_2.mm_dump(); log!("Node 2 log path: {}", mm_node_2.log_path.display()); - let electrum = block_on(enable_electrum( - &mm_node_2, - "tBTC-TEST-segwit", - false, - T_BTC_ELECTRUMS, - None, - )); + let electrum = block_on(enable_electrum(&mm_node_2, "tBTC-TEST-segwit", false, T_BTC_ELECTRUMS)); log!("Node 2 tBTC address: {}", electrum.address); let enable_lightning_2 = block_on(enable_lightning(&mm_node_2, "tBTC-TEST-lightning", 600)); @@ -388,7 +376,7 @@ fn test_enable_lightning() { let (_dump_log, _dump_dashboard) = mm.mm_dump(); log!("log path: {}", mm.log_path.display()); - let _electrum = block_on(enable_electrum(&mm, "tBTC-TEST-segwit", false, T_BTC_ELECTRUMS, None)); + let _electrum = block_on(enable_electrum(&mm, "tBTC-TEST-segwit", false, T_BTC_ELECTRUMS)); let enable_lightning_coin = block_on(enable_lightning(&mm, "tBTC-TEST-lightning", 600)); assert_eq!(&enable_lightning_coin.platform_coin, "tBTC-TEST-segwit"); @@ -1075,7 +1063,7 @@ fn test_sign_verify_message_lightning() { let (_dump_log, _dump_dashboard) = mm.mm_dump(); log!("log path: {}", mm.log_path.display()); - block_on(enable_electrum(&mm, "tBTC-TEST-segwit", false, T_BTC_ELECTRUMS, None)); + block_on(enable_electrum(&mm, "tBTC-TEST-segwit", false, T_BTC_ELECTRUMS)); block_on(enable_lightning(&mm, "tBTC-TEST-lightning", 600)); let response = block_on(sign_message(&mm, "tBTC-TEST-lightning")); diff --git a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs index d90718508d..1c360dba49 100644 --- a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs +++ b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs @@ -4,7 +4,6 @@ use crate::integration_tests_common::*; use common::executor::Timer; use common::{cfg_native, cfg_wasm32, log, new_uuid}; use crypto::privkey::key_pair_from_seed; -use crypto::StandardHDCoinAddress; use http::{HeaderMap, StatusCode}; use mm2_main::mm2::lp_ordermatch::MIN_ORDER_KEEP_ALIVE_INTERVAL; use mm2_metrics::{MetricType, MetricsJson}; @@ -13,16 +12,16 @@ use mm2_rpc::data::legacy::{CoinInitResponse, MmVersionResponse, OrderbookRespon use mm2_test_helpers::electrums::*; #[cfg(all(not(target_arch = "wasm32"), not(feature = "zhtlc-native-tests")))] use mm2_test_helpers::for_tests::wait_check_stats_swap_status; -use mm2_test_helpers::for_tests::{btc_segwit_conf, btc_with_spv_conf, btc_with_sync_starting_header, - check_recent_swaps, enable_qrc20, eth_testnet_conf, find_metrics_in_json, - from_env_file, get_shared_db_id, mm_spat, morty_conf, rick_conf, sign_message, - start_swaps, tbtc_segwit_conf, tbtc_with_spv_conf, test_qrc20_history_impl, - tqrc20_conf, verify_message, wait_for_swaps_finish_and_check_status, - wait_till_history_has_records, MarketMakerIt, Mm2InitPrivKeyPolicy, Mm2TestConf, - Mm2TestConfForSwap, RaiiDump, DOC_ELECTRUM_ADDRS, ETH_MAINNET_NODE, - ETH_MAINNET_SWAP_CONTRACT, ETH_SEPOLIA_NODE, ETH_SEPOLIA_SWAP_CONTRACT, - MARTY_ELECTRUM_ADDRS, MORTY, QRC20_ELECTRUMS, RICK, RICK_ELECTRUM_ADDRS, - TBTC_ELECTRUMS, T_BCH_ELECTRUMS}; +use mm2_test_helpers::for_tests::{account_balance, btc_segwit_conf, btc_with_spv_conf, btc_with_sync_starting_header, + check_recent_swaps, enable_qrc20, enable_utxo_v2_electrum, eth_dev_conf, + find_metrics_in_json, from_env_file, get_new_address, get_shared_db_id, mm_spat, + morty_conf, rick_conf, sign_message, start_swaps, tbtc_segwit_conf, + tbtc_with_spv_conf, test_qrc20_history_impl, tqrc20_conf, verify_message, + wait_for_swaps_finish_and_check_status, wait_till_history_has_records, + MarketMakerIt, Mm2InitPrivKeyPolicy, Mm2TestConf, Mm2TestConfForSwap, RaiiDump, + DOC_ELECTRUM_ADDRS, ETH_MAINNET_NODE, ETH_MAINNET_SWAP_CONTRACT, ETH_SEPOLIA_NODES, + ETH_SEPOLIA_SWAP_CONTRACT, MARTY_ELECTRUM_ADDRS, MORTY, QRC20_ELECTRUMS, RICK, + RICK_ELECTRUM_ADDRS, TBTC_ELECTRUMS, T_BCH_ELECTRUMS}; use mm2_test_helpers::get_passphrase; use mm2_test_helpers::structs::*; use serde_json::{self as json, json, Value as Json}; @@ -230,7 +229,7 @@ fn test_my_balance() { let (_dump_log, _dump_dashboard) = mm.mm_dump(); log!("log path: {}", mm.log_path.display()); // Enable RICK. - let json = block_on(enable_electrum(&mm, "RICK", false, DOC_ELECTRUM_ADDRS, None)); + let json = block_on(enable_electrum(&mm, "RICK", false, DOC_ELECTRUM_ADDRS)); assert_eq!(json.balance, "7.777".parse().unwrap()); let my_balance = block_on(mm.rpc(&json! ({ @@ -386,8 +385,8 @@ fn test_check_balance_on_order_post() { let coins = json!([ {"coin":"RICK","asset":"RICK","rpcport":8923,"txversion":4,"overwintered":1,"protocol":{"type":"UTXO"}}, {"coin":"MORTY","asset":"MORTY","rpcport":11608,"txversion":4,"overwintered":1,"protocol":{"type":"UTXO"}}, - {"coin":"ETH","name":"ethereum","protocol":{"type":"ETH"},"rpcport":80}, - {"coin":"JST","name":"jst","protocol":{"type":"ERC20", "protocol_data":{"platform":"ETH","contract_address":"0x996a8aE0304680F6A69b8A9d7C6E37D65AB5AB56"}}} + {"coin":"ETH","name":"ethereum","chain_id":1,"protocol":{"type":"ETH"},"rpcport":80}, + {"coin":"JST","name":"jst","chain_id":1,"protocol":{"type":"ERC20", "protocol_data":{"platform":"ETH","contract_address":"0x996a8aE0304680F6A69b8A9d7C6E37D65AB5AB56"}}} ]); // start bob and immediately place the order @@ -412,11 +411,9 @@ fn test_check_balance_on_order_post() { // Enable coins. Print the replies in case we need the "address". log!( "enable_coins (bob): {:?}", - block_on(enable_coins_eth_electrum( - &mm, - &["https://mainnet.infura.io/v3/c01c1b4cf66642528547624e1d6d9d6b"], - None - )), + block_on(enable_coins_eth_electrum(&mm, &[ + "https://mainnet.infura.io/v3/c01c1b4cf66642528547624e1d6d9d6b" + ])), ); // issue sell request by setting base/rel price @@ -598,7 +595,7 @@ fn test_mmrpc_v2() { let (_dump_log, _dump_dashboard) = mm.mm_dump(); log!("Log path: {}", mm.log_path.display()); - let _electrum = block_on(enable_electrum(&mm, "RICK", false, DOC_ELECTRUM_ADDRS, None)); + let _electrum = block_on(enable_electrum(&mm, "RICK", false, DOC_ELECTRUM_ADDRS)); // no `userpass` let withdraw = block_on(mm.rpc(&json! ({ @@ -727,8 +724,8 @@ fn test_rpc_password_from_json_no_userpass() { async fn trade_base_rel_electrum( bob_priv_key_policy: Mm2InitPrivKeyPolicy, alice_priv_key_policy: Mm2InitPrivKeyPolicy, - bob_path_to_address: Option, - alice_path_to_address: Option, + bob_path_to_address: Option, + alice_path_to_address: Option, pairs: &[(&'static str, &'static str)], maker_price: f64, taker_price: f64, @@ -792,22 +789,23 @@ async fn trade_base_rel_electrum( log!("enable ZOMBIE alice {:?}", zombie_alice); } // Enable coins on Bob side. Print the replies in case we need the address. - let rc = enable_electrum(&mm_bob, "RICK", false, DOC_ELECTRUM_ADDRS, bob_path_to_address.clone()).await; + let rc = enable_utxo_v2_electrum(&mm_bob, "RICK", doc_electrums(), bob_path_to_address.clone(), 600, None).await; log!("enable RICK (bob): {:?}", rc); - let rc = enable_electrum(&mm_bob, "MORTY", false, MARTY_ELECTRUM_ADDRS, bob_path_to_address).await; + let rc = enable_utxo_v2_electrum(&mm_bob, "MORTY", marty_electrums(), bob_path_to_address, 600, None).await; log!("enable MORTY (bob): {:?}", rc); // Enable coins on Alice side. Print the replies in case we need the address. - let rc = enable_electrum( + let rc = enable_utxo_v2_electrum( &mm_alice, "RICK", - false, - DOC_ELECTRUM_ADDRS, + doc_electrums(), alice_path_to_address.clone(), + 600, + None, ) .await; log!("enable RICK (alice): {:?}", rc); - let rc = enable_electrum(&mm_alice, "MORTY", false, MARTY_ELECTRUM_ADDRS, alice_path_to_address).await; + let rc = enable_utxo_v2_electrum(&mm_alice, "MORTY", marty_electrums(), alice_path_to_address, 600, None).await; log!("enable MORTY (alice): {:?}", rc); let uuids = start_swaps(&mut mm_bob, &mut mm_alice, pairs, maker_price, taker_price, volume).await; @@ -903,17 +901,16 @@ fn trade_test_electrum_rick_zombie() { fn withdraw_and_send( mm: &MarketMakerIt, coin: &str, - from: Option, + from: Option, to: &str, enable_res: &HashMap<&'static str, CoinInitResponse>, expected_bal_change: &str, amount: f64, ) { + use coins::TxFeeDetails; use std::ops::Sub; - use coins::{TxFeeDetails, WithdrawFrom}; - - let from = from.map(WithdrawFrom::HDWalletAddress); + let from = from.map(WithdrawFrom::AddressId); let withdraw = block_on(mm.rpc(&json! ({ "mmrpc": "2.0", "userpass": mm.userpass, @@ -997,13 +994,7 @@ fn test_withdraw_and_send() { let mut enable_res = block_on(enable_coins_rick_morty_electrum(&mm_alice)); enable_res.insert( "MORTY_SEGWIT", - block_on(enable_electrum( - &mm_alice, - "MORTY_SEGWIT", - false, - MARTY_ELECTRUM_ADDRS, - None, - )), + block_on(enable_electrum(&mm_alice, "MORTY_SEGWIT", false, MARTY_ELECTRUM_ADDRS)), ); log!("enable_coins (alice): {:?}", enable_res); @@ -1092,21 +1083,21 @@ fn test_withdraw_and_send_hd() { let (_dump_log, _dump_dashboard) = mm_hd.mm_dump(); log!("log path: {}", mm_hd.log_path.display()); - let rick = block_on(enable_electrum(&mm_hd, "RICK", TX_HISTORY, RICK_ELECTRUM_ADDRS, None)); + let rick = block_on(enable_electrum(&mm_hd, "RICK", TX_HISTORY, RICK_ELECTRUM_ADDRS)); assert_eq!(rick.address, "RXNtAyDSsY3DS3VxTpJegzoHU9bUX54j56"); let mut rick_enable_res = HashMap::new(); rick_enable_res.insert("RICK", rick); - let tbtc_segwit = block_on(enable_electrum(&mm_hd, "tBTC-Segwit", TX_HISTORY, TBTC_ELECTRUMS, None)); + let tbtc_segwit = block_on(enable_electrum(&mm_hd, "tBTC-Segwit", TX_HISTORY, TBTC_ELECTRUMS)); assert_eq!(tbtc_segwit.address, "tb1q7z9vzf8wpp9cks0l4nj5v28zf7jt56kuekegh5"); let mut tbtc_segwit_enable_res = HashMap::new(); tbtc_segwit_enable_res.insert("tBTC-Segwit", tbtc_segwit); // Withdraw from HD account 0, change address 0, index 1 - let from_account_address = StandardHDCoinAddress { - account: 0, - is_change: false, - address_index: 1, + let from_account_address = HDAccountAddressId { + account_id: 0, + chain: Bip44Chain::External, + address_id: 1, }; withdraw_and_send( @@ -1256,13 +1247,7 @@ fn test_withdraw_legacy() { let mut enable_res = block_on(enable_coins_rick_morty_electrum(&mm_alice)); enable_res.insert( "MORTY_SEGWIT", - block_on(enable_electrum( - &mm_alice, - "MORTY_SEGWIT", - false, - MARTY_ELECTRUM_ADDRS, - None, - )), + block_on(enable_electrum(&mm_alice, "MORTY_SEGWIT", false, MARTY_ELECTRUM_ADDRS)), ); log!("enable_coins (alice): {:?}", enable_res); @@ -1492,7 +1477,7 @@ fn test_order_errors_when_base_equal_rel() { .unwrap(); let (_dump_log, _dump_dashboard) = mm.mm_dump(); log!("Log path: {}", mm.log_path.display()); - block_on(enable_electrum(&mm, "RICK", false, DOC_ELECTRUM_ADDRS, None)); + block_on(enable_electrum(&mm, "RICK", false, DOC_ELECTRUM_ADDRS)); let rc = block_on(mm.rpc(&json! ({ "userpass": mm.userpass, @@ -1554,7 +1539,7 @@ fn startup_passphrase(passphrase: &str, expected_address: &str) { { log!("Log path: {}", mm.log_path.display()) } - let enable = block_on(enable_electrum(&mm, "KMD", false, &["electrum1.cipig.net:10001"], None)); + let enable = block_on(enable_electrum(&mm, "KMD", false, &["electrum1.cipig.net:10001"])); assert_eq!(expected_address, enable.address); block_on(mm.stop()).unwrap(); } @@ -1912,33 +1897,21 @@ fn test_electrum_enable_conn_errors() { let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); log!("Bob log path: {}", mm_bob.log_path.display()); // Using working servers and few else with random ports to trigger "connection refused" - block_on(enable_electrum( - &mm_bob, - "RICK", - false, - &[ - "electrum3.cipig.net:10020", - "electrum2.cipig.net:10020", - "electrum1.cipig.net:10020", - "electrum1.cipig.net:60020", - "electrum1.cipig.net:60021", - ], - None, - )); + block_on(enable_electrum(&mm_bob, "RICK", false, &[ + "electrum3.cipig.net:10020", + "electrum2.cipig.net:10020", + "electrum1.cipig.net:10020", + "electrum1.cipig.net:60020", + "electrum1.cipig.net:60021", + ])); // use random domain name to trigger name is not resolved - block_on(enable_electrum( - &mm_bob, - "MORTY", - false, - &[ - "electrum3.cipig.net:10021", - "electrum2.cipig.net:10021", - "electrum1.cipig.net:10021", - "random-electrum-domain-name1.net:60020", - "random-electrum-domain-name2.net:60020", - ], - None, - )); + block_on(enable_electrum(&mm_bob, "MORTY", false, &[ + "electrum3.cipig.net:10021", + "electrum2.cipig.net:10021", + "electrum1.cipig.net:10021", + "random-electrum-domain-name1.net:60020", + "random-electrum-domain-name2.net:60020", + ])); } #[test] @@ -1970,10 +1943,10 @@ fn test_order_should_not_be_displayed_when_node_is_down() { let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); log!("Bob log path: {}", mm_bob.log_path.display()); - let electrum_rick = block_on(enable_electrum(&mm_bob, "RICK", false, DOC_ELECTRUM_ADDRS, None)); + let electrum_rick = block_on(enable_electrum(&mm_bob, "RICK", false, DOC_ELECTRUM_ADDRS)); log!("Bob enable RICK {:?}", electrum_rick); - let electrum_morty = block_on(enable_electrum(&mm_bob, "MORTY", false, MARTY_ELECTRUM_ADDRS, None)); + let electrum_morty = block_on(enable_electrum(&mm_bob, "MORTY", false, MARTY_ELECTRUM_ADDRS)); log!("Bob enable MORTY {:?}", electrum_morty); let mm_alice = MarketMakerIt::start( @@ -1996,10 +1969,10 @@ fn test_order_should_not_be_displayed_when_node_is_down() { let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); log!("Alice log path: {}", mm_alice.log_path.display()); - let electrum_rick = block_on(enable_electrum(&mm_alice, "RICK", false, DOC_ELECTRUM_ADDRS, None)); + let electrum_rick = block_on(enable_electrum(&mm_alice, "RICK", false, DOC_ELECTRUM_ADDRS)); log!("Alice enable RICK {:?}", electrum_rick); - let electrum_morty = block_on(enable_electrum(&mm_alice, "MORTY", false, MARTY_ELECTRUM_ADDRS, None)); + let electrum_morty = block_on(enable_electrum(&mm_alice, "MORTY", false, MARTY_ELECTRUM_ADDRS)); log!("Alice enable MORTY {:?}", electrum_morty); // issue sell request on Bob side by setting base/rel price @@ -2082,10 +2055,10 @@ fn test_own_orders_should_not_be_removed_from_orderbook() { let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); log!("Bob log path: {}", mm_bob.log_path.display()); - let electrum_rick = block_on(enable_electrum(&mm_bob, "RICK", false, DOC_ELECTRUM_ADDRS, None)); + let electrum_rick = block_on(enable_electrum(&mm_bob, "RICK", false, DOC_ELECTRUM_ADDRS)); log!("Bob enable RICK {:?}", electrum_rick); - let electrum_morty = block_on(enable_electrum(&mm_bob, "MORTY", false, MARTY_ELECTRUM_ADDRS, None)); + let electrum_morty = block_on(enable_electrum(&mm_bob, "MORTY", false, MARTY_ELECTRUM_ADDRS)); log!("Bob enable MORTY {:?}", electrum_morty); // issue sell request on Bob side by setting base/rel price @@ -2137,7 +2110,7 @@ fn check_priv_key(mm: &MarketMakerIt, coin: &str, expected_priv_key: &str) { #[cfg(not(target_arch = "wasm32"))] // https://github.com/KomodoPlatform/atomicDEX-API/issues/519#issuecomment-589149811 fn test_show_priv_key() { - let coins = json!([rick_conf(), morty_conf(), eth_testnet_conf()]); + let coins = json!([rick_conf(), morty_conf(), eth_dev_conf()]); let mm = MarketMakerIt::start( json! ({ @@ -2160,7 +2133,7 @@ fn test_show_priv_key() { log!("Log path: {}", mm.log_path.display()); log!( "enable_coins: {:?}", - block_on(enable_coins_eth_electrum(&mm, ETH_SEPOLIA_NODE, None)) + block_on(enable_coins_eth_electrum(&mm, ETH_SEPOLIA_NODES)) ); check_priv_key(&mm, "RICK", "UvCjJf4dKSs2vFGVtCnUTAhR5FTZGdg43DDRa9s7s5DV1sSDX14g"); @@ -2176,7 +2149,7 @@ fn test_show_priv_key() { fn test_electrum_and_enable_response() { let coins = json! ([ {"coin":"RICK","asset":"RICK","rpcport":8923,"txversion":4,"overwintered":1,"protocol":{"type":"UTXO"},"mature_confirmations":101}, - {"coin":"ETH","name":"ethereum","protocol":{"type":"ETH"}}, + eth_dev_conf(), ]); let mm = MarketMakerIt::start( @@ -2248,7 +2221,7 @@ fn test_electrum_and_enable_response() { "userpass": mm.userpass, "method": "enable", "coin": "ETH", - "urls": ETH_SEPOLIA_NODE, + "urls": ETH_SEPOLIA_NODES, "mm2": 1, "swap_contract_address": ETH_SEPOLIA_SWAP_CONTRACT, "required_confirmations": 10, @@ -2403,7 +2376,7 @@ fn set_price_with_cancel_previous_should_broadcast_cancelled_message() { #[test] #[cfg(not(target_arch = "wasm32"))] fn test_batch_requests() { - let coins = json!([rick_conf(), morty_conf(), eth_testnet_conf(),]); + let coins = json!([rick_conf(), morty_conf(), eth_dev_conf(),]); // start bob and immediately place the order let mm_bob = MarketMakerIt::start( @@ -2501,7 +2474,7 @@ fn test_metrics_method() { let (_dump_log, _dump_dashboard) = mm.mm_dump(); log!("log path: {}", mm.log_path.display()); - let _electrum = block_on(enable_electrum(&mm, "RICK", false, DOC_ELECTRUM_ADDRS, None)); + let _electrum = block_on(enable_electrum(&mm, "RICK", false, DOC_ELECTRUM_ADDRS)); let metrics = request_metrics(&mm); assert!(!metrics.metrics.is_empty()); @@ -2553,7 +2526,7 @@ fn test_electrum_tx_history() { log!("log path: {}", mm.log_path.display()); // Enable RICK electrum client with tx_history loop. - let electrum = block_on(enable_electrum(&mm, "RICK", true, DOC_ELECTRUM_ADDRS, None)); + let electrum = block_on(enable_electrum(&mm, "RICK", true, DOC_ELECTRUM_ADDRS)); // Wait till tx_history will not be loaded block_on(mm.wait_for_log(500., |log| log.contains("history has been loaded successfully"))).unwrap(); @@ -2643,7 +2616,7 @@ fn test_convert_utxo_address() { let (_dump_log, _dump_dashboard) = mm.mm_dump(); log!("log path: {}", mm.log_path.display()); - let _electrum = block_on(enable_electrum(&mm, "BCH", false, T_BCH_ELECTRUMS, None)); + let _electrum = block_on(enable_electrum(&mm, "BCH", false, T_BCH_ELECTRUMS)); // test standard to cashaddress let rc = block_on(mm.rpc(&json! ({ @@ -2774,17 +2747,11 @@ fn test_convert_segwit_address() { let (_dump_log, _dump_dashboard) = mm.mm_dump(); log!("log path: {}", mm.log_path.display()); - let _electrum = block_on(enable_electrum( - &mm, - "tBTC", - false, - &[ - "electrum1.cipig.net:10068", - "electrum2.cipig.net:10068", - "electrum3.cipig.net:10068", - ], - None, - )); + let _electrum = block_on(enable_electrum(&mm, "tBTC", false, &[ + "electrum1.cipig.net:10068", + "electrum2.cipig.net:10068", + "electrum3.cipig.net:10068", + ])); // test standard to segwit let rc = block_on(mm.rpc(&json! ({ @@ -2870,9 +2837,7 @@ fn test_convert_segwit_address() { #[test] #[cfg(not(target_arch = "wasm32"))] fn test_convert_eth_address() { - let coins = json!([ - {"coin":"ETH","name":"ethereum","protocol":{"type":"ETH"}}, - ]); + let coins = json!([eth_dev_conf()]); // start mm and immediately place the order let mm = MarketMakerIt::start( @@ -2895,7 +2860,7 @@ fn test_convert_eth_address() { let (_dump_log, _dump_dashboard) = mm.mm_dump(); log!("log path: {}", mm.log_path.display()); - block_on(enable_native(&mm, "ETH", ETH_SEPOLIA_NODE, None)); + block_on(enable_native(&mm, "ETH", ETH_SEPOLIA_NODES, None)); // test single-case to mixed-case let rc = block_on(mm.rpc(&json! ({ @@ -2999,17 +2964,11 @@ fn test_add_delegation_qtum() { ) .unwrap(); - let json = block_on(enable_electrum( - &mm, - "tQTUM", - false, - &[ - "electrum1.cipig.net:10071", - "electrum2.cipig.net:10071", - "electrum3.cipig.net:10071", - ], - None, - )); + let json = block_on(enable_electrum(&mm, "tQTUM", false, &[ + "electrum1.cipig.net:10071", + "electrum2.cipig.net:10071", + "electrum3.cipig.net:10071", + ])); log!("{}", json.balance); let rc = block_on(mm.rpc(&json!({ @@ -3090,17 +3049,11 @@ fn test_remove_delegation_qtum() { ) .unwrap(); - let json = block_on(enable_electrum( - &mm, - "tQTUM", - false, - &[ - "electrum1.cipig.net:10071", - "electrum2.cipig.net:10071", - "electrum3.cipig.net:10071", - ], - None, - )); + let json = block_on(enable_electrum(&mm, "tQTUM", false, &[ + "electrum1.cipig.net:10071", + "electrum2.cipig.net:10071", + "electrum3.cipig.net:10071", + ])); log!("{}", json.balance); let rc = block_on(mm.rpc(&json!({ @@ -3156,17 +3109,11 @@ fn test_get_staking_infos_qtum() { ) .unwrap(); - let json = block_on(enable_electrum( - &mm, - "tQTUM", - false, - &[ - "electrum1.cipig.net:10071", - "electrum2.cipig.net:10071", - "electrum3.cipig.net:10071", - ], - None, - )); + let json = block_on(enable_electrum(&mm, "tQTUM", false, &[ + "electrum1.cipig.net:10071", + "electrum2.cipig.net:10071", + "electrum3.cipig.net:10071", + ])); log!("{}", json.balance); let rc = block_on(mm.rpc(&json!({ @@ -3334,7 +3281,7 @@ fn test_convert_qrc20_address() { #[test] #[cfg(not(target_arch = "wasm32"))] fn test_validateaddress() { - let coins = json!([rick_conf(), morty_conf(), eth_testnet_conf()]); + let coins = json!([rick_conf(), morty_conf(), eth_dev_conf()]); let (bob_file_passphrase, _bob_file_userpass) = from_env_file(slurp(&".env.seed").unwrap()); let bob_passphrase = var("BOB_PASSPHRASE") @@ -3360,7 +3307,7 @@ fn test_validateaddress() { .unwrap(); let (_dump_log, _dump_dashboard) = mm.mm_dump(); log!("Log path: {}", mm.log_path.display()); - log!("{:?}", block_on(enable_coins_eth_electrum(&mm, ETH_SEPOLIA_NODE, None))); + log!("{:?}", block_on(enable_coins_eth_electrum(&mm, ETH_SEPOLIA_NODES))); // test valid RICK address @@ -3826,7 +3773,7 @@ fn test_qrc20_withdraw_error() { fn test_get_raw_transaction() { let coins = json! ([ {"coin":"RICK","asset":"RICK","required_confirmations":0,"txversion":4,"overwintered":1,"protocol":{"type":"UTXO"}}, - {"coin":"ETH","name":"ethereum","protocol":{"type":"ETH"}}, + {"coin":"ETH","name":"ethereum","chain_id":1,"protocol":{"type":"ETH"}}, ]); let mm = MarketMakerIt::start( json! ({ @@ -3845,7 +3792,7 @@ fn test_get_raw_transaction() { let (_dump_log, _dump_dashboard) = mm.mm_dump(); log!("log path: {}", mm.log_path.display()); // RICK - let _electrum = block_on(enable_electrum(&mm, "RICK", false, DOC_ELECTRUM_ADDRS, None)); + let _electrum = block_on(enable_electrum(&mm, "RICK", false, DOC_ELECTRUM_ADDRS)); let raw = block_on(mm.rpc(&json! ({ "mmrpc": "2.0", "userpass": mm.userpass, @@ -4841,7 +4788,7 @@ fn test_orderbook_is_mine_orders() { fn test_mm2_db_migration() { let bob_passphrase = get_passphrase(&".env.seed", "BOB_PASSPHRASE").unwrap(); - let coins = json!([rick_conf(), morty_conf(), eth_testnet_conf(),]); + let coins = json!([rick_conf(), morty_conf(), eth_dev_conf(),]); let mm2_folder = new_mm2_temp_folder_path(None); let swaps_dir = mm2_folder.join(format!( @@ -4887,17 +4834,11 @@ fn test_get_current_mtp() { let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); let (_dump_log, _dump_dashboard) = mm.mm_dump(); - let electrum = block_on(enable_electrum( - &mm, - "KMD", - false, - &[ - "electrum1.cipig.net:10001", - "electrum2.cipig.net:10001", - "electrum3.cipig.net:10001", - ], - None, - )); + let electrum = block_on(enable_electrum(&mm, "KMD", false, &[ + "electrum1.cipig.net:10001", + "electrum2.cipig.net:10001", + "electrum3.cipig.net:10001", + ])); log!("{:?}", electrum); let rc = block_on(mm.rpc(&json!({ @@ -4996,36 +4937,6 @@ fn test_get_public_key_hash() { assert_eq!(v.result.public_key_hash, "b506088aa2a3b4bb1da3a29bf00ce1a550ea1df9") } -#[test] -#[cfg(not(target_arch = "wasm32"))] -fn test_get_my_address_hd() { - const PASSPHRASE: &str = "tank abandon bind salon remove wisdom net size aspect direct source fossil"; - - let coins = json!([eth_testnet_conf()]); - - let conf = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); - let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); - let (_dump_log, _dump_dashboard) = mm.mm_dump(); - log!("log path: {}", mm.log_path.display()); - - let resp = block_on(mm.rpc(&json!({ - "userpass": mm.userpass, - "mmrpc": "2.0", - "method": "get_my_address", - "params": { - "coin": "ETH", - } - }))) - .unwrap(); - - assert_eq!(resp.0, StatusCode::OK); - let my_wallet_address: Json = json::from_str(&resp.1).unwrap(); - assert_eq!( - my_wallet_address["result"]["wallet_address"], - "0x1737F1FaB40c6Fd3dc729B51C0F97DB3297CCA93" - ) -} - #[test] #[cfg(not(target_arch = "wasm32"))] fn test_get_orderbook_with_same_orderbook_ticker() { @@ -5520,7 +5431,7 @@ fn test_sign_verify_message_eth() { // Enable coins on Bob side. Print the replies in case we need the "address". log!( "enable_coins (bob): {:?}", - block_on(enable_native(&mm_bob, "ETH", ETH_SEPOLIA_NODE, None)) + block_on(enable_native(&mm_bob, "ETH", ETH_SEPOLIA_NODES, None)) ); let response = block_on(sign_message(&mm_bob, "ETH")); @@ -5556,8 +5467,8 @@ fn test_no_login() { let (_dump_log, _dump_dashboard) = no_login_node.mm_dump(); log!("log path: {}", no_login_node.log_path.display()); - block_on(enable_electrum_json(&seednode, RICK, false, doc_electrums(), None)); - block_on(enable_electrum_json(&seednode, MORTY, false, marty_electrums(), None)); + block_on(enable_electrum_json(&seednode, RICK, false, doc_electrums())); + block_on(enable_electrum_json(&seednode, MORTY, false, marty_electrums())); let orders = [ // (base, rel, price, volume, min_volume) @@ -5964,7 +5875,7 @@ fn test_enable_btc_with_sync_starting_header() { let (_dump_log, _dump_dashboard) = mm_bob.mm_dump(); log!("log path: {}", mm_bob.log_path.display()); - let utxo_bob = block_on(enable_utxo_v2_electrum(&mm_bob, "BTC", btc_electrums(), 80, None)); + let utxo_bob = block_on(enable_utxo_v2_electrum(&mm_bob, "BTC", btc_electrums(), None, 80, None)); log!("enable UTXO bob {:?}", utxo_bob); block_on(mm_bob.stop()).unwrap(); @@ -5994,7 +5905,14 @@ fn test_btc_block_header_sync() { let (_dump_log, _dump_dashboard) = mm_bob.mm_dump(); log!("log path: {}", mm_bob.log_path.display()); - let utxo_bob = block_on(enable_utxo_v2_electrum(&mm_bob, "BTC", btc_electrums(), 600, None)); + let utxo_bob = block_on(enable_utxo_v2_electrum( + &mm_bob, + "BTC", + btc_electrums(), + None, + 600, + None, + )); log!("enable UTXO bob {:?}", utxo_bob); block_on(mm_bob.stop()).unwrap(); @@ -6029,6 +5947,7 @@ fn test_tbtc_block_header_sync() { &mm_bob, "tBTC-TEST", tbtc_electrums(), + None, 100000, None, )); @@ -6039,116 +5958,134 @@ fn test_tbtc_block_header_sync() { #[test] #[cfg(not(target_arch = "wasm32"))] -fn test_enable_coins_with_enable_hd() { - const TX_HISTORY: bool = false; +fn test_enable_utxo_with_enable_hd() { const PASSPHRASE: &str = "tank abandon bind salon remove wisdom net size aspect direct source fossil"; - let coins = json!([rick_conf(), tqrc20_conf(), btc_segwit_conf(),]); + let coins = json!([rick_conf(), btc_segwit_conf(),]); - let path_to_address = StandardHDCoinAddress { - account: 0, - is_change: false, - address_index: 0, - }; + let path_to_address = HDAccountAddressId::default(); let conf_0 = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); let mm_hd_0 = MarketMakerIt::start(conf_0.conf, conf_0.rpc_password, None).unwrap(); let (_dump_log, _dump_dashboard) = mm_hd_0.mm_dump(); log!("log path: {}", mm_hd_0.log_path.display()); - let rick = block_on(enable_electrum( + let rick = block_on(enable_utxo_v2_electrum( &mm_hd_0, "RICK", - TX_HISTORY, - DOC_ELECTRUM_ADDRS, + doc_electrums(), Some(path_to_address.clone()), + 60, + None, )); - assert_eq!(rick.address, "RXNtAyDSsY3DS3VxTpJegzoHU9bUX54j56"); + let balance = match rick.wallet_balance { + EnableCoinBalance::HD(hd) => hd, + _ => panic!("Expected EnableCoinBalance::HD"), + }; + let account = balance.accounts.get(0).expect("Expected account at index 0"); + assert_eq!(account.addresses[0].address, "RXNtAyDSsY3DS3VxTpJegzoHU9bUX54j56"); + assert_eq!(account.addresses[1].address, "RVyndZp3ZrhGKSwHryyM3Kcz9aq2EJrW1z"); + let new_account = block_on(create_new_account(&mm_hd_0, "RICK", Some(77), 60)); + assert_eq!(new_account.addresses[7].address, "RLNu8gszQ8ENUrY3VSyBS2714CNVwn1f7P"); + + let btc_segwit = block_on(enable_utxo_v2_electrum( + &mm_hd_0, + "BTC-segwit", + btc_electrums(), + Some(path_to_address), + 60, + None, + )); + let balance = match btc_segwit.wallet_balance { + EnableCoinBalance::HD(hd) => hd, + _ => panic!("Expected EnableCoinBalance::HD"), + }; + let account = balance.accounts.get(0).expect("Expected account at index 0"); + // This is the enabled address, so it should be derived and added to the account + assert_eq!( + account.addresses[0].address, + "bc1q6vyur5hjul2m0979aadd6u7ptuj9ac4gt0ha0c" + ); + // The next account address have 0 balance so they are not returned from scanning, We will have to add them manually + assert!(account.addresses.get(1).is_none()); + let get_new_address_1 = block_on(get_new_address(&mm_hd_0, "BTC-segwit", 0, Some(Bip44Chain::External))); + assert_eq!( + get_new_address_1.new_address.address, + "bc1q6kxcwcrsm5z8pe940xxu294q7588mqvarttxcx" + ); + block_on(create_new_account(&mm_hd_0, "BTC-segwit", Some(77), 60)); + // The account addresses have 0 balance so they are not returned from scanning, We will have to add them manually + for _ in 0..8 { + block_on(get_new_address(&mm_hd_0, "BTC-segwit", 77, Some(Bip44Chain::External))); + } + let account_balance: HDAccountBalanceResponse = + block_on(account_balance(&mm_hd_0, "BTC-segwit", 77, Bip44Chain::External)); + assert_eq!( + account_balance.addresses[7].address, + "bc1q0dxnd7afj997a40j86a8a6dq3xs3dwm7rkzams" + ); +} + +// Todo: Ignored until enable_qtum_with_tokens is implemented, and also implemented for HD wallet using task manager. +#[test] +#[ignore] +#[cfg(not(target_arch = "wasm32"))] +fn test_enable_qrc20_with_enable_hd() { + const PASSPHRASE: &str = "tank abandon bind salon remove wisdom net size aspect direct source fossil"; + + let coins = json!([tqrc20_conf(),]); + + let path_to_address = HDAccountAddressId::default(); + let conf_0 = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); + let mm_hd_0 = MarketMakerIt::start(conf_0.conf, conf_0.rpc_password, None).unwrap(); + let (_dump_log, _dump_dashboard) = mm_hd_0.mm_dump(); + log!("log path: {}", mm_hd_0.log_path.display()); + let qrc20 = block_on(enable_qrc20( &mm_hd_0, "QRC20", QRC20_ELECTRUMS, "0xd362e096e873eb7907e205fadc6175c6fec7bc44", - Some(path_to_address.clone()), - )); - assert_eq!(qrc20["address"].as_str(), Some("qRtCTiPHW9e6zH9NcRhjMVfq7sG37SvgrL")); - let btc_segwit = block_on(enable_electrum( - &mm_hd_0, - "BTC-segwit", - TX_HISTORY, - TBTC_ELECTRUMS, Some(path_to_address), )); - assert_eq!(btc_segwit.address, "bc1q6vyur5hjul2m0979aadd6u7ptuj9ac4gt0ha0c"); + assert_eq!(qrc20["address"].as_str(), Some("qRtCTiPHW9e6zH9NcRhjMVfq7sG37SvgrL")); - let path_to_address = StandardHDCoinAddress { - account: 0, - is_change: false, - address_index: 1, + let path_to_address = HDAccountAddressId { + account_id: 0, + chain: Bip44Chain::External, + address_id: 1, }; let conf_1 = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); let mm_hd_1 = MarketMakerIt::start(conf_1.conf, conf_1.rpc_password, None).unwrap(); let (_dump_log, _dump_dashboard) = mm_hd_1.mm_dump(); log!("log path: {}", mm_hd_1.log_path.display()); - let rick = block_on(enable_electrum( - &mm_hd_1, - "RICK", - TX_HISTORY, - DOC_ELECTRUM_ADDRS, - Some(path_to_address.clone()), - )); - assert_eq!(rick.address, "RVyndZp3ZrhGKSwHryyM3Kcz9aq2EJrW1z"); let qrc20 = block_on(enable_qrc20( &mm_hd_1, "QRC20", QRC20_ELECTRUMS, "0xd362e096e873eb7907e205fadc6175c6fec7bc44", - Some(path_to_address.clone()), - )); - assert_eq!(qrc20["address"].as_str(), Some("qY8FNq2ZDUh52BjNvaroFoeHdr3AAhqsxW")); - let btc_segwit = block_on(enable_electrum( - &mm_hd_1, - "BTC-segwit", - TX_HISTORY, - TBTC_ELECTRUMS, Some(path_to_address), )); - assert_eq!(btc_segwit.address, "bc1q6kxcwcrsm5z8pe940xxu294q7588mqvarttxcx"); + assert_eq!(qrc20["address"].as_str(), Some("qY8FNq2ZDUh52BjNvaroFoeHdr3AAhqsxW")); - let path_to_address = StandardHDCoinAddress { - account: 77, - is_change: false, - address_index: 7, + let path_to_address = HDAccountAddressId { + account_id: 77, + chain: Bip44Chain::External, + address_id: 7, }; let conf_1 = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); let mm_hd_1 = MarketMakerIt::start(conf_1.conf, conf_1.rpc_password, None).unwrap(); let (_dump_log, _dump_dashboard) = mm_hd_1.mm_dump(); log!("log path: {}", mm_hd_1.log_path.display()); - let rick = block_on(enable_electrum( - &mm_hd_1, - "RICK", - TX_HISTORY, - DOC_ELECTRUM_ADDRS, - Some(path_to_address.clone()), - )); - assert_eq!(rick.address, "RLNu8gszQ8ENUrY3VSyBS2714CNVwn1f7P"); let qrc20 = block_on(enable_qrc20( &mm_hd_1, "QRC20", QRC20_ELECTRUMS, "0xd362e096e873eb7907e205fadc6175c6fec7bc44", - Some(path_to_address.clone()), - )); - assert_eq!(qrc20["address"].as_str(), Some("qREuDjyn7dzUPgnCkxPvALz9Szgy7diB5w")); - let btc_segwit = block_on(enable_electrum( - &mm_hd_1, - "BTC-segwit", - TX_HISTORY, - TBTC_ELECTRUMS, Some(path_to_address), )); - assert_eq!(btc_segwit.address, "bc1q0dxnd7afj997a40j86a8a6dq3xs3dwm7rkzams"); + assert_eq!(qrc20["address"].as_str(), Some("qREuDjyn7dzUPgnCkxPvALz9Szgy7diB5w")); } /// `shared_db_id` must be the same for Iguana and all HD accounts derived from the same passphrase. @@ -6242,7 +6179,7 @@ fn test_sign_raw_transaction_p2wpkh() { // start bob let mm_bob = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); // Enable coins on Bob side. Print the replies in case we need the "address". - let coin_init_resp = block_on(enable_electrum(&mm_bob, "tBTC-Segwit", false, TBTC_ELECTRUMS, None)); + let coin_init_resp = block_on(enable_electrum(&mm_bob, "tBTC-Segwit", false, TBTC_ELECTRUMS)); assert_eq!( coin_init_resp.result, "success", "enable_coins failed with {}", @@ -6299,9 +6236,14 @@ fn test_sign_raw_transaction_p2wpkh() { #[cfg(all(feature = "run-device-tests", not(target_arch = "wasm32")))] mod trezor_tests { - use super::enable_utxo_v2_electrum; - use coins::utxo::for_tests::test_withdraw_init_loop; + use coins::eth::{eth_coin_from_conf_and_request, EthCoin, ETH_GAS}; + use coins::for_tests::test_withdraw_init_loop; + use coins::rpc_command::account_balance::{AccountBalanceParams, AccountBalanceRpcOps}; + use coins::rpc_command::get_new_address::{GetNewAddressParams, GetNewAddressRpcOps}; + use coins::rpc_command::init_create_account::for_tests::test_create_new_account_init_loop; use coins::utxo::{utxo_standard::UtxoStandardCoin, UtxoActivationParams}; + use coins::{lp_coinfind, CoinProtocol, MmCoinEnum, PrivKeyBuildPolicy}; + use coins_activation::platform_for_tests::init_platform_coin_with_tokens_loop; use coins_activation::{for_tests::init_standalone_coin_loop, InitStandaloneCoinReq}; use common::executor::Timer; use common::serde::Deserialize; @@ -6312,9 +6254,12 @@ mod trezor_tests { use mm2_main::mm2::init_hw::init_trezor_user_action; use mm2_main::mm2::init_hw::{init_trezor, init_trezor_status, InitHwRequest, InitHwResponse}; use mm2_test_helpers::electrums::tbtc_electrums; - use mm2_test_helpers::for_tests::{init_trezor_rpc, init_trezor_status_rpc, init_trezor_user_action_rpc, - init_withdraw, mm_ctx_with_custom_db_with_conf, tbtc_legacy_conf, - tbtc_segwit_conf, withdraw_status, MarketMakerIt, Mm2TestConf}; + use mm2_test_helpers::for_tests::{enable_utxo_v2_electrum, eth_sepolia_trezor_firmware_compat_conf, + eth_testnet_conf_trezor, init_trezor_rpc, init_trezor_status_rpc, + init_trezor_user_action_rpc, init_withdraw, jst_sepolia_trezor_conf, + mm_ctx_with_custom_db_with_conf, tbtc_legacy_conf, tbtc_segwit_conf, + withdraw_status, MarketMakerIt, Mm2TestConf, ETH_SEPOLIA_NODES, + ETH_SEPOLIA_SWAP_CONTRACT}; use mm2_test_helpers::structs::{InitTaskResult, RpcV2Response, TransactionDetails, WithdrawStatus}; use rpc_task::{rpc_common::RpcTaskStatusRequest, RpcTaskStatus}; use serde_json::{self as json, json, Value as Json}; @@ -6337,7 +6282,7 @@ mod trezor_tests { let res = match init_trezor(ctx.clone(), req).await { Ok(res) => res, _ => { - panic!("cannot init trezor"); + panic!("cannot start init trezor task"); }, }; @@ -6348,9 +6293,9 @@ mod trezor_tests { forget_if_finished: false, }; match init_trezor_status(ctx.clone(), status_req).await { - Ok(res) => { - log!("trezor init status={:?}", serde_json::to_string(&res).unwrap()); - match res { + Ok(status_res) => { + log!("trezor init status={:?}", serde_json::to_string(&status_res).unwrap()); + match status_res { RpcTaskStatus::Ok(_) => { log!("device initialized"); break; @@ -6393,6 +6338,46 @@ mod trezor_tests { ctx } + /// We cannot put this code in coins/eth_tests.rs as trezor init needs some structs in mm2_main + #[test] + pub fn eth_my_balance() { + let req = json!({ + "method": "enable", + "coin": "ETH", + "urls": ETH_SEPOLIA_NODES, + "swap_contract_address": ETH_SEPOLIA_SWAP_CONTRACT, + "priv_key_policy": "Trezor", + }); + + let mut eth_conf = eth_sepolia_trezor_firmware_compat_conf(); + eth_conf["mm2"] = 2.into(); + let mm_conf = json!({ "coins": [eth_conf] }); + + let ctx = block_on(mm_ctx_with_trezor(mm_conf)); + let priv_key_policy = PrivKeyBuildPolicy::Trezor; + // this activate method does not create a default hd wallet account what is needed for trezor + // maybe make a new account as a separate call? + // for that we need get_activation_result() to be called (which calls enable_balance and then create_new_account) + let eth_coin = block_on(eth_coin_from_conf_and_request( + &ctx, + "ETH", + ð_conf, + &req, + CoinProtocol::ETH, + priv_key_policy, + )) + .unwrap(); + + let account_balance = block_on(eth_coin.account_balance_rpc(AccountBalanceParams { + account_index: 0, + chain: crypto::Bip44Chain::External, + limit: Default::default(), + paging_options: Default::default(), + })) + .unwrap(); + println!("account_balance={:?}", account_balance); + } + /// Tool to run withdraw directly with trezor device or emulator (no rpc version, added for easier debugging) /// run cargo test with '--features run-device-tests' option /// to use trezor emulator also add '--features trezor-udp' option to cargo params @@ -6425,7 +6410,8 @@ mod trezor_tests { ticker, "tb1q3zkv6g29ku3jh9vdkhxlpyek44se2s0zrv7ctn", "0.00001", - "m/84'/1'/0'/0/0", + Some("m/84'/1'/0'/0/0"), + None, )) .expect("withdraw must end successfully"); log!("tx_hex={}", serde_json::to_string(&tx_details.tx_hex).unwrap()); @@ -6523,6 +6509,7 @@ mod trezor_tests { &mm_bob, ticker, tbtc_electrums(), + None, 80, Some("Trezor"), )); @@ -6563,6 +6550,7 @@ mod trezor_tests { &mm_bob, ticker, tbtc_electrums(), + None, 80, Some("Trezor"), )); @@ -6578,4 +6566,133 @@ mod trezor_tests { log!("tx_hex={}", serde_json::to_string(&tx_details.tx_hex).unwrap()); block_on(mm_bob.stop()).unwrap(); } + + /// Test to run eth withdraw directly with trezor device or emulator (for checking or debugging) + /// run cargo test with '--features run-device-tests' option + /// to use trezor emulator also add '--features trezor-udp' option to cargo params + #[test] + fn test_eth_withdraw_from_trezor_no_rpc() { + use coins::WithdrawFee; + use std::convert::TryInto; + + let ticker_coin = "tETH"; + let ticker_token = "tJST"; + let eth_conf = eth_sepolia_trezor_firmware_compat_conf(); + let jst_conf = jst_sepolia_trezor_conf(); + let mm_conf = json!({ "coins": [eth_conf, jst_conf] }); + let ctx = block_on(mm_ctx_with_trezor(mm_conf)); + block_on(init_platform_coin_with_tokens_loop::( + ctx.clone(), + serde_json::from_value(json!({ + "ticker": ticker_coin, + "rpc_mode": "Default", + "nodes": [ + {"url": "https://rpc2.sepolia.org"}, + {"url": "https://rpc.sepolia.org/"} + ], + "swap_contract_address": ETH_SEPOLIA_SWAP_CONTRACT, + "erc20_tokens_requests": [{"ticker": ticker_token}], + "priv_key_policy": "Trezor" + })) + .unwrap(), + )) + .unwrap(); + + let coin = block_on(lp_coinfind(&ctx, ticker_coin)).unwrap(); + let eth_coin = if let Some(MmCoinEnum::EthCoin(eth_coin)) = coin { + eth_coin + } else { + panic!("eth coin not enabled"); + }; + + // try get eth balance + let _account_balance = block_on(eth_coin.account_balance_rpc(AccountBalanceParams { + account_index: 0, + chain: crypto::Bip44Chain::External, + limit: 1, + paging_options: Default::default(), + })) + .expect("account_balance result okay"); + + // try to create eth withdrawal tx + let tx_details = block_on(test_withdraw_init_loop( + ctx.clone(), + ticker_coin, + "0xc06eFafa6527fc4b3C8F69Afb173964A3780a104", + "0.00001", + None, // try withdraw from default account + Some(WithdrawFee::EthGas { + gas: ETH_GAS, + gas_price: 0.1_f32.try_into().unwrap(), + }), + )) + .expect("withdraw must end successfully"); + log!("tx_hex={}", serde_json::to_string(&tx_details.tx_hex).unwrap()); + + // create a non-default address expected as "m/44'/1'/0'/0/1" (must be topped up already) + let new_addr_params: GetNewAddressParams = serde_json::from_value(json!({ + "account_id": 0, + "chain": "External" + })) + .unwrap(); + + // TODO: ideally should be in loop to handle pin + let new_addr_resp = + block_on(eth_coin.get_new_address_rpc_without_conf(new_addr_params)).expect("new account created"); + println!("create new_addr_resp={:?}", new_addr_resp); + + // try to create JST ERC20 token withdrawal tx from a non-default account (should have some tokens on it) + let tx_details = block_on(test_withdraw_init_loop( + ctx, + ticker_token, + "0xbAB36286672fbdc7B250804bf6D14Be0dF69fa29", + "0.000000000000000001", // 1 wei + Some("m/44'/1'/0'/0/1"), // Note: Trezor uses 1' type for all testnets + Some(WithdrawFee::EthGas { + gas: ETH_GAS, + gas_price: 0.1_f32.try_into().unwrap(), + }), + )) + .expect("withdraw must end successfully"); + log!("tx_hex={}", serde_json::to_string(&tx_details.tx_hex).unwrap()); + + // if you need to send the tx: + /* let send_tx_res = block_on(send_raw_transaction(ctx, json!({ + "coin": ticker_token, + "tx_hex": tx_details.tx_hex, + }))); + assert!(send_tx_res.is_ok(), "!{} send: {:?}", ticker_token, send_tx_res); + if send_tx_res.is_ok() { + println!("tx_hash={}", tx_details.tx_hash); + } */ + } + + /// Test to create a new eth account with trezor + /// run cargo test with '--features run-device-tests' option + /// to use trezor emulator also add '--features trezor-udp' option to cargo params + #[test] + fn test_eth_create_new_account_trezor_no_rpc() { + let ticker_coin = "ETH"; + let eth_conf = eth_testnet_conf_trezor(); + let mm_conf = json!({ "coins": [eth_conf] }); + let ctx = block_on(mm_ctx_with_trezor(mm_conf)); + block_on(init_platform_coin_with_tokens_loop::( + ctx.clone(), + serde_json::from_value(json!({ + "ticker": ticker_coin, + "rpc_mode": "Default", + "nodes": [ + {"url": ETH_SEPOLIA_NODES[0]} + ], + "swap_contract_address": ETH_SEPOLIA_SWAP_CONTRACT, + "erc20_tokens_requests": [], + "priv_key_policy": "Trezor" + })) + .unwrap(), + )) + .unwrap(); + + let create_acc_res = block_on(test_create_new_account_init_loop(ctx, ticker_coin, Some(1))); + println!("create_acc_res= {:?}", create_acc_res); + } } diff --git a/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs b/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs index d27d646190..40112ea591 100644 --- a/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs @@ -282,7 +282,7 @@ fn alice_can_see_the_active_order_after_orderbook_sync_segwit() { let enable_tbtc_res: CoinInitResponse = json::from_str(&electrum.1).unwrap(); let tbtc_segwit_address = enable_tbtc_res.address; - let enable_rick_res = block_on(enable_electrum(&mm_bob, "RICK", false, DOC_ELECTRUM_ADDRS, None)); + let enable_rick_res = block_on(enable_electrum(&mm_bob, "RICK", false, DOC_ELECTRUM_ADDRS)); log!("enable RICK: {:?}", enable_rick_res); let rick_address = enable_rick_res.address; @@ -361,7 +361,7 @@ fn alice_can_see_the_active_order_after_orderbook_sync_segwit() { ); log!("enable Alice tBTC: {:?}", electrum); - let electrum = block_on(enable_electrum(&mm_alice, "RICK", false, DOC_ELECTRUM_ADDRS, None)); + let electrum = block_on(enable_electrum(&mm_alice, "RICK", false, DOC_ELECTRUM_ADDRS)); log!("enable Alice RICK: {:?}", electrum); // setting the price will trigger Alice's subscription to the orderbook topic @@ -455,7 +455,7 @@ fn test_orderbook_segwit() { let enable_tbtc_res: CoinInitResponse = json::from_str(&electrum.1).unwrap(); let tbtc_segwit_address = enable_tbtc_res.address; - let enable_rick_res = block_on(enable_electrum(&mm_bob, "RICK", false, DOC_ELECTRUM_ADDRS, None)); + let enable_rick_res = block_on(enable_electrum(&mm_bob, "RICK", false, DOC_ELECTRUM_ADDRS)); log!("enable RICK: {:?}", enable_rick_res); let rick_address = enable_rick_res.address; @@ -855,8 +855,8 @@ fn orderbook_extended_data() { .unwrap(); let (_dump_log, _dump_dashboard) = &mm.mm_dump(); log!("Log path: {}", mm.log_path.display()); - block_on(enable_electrum(&mm, "RICK", false, DOC_ELECTRUM_ADDRS, None)); - block_on(enable_electrum(&mm, "MORTY", false, MARTY_ELECTRUM_ADDRS, None)); + block_on(enable_electrum(&mm, "RICK", false, DOC_ELECTRUM_ADDRS)); + block_on(enable_electrum(&mm, "MORTY", false, MARTY_ELECTRUM_ADDRS)); let bob_orders = &[ // (base, rel, price, volume) @@ -967,8 +967,8 @@ fn orderbook_should_display_base_rel_volumes() { .unwrap(); let (_dump_log, _dump_dashboard) = &mm.mm_dump(); log!("Log path: {}", mm.log_path.display()); - block_on(enable_electrum(&mm, "RICK", false, DOC_ELECTRUM_ADDRS, None)); - block_on(enable_electrum(&mm, "MORTY", false, MARTY_ELECTRUM_ADDRS, None)); + block_on(enable_electrum(&mm, "RICK", false, DOC_ELECTRUM_ADDRS)); + block_on(enable_electrum(&mm, "MORTY", false, MARTY_ELECTRUM_ADDRS)); let price = BigRational::new(2.into(), 1.into()); let volume = BigRational::new(1.into(), 1.into()); @@ -1087,9 +1087,9 @@ fn orderbook_should_work_without_coins_activation() { let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); log!("Alice log path: {}", mm_alice.log_path.display()); - let electrum = block_on(enable_electrum(&mm_bob, "RICK", false, DOC_ELECTRUM_ADDRS, None)); + let electrum = block_on(enable_electrum(&mm_bob, "RICK", false, DOC_ELECTRUM_ADDRS)); log!("enable RICK on Bob: {:?}", electrum); - let electrum = block_on(enable_electrum(&mm_bob, "MORTY", false, MARTY_ELECTRUM_ADDRS, None)); + let electrum = block_on(enable_electrum(&mm_bob, "MORTY", false, MARTY_ELECTRUM_ADDRS)); log!("enable MORTY on Bob: {:?}", electrum); let rc = block_on(mm_bob.rpc(&json!({ @@ -1146,8 +1146,8 @@ fn test_all_orders_per_pair_per_node_must_be_displayed_in_orderbook() { .unwrap(); let (_dump_log, _dump_dashboard) = mm.mm_dump(); log!("Log path: {}", mm.log_path.display()); - block_on(enable_electrum(&mm, "RICK", false, DOC_ELECTRUM_ADDRS, None)); - block_on(enable_electrum(&mm, "MORTY", false, MARTY_ELECTRUM_ADDRS, None)); + block_on(enable_electrum(&mm, "RICK", false, DOC_ELECTRUM_ADDRS)); + block_on(enable_electrum(&mm, "MORTY", false, MARTY_ELECTRUM_ADDRS)); // set 2 orders with different prices let rc = block_on(mm.rpc(&json!({ @@ -1239,14 +1239,14 @@ fn setprice_min_volume_should_be_displayed_in_orderbook() { let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); log!("Alice log path: {}", mm_alice.log_path.display()); - let electrum = block_on(enable_electrum(&mm_bob, "RICK", false, DOC_ELECTRUM_ADDRS, None)); + let electrum = block_on(enable_electrum(&mm_bob, "RICK", false, DOC_ELECTRUM_ADDRS)); log!("enable RICK on Bob: {:?}", electrum); - let electrum = block_on(enable_electrum(&mm_bob, "MORTY", false, MARTY_ELECTRUM_ADDRS, None)); + let electrum = block_on(enable_electrum(&mm_bob, "MORTY", false, MARTY_ELECTRUM_ADDRS)); log!("enable MORTY on Bob: {:?}", electrum); - let electrum = block_on(enable_electrum(&mm_alice, "RICK", false, DOC_ELECTRUM_ADDRS, None)); + let electrum = block_on(enable_electrum(&mm_alice, "RICK", false, DOC_ELECTRUM_ADDRS)); log!("enable RICK on Alice: {:?}", electrum); - let electrum = block_on(enable_electrum(&mm_alice, "MORTY", false, MARTY_ELECTRUM_ADDRS, None)); + let electrum = block_on(enable_electrum(&mm_alice, "MORTY", false, MARTY_ELECTRUM_ADDRS)); log!("enable MORTY on Alice: {:?}", electrum); // issue orderbook call on Alice side to trigger subscription to a topic @@ -1329,7 +1329,7 @@ fn zhtlc_orders_sync_alice_connected_before_creation() { let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); log!("Alice log path: {}", mm_alice.log_path.display()); - block_on(enable_electrum_json(&mm_bob, RICK, false, doc_electrums(), None)); + block_on(enable_electrum_json(&mm_bob, RICK, false, doc_electrums())); block_on(enable_z_coin_light( &mm_bob, ZOMBIE_TICKER, @@ -1393,7 +1393,7 @@ fn zhtlc_orders_sync_alice_connected_after_creation() { let (_dump_log, _dump_dashboard) = mm_bob.mm_dump(); log!("Bob log path: {}", mm_bob.log_path.display()); - block_on(enable_electrum_json(&mm_bob, "RICK", false, doc_electrums(), None)); + block_on(enable_electrum_json(&mm_bob, "RICK", false, doc_electrums())); block_on(enable_z_coin_light( &mm_bob, ZOMBIE_TICKER, @@ -1423,7 +1423,7 @@ fn zhtlc_orders_sync_alice_connected_after_creation() { let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); log!("Alice log path: {}", mm_alice.log_path.display()); - block_on(enable_electrum_json(&mm_alice, RICK, false, doc_electrums(), None)); + block_on(enable_electrum_json(&mm_alice, RICK, false, doc_electrums())); block_on(enable_z_coin_light( &mm_alice, ZOMBIE_TICKER, diff --git a/mm2src/mm2_main/tests/mm2_tests/tendermint_tests.rs b/mm2src/mm2_main/tests/mm2_tests/tendermint_tests.rs index eb507d1a1c..eade48b2c6 100644 --- a/mm2src/mm2_main/tests/mm2_tests/tendermint_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/tendermint_tests.rs @@ -1,12 +1,12 @@ use common::{block_on, log}; -use crypto::StandardHDCoinAddress; use mm2_number::BigDecimal; use mm2_test_helpers::for_tests::{atom_testnet_conf, disable_coin, disable_coin_err, enable_tendermint, enable_tendermint_token, enable_tendermint_without_balance, get_tendermint_my_tx_history, ibc_withdraw, iris_nimda_testnet_conf, iris_testnet_conf, my_balance, send_raw_transaction, withdraw_v1, MarketMakerIt, Mm2TestConf}; -use mm2_test_helpers::structs::{RpcV2Response, TendermintActivationResult, TransactionDetails}; +use mm2_test_helpers::structs::{Bip44Chain, HDAccountAddressId, RpcV2Response, TendermintActivationResult, + TransactionDetails}; use serde_json::json; const ATOM_TEST_BALANCE_SEED: &str = "atom test seed"; @@ -172,10 +172,10 @@ fn test_tendermint_withdraw_hd() { ); // We will withdraw from HD account 0 and change 0 and address_index 1 - let path_to_address = StandardHDCoinAddress { - account: 0, - is_change: false, - address_index: 1, + let path_to_address = HDAccountAddressId { + account_id: 0, + chain: Bip44Chain::External, + address_id: 1, }; // just call withdraw without sending to check response correctness @@ -329,10 +329,10 @@ fn test_tendermint_ibc_withdraw_hd() { ); // We will withdraw from HD account 0 and change 0 and address_index 1 - let path_to_address = StandardHDCoinAddress { - account: 0, - is_change: false, - address_index: 1, + let path_to_address = HDAccountAddressId { + account_id: 0, + chain: Bip44Chain::External, + address_id: 1, }; let tx_details = block_on(ibc_withdraw( @@ -742,21 +742,9 @@ mod swap { false ))); - dbg!(block_on(enable_electrum( - &mm_bob, - "DOC", - false, - DOC_ELECTRUM_ADDRS, - None - ))); + dbg!(block_on(enable_electrum(&mm_bob, "DOC", false, DOC_ELECTRUM_ADDRS,))); - dbg!(block_on(enable_electrum( - &mm_alice, - "DOC", - false, - DOC_ELECTRUM_ADDRS, - None - ))); + dbg!(block_on(enable_electrum(&mm_alice, "DOC", false, DOC_ELECTRUM_ADDRS,))); block_on(trade_base_rel_tendermint( mm_bob, @@ -832,21 +820,9 @@ mod swap { false ))); - dbg!(block_on(enable_electrum( - &mm_bob, - "DOC", - false, - DOC_ELECTRUM_ADDRS, - None - ))); + dbg!(block_on(enable_electrum(&mm_bob, "DOC", false, DOC_ELECTRUM_ADDRS))); - dbg!(block_on(enable_electrum( - &mm_alice, - "DOC", - false, - DOC_ELECTRUM_ADDRS, - None - ))); + dbg!(block_on(enable_electrum(&mm_alice, "DOC", false, DOC_ELECTRUM_ADDRS))); block_on(trade_base_rel_tendermint( mm_bob, @@ -922,21 +898,9 @@ mod swap { false ))); - dbg!(block_on(enable_electrum( - &mm_bob, - "DOC", - false, - DOC_ELECTRUM_ADDRS, - None - ))); + dbg!(block_on(enable_electrum(&mm_bob, "DOC", false, DOC_ELECTRUM_ADDRS))); - dbg!(block_on(enable_electrum( - &mm_alice, - "DOC", - false, - DOC_ELECTRUM_ADDRS, - None - ))); + dbg!(block_on(enable_electrum(&mm_alice, "DOC", false, DOC_ELECTRUM_ADDRS))); block_on(trade_base_rel_tendermint( mm_bob, @@ -1012,21 +976,9 @@ mod swap { false ))); - dbg!(block_on(enable_electrum( - &mm_bob, - "DOC", - false, - DOC_ELECTRUM_ADDRS, - None - ))); + dbg!(block_on(enable_electrum(&mm_bob, "DOC", false, DOC_ELECTRUM_ADDRS))); - dbg!(block_on(enable_electrum( - &mm_alice, - "DOC", - false, - DOC_ELECTRUM_ADDRS, - None - ))); + dbg!(block_on(enable_electrum(&mm_alice, "DOC", false, DOC_ELECTRUM_ADDRS))); block_on(trade_base_rel_tendermint( mm_bob, diff --git a/mm2src/mm2_main/tests/mm2_tests/z_coin_tests.rs b/mm2src/mm2_main/tests/mm2_tests/z_coin_tests.rs index 997feca86a..004ee27cac 100644 --- a/mm2src/mm2_main/tests/mm2_tests/z_coin_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/z_coin_tests.rs @@ -465,7 +465,7 @@ fn trade_rick_zombie_light() { log!("Bob ZOMBIE activation {:?}", zombie_activation); - let rick_activation = block_on(enable_electrum_json(&mm_bob, RICK, false, doc_electrums(), None)); + let rick_activation = block_on(enable_electrum_json(&mm_bob, RICK, false, doc_electrums())); log!("Bob RICK activation {:?}", rick_activation); @@ -499,7 +499,7 @@ fn trade_rick_zombie_light() { log!("Alice ZOMBIE activation {:?}", zombie_activation); - let rick_activation = block_on(enable_electrum_json(&mm_alice, RICK, false, doc_electrums(), None)); + let rick_activation = block_on(enable_electrum_json(&mm_alice, RICK, false, doc_electrums())); log!("Alice RICK activation {:?}", rick_activation); diff --git a/mm2src/mm2_net/src/network_event.rs b/mm2src/mm2_net/src/network_event.rs index beee72e36f..b88655f383 100644 --- a/mm2src/mm2_net/src/network_event.rs +++ b/mm2src/mm2_net/src/network_event.rs @@ -5,7 +5,7 @@ use common::{executor::{SpawnFuture, Timer}, use futures::channel::oneshot::{self, Receiver, Sender}; use mm2_core::mm_ctx::MmArc; pub use mm2_event_stream::behaviour::EventBehaviour; -use mm2_event_stream::{behaviour::EventInitStatus, Event, EventStreamConfiguration}; +use mm2_event_stream::{behaviour::EventInitStatus, Event, EventName, EventStreamConfiguration}; use mm2_libp2p::behaviours::atomicdex; use serde_json::json; @@ -19,7 +19,7 @@ impl NetworkEvent { #[async_trait] impl EventBehaviour for NetworkEvent { - const EVENT_NAME: &'static str = "NETWORK"; + fn event_name() -> EventName { EventName::NETWORK } async fn handle(self, interval: f64, tx: oneshot::Sender) { let p2p_ctx = P2PContext::fetch_from_mm_arc(&self.ctx); @@ -47,7 +47,7 @@ impl EventBehaviour for NetworkEvent { if previously_sent != event_data { self.ctx .stream_channel_controller - .broadcast(Event::new(Self::EVENT_NAME.to_string(), event_data.to_string())) + .broadcast(Event::new(Self::event_name().to_string(), event_data.to_string())) .await; previously_sent = event_data; @@ -58,7 +58,7 @@ impl EventBehaviour for NetworkEvent { } async fn spawn_if_active(self, config: &EventStreamConfiguration) -> EventInitStatus { - if let Some(event) = config.get_event(Self::EVENT_NAME) { + if let Some(event) = config.get_event(&Self::event_name()) { info!( "NETWORK event is activated with {} seconds interval.", event.stream_interval_seconds diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index fc86b22c17..7c99beb924 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -1,5 +1,7 @@ //! Helpers used in the unit and integration tests. +#![allow(missing_docs)] + use crate::electrums::qtum_electrums; use crate::structs::*; use common::custom_futures::repeatable::{Ready, Retry}; @@ -7,7 +9,7 @@ use common::executor::Timer; use common::log::{debug, info}; use common::{cfg_native, now_float, now_ms, now_sec, repeatable, wait_until_ms, wait_until_sec, PagingOptionsEnum}; use common::{get_utc_timestamp, log}; -use crypto::{CryptoCtx, StandardHDCoinAddress}; +use crypto::CryptoCtx; use gstuff::{try_s, ERR, ERRL}; use http::{HeaderMap, StatusCode}; use lazy_static::lazy_static; @@ -21,7 +23,6 @@ use serde_json::{self as json, json, Value as Json}; use std::collections::HashMap; use std::convert::TryFrom; use std::env; -#[cfg(not(target_arch = "wasm32"))] use std::io::Write; use std::net::IpAddr; use std::num::NonZeroUsize; use std::process::Child; @@ -41,6 +42,7 @@ cfg_native! { use http::Request; use regex::Regex; use std::fs; + use std::io::Write; use std::net::Ipv4Addr; use std::path::{Path, PathBuf}; use std::process::Command; @@ -231,9 +233,11 @@ pub const TBTC_ELECTRUMS: &[&str] = &[ ]; pub const ETH_MAINNET_NODE: &str = "https://mainnet.infura.io/v3/c01c1b4cf66642528547624e1d6d9d6b"; +pub const ETH_MAINNET_CHAIN_ID: u64 = 1; pub const ETH_MAINNET_SWAP_CONTRACT: &str = "0x24abe4c71fc658c91313b6552cd40cd808b3ea80"; -pub const ETH_SEPOLIA_NODE: &[&str] = &["https://rpc2.sepolia.org"]; +pub const ETH_SEPOLIA_NODES: &[&str] = &["https://rpc2.sepolia.org"]; +pub const ETH_SEPOLIA_CHAIN_ID: u64 = 11155111; pub const ETH_SEPOLIA_SWAP_CONTRACT: &str = "0xeA6D65434A15377081495a9E7C5893543E7c32cB"; pub const ETH_SEPOLIA_TOKEN_CONTRACT: &str = "0x09d0d71FBC00D7CCF9CFf132f5E6825C88293F19"; @@ -777,15 +781,17 @@ pub fn tbtc_legacy_conf() -> Json { }) } -pub fn eth_testnet_conf() -> Json { +pub fn eth_testnet_conf_trezor() -> Json { json!({ "coin": "ETH", "name": "ethereum", "mm2": 1, - "derivation_path": "m/44'/60'", + "chain_id": 1337, + "derivation_path": "m/44'/1'", // Trezor uses coin type 1 for testnet "protocol": { "type": "ETH" - } + }, + "trezor_coin": "Ethereum" }) } @@ -842,10 +848,25 @@ pub fn eth_sepolia_conf() -> Json { json!({ "coin": "ETH", "name": "ethereum", + "derivation_path": "m/44'/60'", "chain_id": 11155111, "protocol": { "type": "ETH" - } + }, + "trezor_coin": "Ethereum" + }) +} + +pub fn eth_sepolia_trezor_firmware_compat_conf() -> Json { + json!({ + "coin": "tETH", + "name": "ethereum", + "derivation_path": "m/44'/1'", // Note: trezor uses coin type 1' for eth for testnet (SLIP44_TESTNET) + "chain_id": 11155111, + "protocol": { + "type": "ETH" + }, + "trezor_coin": "tETH" }) } @@ -853,6 +874,7 @@ pub fn eth_jst_testnet_conf() -> Json { json!({ "coin": "JST", "name": "jst", + "chain_id": 1337, "derivation_path": "m/44'/60'", "protocol": { "type": "ERC20", @@ -880,6 +902,24 @@ pub fn jst_sepolia_conf() -> Json { }) } +pub fn jst_sepolia_trezor_conf() -> Json { + json!({ + "coin": "tJST", + "name": "tjst", + "chain_id": 11155111, + "derivation_path": "m/44'/1'", // Note: Trezor uses 1' coin type for all testnets + "trezor_coin": "tETH", + "protocol": { + "type": "ERC20", + "protocol_data": { + "platform": "ETH", + "chain_id": 11155111, + "contract_address": ETH_SEPOLIA_TOKEN_CONTRACT + } + } + }) +} + pub fn iris_testnet_conf() -> Json { json!({ "coin": "IRIS-TEST", @@ -1724,26 +1764,14 @@ pub fn mm_spat() -> (&'static str, MarketMakerIt, RaiiDump, RaiiDump) { /// Asks MM to enable the given currency in electrum mode /// fresh list of servers at https://github.com/jl777/coins/blob/master/electrums/. -pub async fn enable_electrum( - mm: &MarketMakerIt, - coin: &str, - tx_history: bool, - urls: &[&str], - path_to_address: Option, -) -> Json { +pub async fn enable_electrum(mm: &MarketMakerIt, coin: &str, tx_history: bool, urls: &[&str]) -> Json { let servers = urls.iter().map(|url| json!({ "url": url })).collect(); - enable_electrum_json(mm, coin, tx_history, servers, path_to_address).await + enable_electrum_json(mm, coin, tx_history, servers).await } /// Asks MM to enable the given currency in electrum mode /// fresh list of servers at https://github.com/jl777/coins/blob/master/electrums/. -pub async fn enable_electrum_json( - mm: &MarketMakerIt, - coin: &str, - tx_history: bool, - servers: Vec, - path_to_address: Option, -) -> Json { +pub async fn enable_electrum_json(mm: &MarketMakerIt, coin: &str, tx_history: bool, servers: Vec) -> Json { let electrum = mm .rpc(&json!({ "userpass": mm.userpass, @@ -1752,7 +1780,6 @@ pub async fn enable_electrum_json( "servers": servers, "mm2": 1, "tx_history": tx_history, - "path_to_address": path_to_address.unwrap_or_default(), })) .await .unwrap(); @@ -1771,7 +1798,7 @@ pub async fn enable_qrc20( coin: &str, urls: &[&str], swap_contract_address: &str, - path_to_address: Option, + path_to_address: Option, ) -> Json { let servers: Vec<_> = urls.iter().map(|url| json!({ "url": url })).collect(); let electrum = mm @@ -1858,7 +1885,7 @@ pub async fn enable_native( mm: &MarketMakerIt, coin: &str, urls: &[&str], - path_to_address: Option, + path_to_address: Option, ) -> Json { let native = mm .rpc(&json!({ @@ -1902,28 +1929,6 @@ pub async fn enable_eth_coin( json::from_str(&enable.1).unwrap() } -pub async fn enable_eth_coin_hd( - mm: &MarketMakerIt, - coin: &str, - urls: &[&str], - swap_contract_address: &str, - path_to_address: Option, -) -> Json { - let enable = mm - .rpc(&json!({ - "userpass": mm.userpass, - "method": "enable", - "coin": coin, - "urls": urls, - "swap_contract_address": swap_contract_address, - "mm2": 1, - "path_to_address": path_to_address.unwrap_or_default(), - })) - .await - .unwrap(); - assert_eq!(enable.0, StatusCode::OK, "'enable' failed: {}", enable.1); - json::from_str(&enable.1).unwrap() -} pub async fn enable_spl(mm: &MarketMakerIt, coin: &str) -> Json { let req = json!({ @@ -2007,7 +2012,7 @@ pub async fn enable_bch_with_tokens( tokens: &[&str], mode: UtxoRpcMode, tx_history: bool, - path_to_address: Option, + path_to_address: Option, ) -> Json { let slp_requests: Vec<_> = tokens.iter().map(|ticker| json!({ "ticker": ticker })).collect(); @@ -2573,7 +2578,7 @@ pub async fn withdraw_v1( coin: &str, to: &str, amount: &str, - from: Option, + from: Option, ) -> TransactionDetails { let request = mm .rpc(&json!({ @@ -2596,7 +2601,7 @@ pub async fn ibc_withdraw( coin: &str, to: &str, amount: &str, - from: Option, + from: Option, ) -> TransactionDetails { let request = mm .rpc(&json!({ @@ -3004,13 +3009,15 @@ pub async fn init_utxo_electrum( mm: &MarketMakerIt, coin: &str, servers: Vec, + path_to_address: Option, priv_key_policy: Option<&str>, ) -> Json { let mut activation_params = json!({ "mode": { "rpc": "Electrum", "rpc_data": { - "servers": servers + "servers": servers, + "path_to_address": path_to_address, } } }); @@ -3059,6 +3066,119 @@ pub async fn init_utxo_status(mm: &MarketMakerIt, task_id: u64) -> Json { json::from_str(&request.1).unwrap() } +pub async fn enable_utxo_v2_electrum( + mm: &MarketMakerIt, + coin: &str, + servers: Vec, + path_to_address: Option, + timeout: u64, + priv_key_policy: Option<&str>, +) -> UtxoStandardActivationResult { + let init = init_utxo_electrum(mm, coin, servers, path_to_address, priv_key_policy).await; + let init: RpcV2Response = json::from_value(init).unwrap(); + let timeout = wait_until_ms(timeout * 1000); + + loop { + if now_ms() > timeout { + panic!("{} initialization timed out", coin); + } + + let status = init_utxo_status(mm, init.result.task_id).await; + let status: RpcV2Response = json::from_value(status).unwrap(); + log!("init_utxo_status: {:?}", status); + match status.result { + InitUtxoStatus::Ok(result) => break result, + InitUtxoStatus::Error(e) => panic!("{} initialization error {:?}", coin, e), + _ => Timer::sleep(1.).await, + } + } +} + +pub async fn init_eth_with_tokens( + mm: &MarketMakerIt, + platform_coin: &str, + tokens: &[&str], + swap_contract_address: &str, + nodes: &[&str], + path_to_address: Option, +) -> Json { + let erc20_tokens_requests: Vec<_> = tokens.iter().map(|ticker| json!({ "ticker": ticker })).collect(); + let nodes: Vec<_> = nodes.iter().map(|url| json!({ "url": url })).collect(); + + let response = mm + .rpc(&json!({ + "userpass": mm.userpass, + "method": "task::enable_eth::init", + "mmrpc": "2.0", + "params": { + "ticker": platform_coin, + "swap_contract_address": swap_contract_address, + "nodes": nodes, + "tx_history": true, + "erc20_tokens_requests": erc20_tokens_requests, + "path_to_address": path_to_address.unwrap_or_default(), + } + })) + .await + .unwrap(); + assert_eq!( + response.0, + StatusCode::OK, + "'task::enable_eth::init' failed: {}", + response.1 + ); + json::from_str(&response.1).unwrap() +} + +pub async fn init_eth_with_tokens_status(mm: &MarketMakerIt, task_id: u64) -> Json { + let request = mm + .rpc(&json!({ + "userpass": mm.userpass, + "method": "task::enable_eth::status", + "mmrpc": "2.0", + "params": { + "task_id": task_id, + } + })) + .await + .unwrap(); + assert_eq!( + request.0, + StatusCode::OK, + "'task::enable_eth::status' failed: {}", + request.1 + ); + json::from_str(&request.1).unwrap() +} + +pub async fn enable_eth_with_tokens_v2( + mm: &MarketMakerIt, + platform_coin: &str, + tokens: &[&str], + swap_contract_address: &str, + nodes: &[&str], + timeout: u64, + path_to_address: Option, +) -> EthWithTokensActivationResult { + let init = init_eth_with_tokens(mm, platform_coin, tokens, swap_contract_address, nodes, path_to_address).await; + let init: RpcV2Response = json::from_value(init).unwrap(); + let timeout = wait_until_ms(timeout * 1000); + + loop { + if now_ms() > timeout { + panic!("{} initialization timed out", platform_coin); + } + + let status = init_eth_with_tokens_status(mm, init.result.task_id).await; + let status: RpcV2Response = json::from_value(status).unwrap(); + match status.result { + InitEthWithTokensStatus::Ok(result) => break result, + InitEthWithTokensStatus::Error(e) => panic!("{} initialization error {:?}", platform_coin, e), + _ => Timer::sleep(1.).await, + } + } +} + /// Note that mm2 ignores `volume` if `max` is true. pub async fn set_price( mm: &MarketMakerIt, @@ -3364,6 +3484,93 @@ pub async fn enable_z_coin_light( } } +pub async fn get_new_address( + mm: &MarketMakerIt, + coin: &str, + account_id: u32, + chain: Option, +) -> GetNewAddressResponse { + let request = json!({ + "userpass": mm.userpass, + "method": "get_new_address", + "mmrpc": "2.0", + "params": { + "coin": coin, + "account_id": account_id, + "chain": chain + } + }); + + let request = mm.rpc(&request).await.unwrap(); + assert_eq!(request.0, StatusCode::OK, "'get_new_address' failed: {}", request.1); + let response: RpcV2Response = json::from_str(&request.1).unwrap(); + response.result +} + +pub async fn account_balance( + mm: &MarketMakerIt, + coin: &str, + account_index: u32, + chain: Bip44Chain, +) -> HDAccountBalanceResponse { + let request = json!({ + "userpass": mm.userpass, + "method": "account_balance", + "mmrpc": "2.0", + "params": { + "coin": coin, + "account_index": account_index, + "chain": chain + } + }); + + let request = mm.rpc(&request).await.unwrap(); + assert_eq!(request.0, StatusCode::OK, "'account_balance' failed: {}", request.1); + let response: RpcV2Response = json::from_str(&request.1).unwrap(); + response.result +} + +pub async fn init_create_new_account(mm: &MarketMakerIt, coin: &str, account_id: Option) -> Json { + let request = json!({ + "userpass": mm.userpass, + "method": "task::create_new_account::init", + "mmrpc": "2.0", + "params": { + "coin": coin, + "account_id": account_id + } + }); + + let request = mm.rpc(&request).await.unwrap(); + assert_eq!( + request.0, + StatusCode::OK, + "'task::create_new_account::init' failed: {}", + request.1 + ); + json::from_str(&request.1).unwrap() +} + +pub async fn create_new_account_status(mm: &MarketMakerIt, task_id: u64) -> Json { + let request = json!({ + "userpass": mm.userpass, + "method": "task::create_new_account::status", + "mmrpc": "2.0", + "params": { + "task_id": task_id, + } + }); + + let request = mm.rpc(&request).await.unwrap(); + assert_eq!( + request.0, + StatusCode::OK, + "'task::create_new_account::status' failed: {}", + request.1 + ); + json::from_str(&request.1).unwrap() +} + #[test] #[cfg(not(target_arch = "wasm32"))] fn test_parse_env_file() { diff --git a/mm2src/mm2_test_helpers/src/structs.rs b/mm2src/mm2_test_helpers/src/structs.rs index 579431f843..a8742d4ea2 100644 --- a/mm2src/mm2_test_helpers/src/structs.rs +++ b/mm2src/mm2_test_helpers/src/structs.rs @@ -492,6 +492,13 @@ pub struct IguanaWalletBalance { pub balance: CoinBalance, } +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct IguanaWalletBalanceMap { + pub address: String, + pub balance: HashMap, +} + #[derive(Clone, Debug, Deserialize, Serialize)] pub enum Bip44Chain { External = 0, @@ -504,6 +511,12 @@ pub struct HDWalletBalance { pub accounts: Vec, } +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct HDWalletBalanceMap { + pub accounts: Vec, +} + #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] pub struct HDAccountBalance { @@ -513,6 +526,15 @@ pub struct HDAccountBalance { pub addresses: Vec, } +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct HDAccountBalanceMap { + pub account_index: u32, + pub derivation_path: String, + pub total_balance: HashMap, + pub addresses: Vec, +} + #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] pub struct HDAddressBalance { @@ -522,6 +544,15 @@ pub struct HDAddressBalance { pub balance: CoinBalance, } +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct HDAddressBalanceMap { + pub address: String, + pub derivation_path: String, + pub chain: Bip44Chain, + pub balance: HashMap, +} + #[derive(Clone, Debug, Deserialize, Serialize)] pub struct HDAccountAddressId { pub account_id: u32, @@ -529,6 +560,16 @@ pub struct HDAccountAddressId { pub address_id: u32, } +impl Default for HDAccountAddressId { + fn default() -> Self { + HDAccountAddressId { + account_id: 0, + chain: Bip44Chain::External, + address_id: 0, + } + } +} + #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields, tag = "wallet_type")] pub enum EnableCoinBalance { @@ -536,6 +577,13 @@ pub enum EnableCoinBalance { HD(HDWalletBalance), } +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields, tag = "wallet_type")] +pub enum EnableCoinBalanceMap { + Iguana(IguanaWalletBalanceMap), + HD(HDWalletBalanceMap), +} + /// The `FirstSyncBlock` struct contains details about the block block that is used to start the synchronization /// process. /// It includes information about the requested block height, whether it predates the Sapling activation, and the @@ -569,6 +617,26 @@ pub struct ZCoinActivationResult { pub first_sync_block: Option, } +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct GetNewAddressResponse { + pub new_address: HDAddressBalance, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct HDAccountBalanceResponse { + pub account_index: u32, + pub derivation_path: String, + pub addresses: Vec, + pub page_balance: CoinBalance, + pub limit: usize, + pub skipped: u32, + pub total: u32, + pub total_pages: usize, + pub paging_options: PagingOptionsEnum, +} + #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] pub struct CoinActivationResult { @@ -636,6 +704,15 @@ pub enum InitUtxoStatus { UserActionRequired(Json), } +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields, tag = "status", content = "details")] +pub enum InitEthWithTokensStatus { + Ok(EthWithTokensActivationResult), + Error(Json), + InProgress(Json), + UserActionRequired(Json), +} + #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields, tag = "status", content = "details")] pub enum InitLightningStatus { @@ -645,6 +722,23 @@ pub enum InitLightningStatus { UserActionRequired(Json), } +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields, tag = "status", content = "details")] +pub enum CreateNewAccountStatus { + Ok(HDAccountBalance), + Error(Json), + InProgress(Json), + UserActionRequired(Json), +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +#[serde(untagged)] +pub enum WithdrawFrom { + AddressId(HDAccountAddressId), + DerivationPath { derivation_path: String }, +} + #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields, tag = "status", content = "details")] pub enum WithdrawStatus { @@ -785,13 +879,30 @@ pub type TokenBalances = HashMap; #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] -pub struct EnableEthWithTokensResponse { +pub struct IguanaEthWithTokensActivationResult { pub current_block: u64, pub eth_addresses_infos: HashMap>, pub erc20_addresses_infos: HashMap>, pub nfts_infos: Json, } +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct HDEthWithTokensActivationResult { + pub current_block: u64, + pub ticker: String, + pub wallet_balance: EnableCoinBalanceMap, + pub nfts_infos: Json, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +#[serde(untagged)] +pub enum EthWithTokensActivationResult { + Iguana(IguanaEthWithTokensActivationResult), + HD(HDEthWithTokensActivationResult), +} + #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] pub struct EnableBchWithTokensResponse { diff --git a/mm2src/trezor/Cargo.toml b/mm2src/trezor/Cargo.toml index 6ba813f5f9..76fd39f5f8 100644 --- a/mm2src/trezor/Cargo.toml +++ b/mm2src/trezor/Cargo.toml @@ -19,6 +19,11 @@ rand = { version = "0.7", features = ["std", "wasm-bindgen"] } rpc_task = { path = "../rpc_task" } serde = "1.0" serde_derive = "1.0" +ethcore-transaction = { git = "https://github.com/KomodoPlatform/mm2-parity-ethereum.git" } +ethereum-types = { version = "0.13", default-features = false, features = ["std", "serialize"] } +ethkey = { git = "https://github.com/KomodoPlatform/mm2-parity-ethereum.git" } +bip32 = { version = "0.2.2", default-features = false, features = ["alloc", "secp256k1-ffi"] } +lazy_static = "1.4" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] bip32 = { version = "0.2.2", default-features = false, features = ["alloc", "secp256k1-ffi"] } diff --git a/mm2src/trezor/build.rs b/mm2src/trezor/build.rs index c40792c165..01634fc3dc 100644 --- a/mm2src/trezor/build.rs +++ b/mm2src/trezor/build.rs @@ -1,11 +1,15 @@ #[allow(dead_code)] -const PROTOS: [&str; 4] = [ +const PROTOS: [&str; 6] = [ "proto/messages.proto", "proto/messages-common.proto", "proto/messages-management.proto", "proto/messages-bitcoin.proto", + "proto/messages-ethereum-definitions.proto", + "proto/messages-ethereum.proto", ]; +/// Note this builder is not used and .proto files are just for info of message layouts. +/// Instead message structs are created manually and auto derivation macro prost::Message was added fn main() { // prost_build::compile_protos(&PROTOS, &["proto"]).unwrap(); } diff --git a/mm2src/trezor/proto/messages-ethereum-definitions.proto b/mm2src/trezor/proto/messages-ethereum-definitions.proto new file mode 100644 index 0000000000..d770032db0 --- /dev/null +++ b/mm2src/trezor/proto/messages-ethereum-definitions.proto @@ -0,0 +1,60 @@ +syntax = "proto2"; +package hw.trezor.messages.ethereum_definitions; + +// Sugar for easier handling in Java +option java_package = "com.satoshilabs.trezor.lib.protobuf"; +option java_outer_classname = "TrezorMessageEthereumDefinitions"; + + +/** + * Ethereum definitions type enum. + * Used to check the encoded EthereumNetworkInfo or EthereumTokenInfo message. + */ + enum EthereumDefinitionType { + NETWORK = 0; + TOKEN = 1; +} + +/** + * Ethereum network definition. Used to (de)serialize the definition. + * + * Definition types should not be cross-parseable, i.e., it should not be possible to + * incorrectly parse network info as token info or vice versa. + * To achieve that, the first field is wire type varint while the second field is wire type + * length-delimited. Both are a mismatch for the token definition. + * + * @embed + */ +message EthereumNetworkInfo { + required uint64 chain_id = 1; + required string symbol = 2; + required uint32 slip44 = 3; + required string name = 4; +} + +/** + * Ethereum token definition. Used to (de)serialize the definition. + * + * Definition types should not be cross-parseable, i.e., it should not be possible to + * incorrectly parse network info as token info or vice versa. + * To achieve that, the first field is wire type length-delimited while the second field + * is wire type varint. Both are a mismatch for the network definition. + * + * @embed + */ +message EthereumTokenInfo { + required bytes address = 1; + required uint64 chain_id = 2; + required string symbol = 3; + required uint32 decimals = 4; + required string name = 5; +} + +/** + * Contains an encoded Ethereum network and/or token definition. See ethereum-definitions.md for details. + * @embed + */ +message EthereumDefinitions { + optional bytes encoded_network = 1; // encoded Ethereum network + optional bytes encoded_token = 2; // encoded Ethereum token +} diff --git a/mm2src/trezor/proto/messages-ethereum.proto b/mm2src/trezor/proto/messages-ethereum.proto new file mode 100644 index 0000000000..dae98de869 --- /dev/null +++ b/mm2src/trezor/proto/messages-ethereum.proto @@ -0,0 +1,181 @@ +syntax = "proto2"; +package hw.trezor.messages.ethereum; + +// Sugar for easier handling in Java +option java_package = "com.satoshilabs.trezor.lib.protobuf"; +option java_outer_classname = "TrezorMessageEthereum"; + +import "messages-common.proto"; +import "messages-ethereum-definitions.proto"; + + +/** + * Request: Ask device for public key corresponding to address_n path + * @start + * @next EthereumPublicKey + * @next Failure + */ +message EthereumGetPublicKey { + repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node + optional bool show_display = 2; // optionally show on display before sending the result +} + +/** + * Response: Contains public key derived from device private seed + * @end + */ +message EthereumPublicKey { + required hw.trezor.messages.common.HDNodeType node = 1; // BIP32 public node + required string xpub = 2; // serialized form of public node +} + +/** + * Request: Ask device for Ethereum address corresponding to address_n path + * @start + * @next EthereumAddress + * @next Failure + */ +message EthereumGetAddress { + repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node + optional bool show_display = 2; // optionally show on display before sending the result + optional bytes encoded_network = 3; // encoded Ethereum network, see ethereum-definitions.md for details + optional bool chunkify = 4; // display the address in chunks of 4 characters +} + +/** + * Response: Contains an Ethereum address derived from device private seed + * @end + */ +message EthereumAddress { + optional bytes _old_address = 1 [deprecated=true]; // trezor <1.8.0, <2.1.0 - raw bytes of Ethereum address + optional string address = 2; // Ethereum address as hex-encoded string +} + +/** + * Request: Ask device to sign transaction + * gas_price, gas_limit and chain_id must be provided and non-zero. + * All other fields are optional and default to value `0` if missing. + * Note: the first at most 1024 bytes of data MUST be transmitted as part of this message. + * @start + * @next EthereumTxRequest + * @next Failure + */ +message EthereumSignTx { + repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node + optional bytes nonce = 2 [default='']; // <=256 bit unsigned big endian + required bytes gas_price = 3; // <=256 bit unsigned big endian (in wei) + required bytes gas_limit = 4; // <=256 bit unsigned big endian + optional string to = 11 [default='']; // recipient address + optional bytes value = 6 [default='']; // <=256 bit unsigned big endian (in wei) + optional bytes data_initial_chunk = 7 [default='']; // The initial data chunk (<= 1024 bytes) + optional uint32 data_length = 8 [default=0]; // Length of transaction payload + required uint64 chain_id = 9; // Chain Id for EIP 155 + optional uint32 tx_type = 10; // Used for Wanchain + optional ethereum_definitions.EthereumDefinitions definitions = 12; // network and/or token definitions for tx + optional bool chunkify = 13; // display the address in chunks of 4 characters +} + +/** + * Request: Ask device to sign EIP1559 transaction + * Note: the first at most 1024 bytes of data MUST be transmitted as part of this message. + * @start + * @next EthereumTxRequest + * @next Failure + */ +message EthereumSignTxEIP1559 { + repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node + required bytes nonce = 2; // <=256 bit unsigned big endian + required bytes max_gas_fee = 3; // <=256 bit unsigned big endian (in wei) + required bytes max_priority_fee = 4; // <=256 bit unsigned big endian (in wei) + required bytes gas_limit = 5; // <=256 bit unsigned big endian + optional string to = 6 [default='']; // recipient address + required bytes value = 7; // <=256 bit unsigned big endian (in wei) + optional bytes data_initial_chunk = 8 [default='']; // The initial data chunk (<= 1024 bytes) + required uint32 data_length = 9; // Length of transaction payload + required uint64 chain_id = 10; // Chain Id for EIP 155 + repeated EthereumAccessList access_list = 11; // Access List + optional ethereum_definitions.EthereumDefinitions definitions = 12; // network and/or token definitions for tx + optional bool chunkify = 13; // display the address in chunks of 4 characters + + message EthereumAccessList { + required string address = 1; + repeated bytes storage_keys = 2; + } +} + +/** + * Response: Device asks for more data from transaction payload, or returns the signature. + * If data_length is set, device awaits that many more bytes of payload. + * Otherwise, the signature_* fields contain the computed transaction signature. All three fields will be present. + * @end + * @next EthereumTxAck + */ +message EthereumTxRequest { + optional uint32 data_length = 1; // Number of bytes being requested (<= 1024) + optional uint32 signature_v = 2; // Computed signature (recovery parameter, limited to 27 or 28) + optional bytes signature_r = 3; // Computed signature R component (256 bit) + optional bytes signature_s = 4; // Computed signature S component (256 bit) +} + +/** + * Request: Transaction payload data. + * @next EthereumTxRequest + */ +message EthereumTxAck { + required bytes data_chunk = 1; // Bytes from transaction payload (<= 1024 bytes) +} + +/** + * Request: Ask device to sign message + * @start + * @next EthereumMessageSignature + * @next Failure + */ +message EthereumSignMessage { + repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node + required bytes message = 2; // message to be signed + optional bytes encoded_network = 3; // encoded Ethereum network, see ethereum-definitions.md for details +} + +/** + * Response: Signed message + * @end + */ +message EthereumMessageSignature { + required bytes signature = 2; // signature of the message + required string address = 3; // address used to sign the message +} + +/** + * Request: Ask device to verify message + * @start + * @next Success + * @next Failure + */ +message EthereumVerifyMessage { + required bytes signature = 2; // signature to verify + required bytes message = 3; // message to verify + required string address = 4; // address to verify +} + +/** + * Request: Ask device to sign hash of typed data + * @start + * @next EthereumTypedDataSignature + * @next Failure + */ +message EthereumSignTypedHash { + repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node + required bytes domain_separator_hash = 2; // Hash of domainSeparator of typed data to be signed + optional bytes message_hash = 3; // Hash of the data of typed data to be signed (empty if domain-only data) + optional bytes encoded_network = 4; // encoded Ethereum network, see ethereum-definitions.md for details +} + +/** + * Response: Signed typed data + * @end + */ +message EthereumTypedDataSignature { + required bytes signature = 1; // signature of the typed data + required string address = 2; // address used to sign the typed data +} diff --git a/mm2src/trezor/src/eth/definitions/sepolia.dat b/mm2src/trezor/src/eth/definitions/sepolia.dat new file mode 100644 index 0000000000..7666b9da4a Binary files /dev/null and b/mm2src/trezor/src/eth/definitions/sepolia.dat differ diff --git a/mm2src/trezor/src/eth/eth_command.rs b/mm2src/trezor/src/eth/eth_command.rs new file mode 100644 index 0000000000..c9629c5f49 --- /dev/null +++ b/mm2src/trezor/src/eth/eth_command.rs @@ -0,0 +1,216 @@ +use crate::proto::{messages_ethereum as proto_ethereum, messages_ethereum_definitions as proto_ethereum_definitions}; +use crate::response_processor::ProcessTrezorResponse; +use crate::result_handler::ResultHandler; +use crate::{serialize_derivation_path, OperationFailure, TrezorError, TrezorResponse, TrezorResult, TrezorSession}; +use ethcore_transaction::{signature, Action, Transaction as UnSignedEthTx, UnverifiedTransaction as UnverifiedEthTx}; +use ethereum_types::H256; +use ethkey::Signature; +use hw_common::primitives::{DerivationPath, XPub}; +use lazy_static::lazy_static; +use mm2_err_handle::map_mm_error::MapMmError; +use mm2_err_handle::or_mm_error::OrMmError; +use mm2_err_handle::prelude::MmError; +use std::collections::BTreeMap; + +type ChainId = u64; +type StaticDefinitionBytes = &'static [u8]; +type StaticAddressBytes = &'static [u8]; + +// new supported eth networks: +const SEPOLIA_ID: u64 = 11155111; + +lazy_static! { + + // External eth network definitions + static ref ETH_NETWORK_DEFS: BTreeMap = [ + (SEPOLIA_ID, SEPOLIA_NETWORK_DEF.as_ref()) + ].iter().cloned().collect(); + + // External eth token definitions + static ref ETH_TOKEN_DEFS: BTreeMap = [ + ].iter().cloned().collect(); + + static ref SEPOLIA_NETWORK_DEF: Vec = include_bytes!("definitions/sepolia.dat").to_vec(); + // add more files with external network or token definitions +} + +/// Get external network definition by chain id +/// check this doc how to find network definition files https://docs.trezor.io/trezor-firmware/common/ethereum-definitions.html +fn get_eth_network_def(chain_id: ChainId) -> Option> { + ETH_NETWORK_DEFS + .iter() + .find(|(id, _def)| id == &&chain_id) + .map(|found| found.1.to_vec()) +} + +/// Get external token definition by token contract address and chain id +/// check this doc how to find token definition files https://docs.trezor.io/trezor-firmware/common/ethereum-definitions.html +#[allow(dead_code)] +fn get_eth_token_def(address_bytes: &[u8], chain_id: ChainId) -> Option> { + ETH_TOKEN_DEFS + .iter() + .find(|(address, def)| address == &&address_bytes && def.0 == chain_id) + .map(|found| found.1 .1.to_vec()) +} + +/// trim leading zeros in array +macro_rules! trim_left { + ($param:expr) => {{ + $param.iter().skip_while(|el| el == &&0).cloned().collect::>() + }}; +} + +impl<'a> TrezorSession<'a> { + /// Retrieves the EVM address associated with a given derivation path from the Trezor device. + pub async fn get_eth_address<'b>( + &'b mut self, + derivation_path: DerivationPath, + show_display: bool, + ) -> TrezorResult>> { + let req = proto_ethereum::EthereumGetAddress { + address_n: derivation_path.iter().map(|child| child.0).collect(), + show_display: Some(show_display), + encoded_network: None, + chunkify: None, + }; + let result_handler = ResultHandler::new(|m: proto_ethereum::EthereumAddress| Ok(m.address)); + self.call(req, result_handler).await + } + + /// Retrieves the EVM public key associated with a given derivation path from the Trezor device. + pub async fn get_eth_public_key<'b>( + &'b mut self, + derivation_path: &DerivationPath, + show_display: bool, + ) -> TrezorResult> { + let req = proto_ethereum::EthereumGetPublicKey { + address_n: serialize_derivation_path(derivation_path), + show_display: Some(show_display), + }; + let result_handler = ResultHandler::new(|m: proto_ethereum::EthereumPublicKey| Ok(m.xpub)); + self.call(req, result_handler).await + } + + /// Signs a transaction for any EVM-based chain using the Trezor device. + pub async fn sign_eth_tx( + &mut self, + derivation_path: &DerivationPath, + unsigned_tx: &UnSignedEthTx, + chain_id: u64, + ) -> TrezorResult { + let mut data: Vec = vec![]; + let req = to_sign_eth_message(unsigned_tx, derivation_path, chain_id, &mut data); + let processor = self + .processor + .as_ref() + .or_mm_err(|| TrezorError::InternalNoProcessor)? + .clone(); + let mut tx_request = self + .send_sign_eth_tx(req) + .await? + .process(processor.clone()) + .await + .mm_err(|e| TrezorError::Internal(e.to_string()))?; + + while let Some(data_length) = tx_request.data_length { + if data_length > 0 { + let req = proto_ethereum::EthereumTxAck { + data_chunk: data.splice(..data_length as usize, []).collect(), + }; + tx_request = self + .send_eth_tx_ack(req) + .await? + .process(processor.clone()) + .await + .mm_err(|e| TrezorError::Internal(e.to_string()))?; + } else { + break; + } + } + + let sig = extract_eth_signature(&tx_request)?; + Ok(unsigned_tx.clone().with_signature(sig, Some(chain_id))) + } + + async fn send_sign_eth_tx<'b>( + &'b mut self, + req: proto_ethereum::EthereumSignTx, + ) -> TrezorResult> { + let result_handler = ResultHandler::::new(Ok); + self.call(req, result_handler).await + } + + async fn send_eth_tx_ack<'b>( + &'b mut self, + req: proto_ethereum::EthereumTxAck, + ) -> TrezorResult> { + let result_handler = ResultHandler::::new(Ok); + self.call(req, result_handler).await + } +} + +fn to_sign_eth_message( + unsigned_tx: &UnSignedEthTx, + derivation_path: &DerivationPath, + chain_id: u64, + data: &mut Vec, +) -> proto_ethereum::EthereumSignTx { + // if we have it, pass network or token definition info to show on the device screen: + let eth_defs = proto_ethereum_definitions::EthereumDefinitions { + encoded_network: get_eth_network_def(chain_id), + encoded_token: None, // TODO add looking for tokens defs + }; + + let mut nonce: [u8; 32] = [0; 32]; + let mut gas_price: [u8; 32] = [0; 32]; + let mut gas_limit: [u8; 32] = [0; 32]; + let mut value: [u8; 32] = [0; 32]; + + unsigned_tx.nonce.to_big_endian(&mut nonce); + unsigned_tx.gas_price.to_big_endian(&mut gas_price); + unsigned_tx.gas.to_big_endian(&mut gas_limit); + unsigned_tx.value.to_big_endian(&mut value); + + let addr_hex = if let Action::Call(addr) = unsigned_tx.action { + Some(format!("{:X}", addr)) // Trezor works okay with both '0x' prefixed and non-prefixed addresses in hex + } else { + None + }; + *data = unsigned_tx.data.clone(); + let data_length = if data.is_empty() { None } else { Some(data.len() as u32) }; + proto_ethereum::EthereumSignTx { + address_n: serialize_derivation_path(derivation_path), + nonce: Some(trim_left!(nonce)), + gas_price: trim_left!(gas_price), + gas_limit: trim_left!(gas_limit), + to: addr_hex, + value: Some(trim_left!(value)), + data_initial_chunk: Some(data.splice(..std::cmp::min(1024, data.len()), []).collect()), + data_length, + chain_id, + tx_type: None, + definitions: Some(eth_defs), + chunkify: if data.is_empty() { None } else { Some(true) }, + } +} + +fn extract_eth_signature(tx_request: &proto_ethereum::EthereumTxRequest) -> TrezorResult { + match ( + tx_request.signature_r.as_ref(), + tx_request.signature_s.as_ref(), + tx_request.signature_v, + ) { + (Some(r), Some(s), Some(v)) => { + let v_refined = signature::check_replay_protection(v as u64); // remove replay protection added by trezor as the ethcore lib will add it itself + if v_refined == 4 { + return Err(MmError::new(TrezorError::Failure(OperationFailure::InvalidSignature))); + } + Ok(Signature::from_rsv( + &H256::from_slice(r.as_slice()), + &H256::from_slice(s.as_slice()), + v_refined, + )) + }, + (_, _, _) => Err(MmError::new(TrezorError::Failure(OperationFailure::InvalidSignature))), + } +} diff --git a/mm2src/trezor/src/eth/mod.rs b/mm2src/trezor/src/eth/mod.rs new file mode 100644 index 0000000000..29e80663d6 --- /dev/null +++ b/mm2src/trezor/src/eth/mod.rs @@ -0,0 +1 @@ +mod eth_command; diff --git a/mm2src/trezor/src/lib.rs b/mm2src/trezor/src/lib.rs index c3847c0542..37c4bd9b4c 100644 --- a/mm2src/trezor/src/lib.rs +++ b/mm2src/trezor/src/lib.rs @@ -3,6 +3,7 @@ pub mod client; pub mod device_info; pub mod error; +pub mod eth; mod proto; pub mod response; mod response_processor; @@ -28,3 +29,12 @@ pub(crate) fn ecdsa_curve_to_string(curve: EcdsaCurve) -> String { EcdsaCurve::Secp256k1 => "secp256k1".to_owned(), } } + +/// Currently implemented trezor message types +pub enum TrezorMessageType { + /// Utxo based coins + Bitcoin, + /// Eth coin and tokens + Ethereum, + // ... +} diff --git a/mm2src/trezor/src/proto/messages_ethereum.rs b/mm2src/trezor/src/proto/messages_ethereum.rs new file mode 100644 index 0000000000..0593bed546 --- /dev/null +++ b/mm2src/trezor/src/proto/messages_ethereum.rs @@ -0,0 +1,141 @@ +///* +/// Request: Ask device for Ethereum address corresponding to address_n path +/// @start +/// @next EthereumAddress +/// @next Failure +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EthereumGetAddress { + /// BIP-32 path to derive the key from master node + #[prost(uint32, repeated, tag = "1")] + pub address_n: ::prost::alloc::vec::Vec, + /// optionally show on display before sending the result + #[prost(bool, optional, tag = "2")] + pub show_display: ::std::option::Option, + /// encoded Ethereum network, see ethereum-definitions.md for details + #[prost(bytes = "vec", optional, tag = "3")] + pub encoded_network: ::std::option::Option<::prost::alloc::vec::Vec>, + /// display the address in chunks of 4 characters + #[prost(bool, optional, tag = "4")] + pub chunkify: ::std::option::Option, +} + +///* +/// Response: Contains an Ethereum address derived from device private seed +/// @end +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EthereumAddress { + /// trezor <1.8.0, <2.1.0 - raw bytes of Ethereum address + #[prost(bytes = "vec", optional, tag = "1")] + pub encoded_network: ::std::option::Option<::prost::alloc::vec::Vec>, + /// Ethereum address as hex-encoded string + #[prost(string, optional, tag = "2")] + pub address: ::core::option::Option<::prost::alloc::string::String>, +} + +///* +/// Request: Ask device for public key corresponding to address_n path +/// @start +/// @next EthereumPublicKey +/// @next Failure +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EthereumGetPublicKey { + // BIP-32 path to derive the key from master node + #[prost(uint32, repeated, tag = "1")] + pub address_n: ::prost::alloc::vec::Vec, // repeated uint32 address_n = 1; + // optionally show on display before sending the result + #[prost(bool, optional, tag = "2")] + pub show_display: ::std::option::Option, // optional bool show_display = 2; +} + +///* +/// Response: Contains public key derived from device private seed +/// @end +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EthereumPublicKey { + // BIP32 public node + #[prost(message, required, tag = "1")] + pub node: super::messages_common::HdNodeType, // required hw.trezor.messages.common.HDNodeType node = 1; + // serialized form of public node + #[prost(string, required, tag = "2")] + pub xpub: ::prost::alloc::string::String, // required string xpub = 2; +} + +///* +/// Request: Ask device to sign transaction +/// gas_price, gas_limit and chain_id must be provided and non-zero. +/// All other fields are optional and default to value `0` if missing. +/// Note: the first at most 1024 bytes of data MUST be transmitted as part of this message. +/// @start +/// @next EthereumTxRequest +/// @next Failure +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EthereumSignTx { + /// BIP-32 path to derive the key from master node + #[prost(uint32, repeated, tag = "1")] + pub address_n: ::prost::alloc::vec::Vec, + /// <=256 bit unsigned big endian + #[prost(bytes = "vec", optional, tag = "2", default = "b\"\"")] + pub nonce: ::core::option::Option<::prost::alloc::vec::Vec>, + /// <=256 bit unsigned big endian (in wei) + #[prost(bytes = "vec", required, tag = "3")] + pub gas_price: ::prost::alloc::vec::Vec, + /// <=256 bit unsigned big endian + #[prost(bytes = "vec", required, tag = "4")] + pub gas_limit: ::prost::alloc::vec::Vec, + /// recipient address + #[prost(string, optional, tag = "11", default = "")] + pub to: ::core::option::Option<::prost::alloc::string::String>, + /// <=256 bit unsigned big endian (in wei) + #[prost(bytes = "vec", optional, tag = "6", default = "b\"\"")] + pub value: ::core::option::Option<::prost::alloc::vec::Vec>, + /// The initial data chunk (<= 1024 bytes) + #[prost(bytes = "vec", optional, tag = "7", default = "b\"\"")] + pub data_initial_chunk: ::core::option::Option<::prost::alloc::vec::Vec>, + /// Length of transaction payload + #[prost(uint32, optional, tag = "8", default = 0)] + pub data_length: ::core::option::Option, + /// Chain Id for EIP 155 + #[prost(uint64, required, tag = "9")] + pub chain_id: u64, + /// Used for Wanchain + #[prost(uint32, optional, tag = "10")] + pub tx_type: ::core::option::Option, + /// network and/or token definitions for tx + #[prost(message, optional, tag = "12")] + pub definitions: ::core::option::Option, + /// display the address in chunks of 4 characters + #[prost(bool, optional, tag = "13")] + pub chunkify: ::std::option::Option, +} + +///* +/// Response: Device asks for more data from transaction payload, or returns the signature. +/// If data_length is set, device awaits that many more bytes of payload. +/// Otherwise, the signature_* fields contain the computed transaction signature. All three fields will be present. +/// @end +/// @next EthereumTxAck +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EthereumTxRequest { + /// Number of bytes being requested (<= 1024) + #[prost(uint32, optional, tag = "1")] + pub data_length: ::std::option::Option, + /// Computed signature (recovery parameter, limited to 27 or 28) + #[prost(uint32, optional, tag = "2")] + pub signature_v: ::std::option::Option, + /// Computed signature R component (256 bit) + #[prost(bytes = "vec", optional, tag = "3")] + pub signature_r: ::std::option::Option<::prost::alloc::vec::Vec>, + /// Computed signature S component (256 bit) + #[prost(bytes = "vec", optional, tag = "4")] + pub signature_s: ::std::option::Option<::prost::alloc::vec::Vec>, +} + +///* +/// Request: Transaction payload data. +/// @next EthereumTxRequest +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EthereumTxAck { + /// Bytes from transaction payload (<= 1024 bytes) + #[prost(bytes = "vec", required, tag = "1")] + pub data_chunk: ::prost::alloc::vec::Vec, +} diff --git a/mm2src/trezor/src/proto/messages_ethereum_definitions.rs b/mm2src/trezor/src/proto/messages_ethereum_definitions.rs new file mode 100644 index 0000000000..0671d47ad2 --- /dev/null +++ b/mm2src/trezor/src/proto/messages_ethereum_definitions.rs @@ -0,0 +1,58 @@ +///* +/// Ethereum network definition. Used to (de)serialize the definition. +/// Must be signed by vendor signatures and could be found on the trezor web site +/// +/// Definition types should not be cross-parseable, i.e., it should not be possible to +/// incorrectly parse network info as token info or vice versa. +/// To achieve that, the first field is wire type varint while the second field is wire type +/// length-delimited. Both are a mismatch for the token definition. +/// +/// @embed +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EthereumNetworkInfo { + #[prost(uint64, required, tag = "1")] + pub chain_id: u64, + #[prost(string, required, tag = "2")] + pub symbol: ::prost::alloc::string::String, + #[prost(uint32, required, tag = "3")] + pub slip44: u32, + #[prost(string, required, tag = "4")] + pub name: ::prost::alloc::string::String, +} + +///* +/// Ethereum token definition. Used to (de)serialize the definition. +/// Must be signed by vendor signatures and could be found on the trezor web site +/// +/// Definition types should not be cross-parseable, i.e., it should not be possible to +/// incorrectly parse network info as token info or vice versa. +/// To achieve that, the first field is wire type length-delimited while the second field +/// is wire type varint. Both are a mismatch for the network definition. +/// +/// @embed +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EthereumTokenInfo { + #[prost(bytes = "vec", required, tag = "1")] + pub address: ::prost::alloc::vec::Vec, + #[prost(uint64, required, tag = "2")] + pub chain_id: u64, + #[prost(string, required, tag = "3")] + pub symbol: ::prost::alloc::string::String, + #[prost(uint32, required, tag = "4")] + pub decimals: u32, + #[prost(string, required, tag = "5")] + pub name: ::prost::alloc::string::String, +} + +///* +/// Ethereum definitions +/// @embed +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EthereumDefinitions { + /// encoded Ethereum network + #[prost(bytes = "vec", optional, tag = "1")] + pub encoded_network: ::core::option::Option<::prost::alloc::vec::Vec>, + /// encoded Ethereum token + #[prost(bytes = "vec", optional, tag = "2")] + pub encoded_token: ::core::option::Option<::prost::alloc::vec::Vec>, +} diff --git a/mm2src/trezor/src/proto/mod.rs b/mm2src/trezor/src/proto/mod.rs index ab80efff8f..fcd3a18efe 100644 --- a/mm2src/trezor/src/proto/mod.rs +++ b/mm2src/trezor/src/proto/mod.rs @@ -6,6 +6,8 @@ use prost::bytes::BytesMut; pub mod messages; pub mod messages_bitcoin; pub mod messages_common; +pub mod messages_ethereum; +pub mod messages_ethereum_definitions; pub mod messages_management; /// This is needed by generated protobuf modules. @@ -14,6 +16,7 @@ pub(crate) use messages_common as common; use messages::MessageType; use messages_bitcoin::*; use messages_common::*; +use messages_ethereum::*; use messages_management::*; /// This macro provides the TrezorMessage trait for a protobuf message. @@ -100,3 +103,12 @@ trezor_message_impl!(TxAckPrevMeta, MessageType::TxAck); trezor_message_impl!(TxAckPrevInput, MessageType::TxAck); trezor_message_impl!(TxAckPrevOutput, MessageType::TxAck); trezor_message_impl!(TxAckPrevExtraData, MessageType::TxAck); + +// Ethereum +trezor_message_impl!(EthereumSignTx, MessageType::EthereumSignTx); +trezor_message_impl!(EthereumTxRequest, MessageType::EthereumTxRequest); +trezor_message_impl!(EthereumTxAck, MessageType::EthereumTxAck); +trezor_message_impl!(EthereumGetAddress, MessageType::EthereumGetAddress); +trezor_message_impl!(EthereumAddress, MessageType::EthereumAddress); +trezor_message_impl!(EthereumGetPublicKey, MessageType::EthereumGetPublicKey); +trezor_message_impl!(EthereumPublicKey, MessageType::EthereumPublicKey); diff --git a/mm2src/trezor/src/transport/udp.rs b/mm2src/trezor/src/transport/udp.rs index d47ae1f31c..f1cbd1f832 100644 --- a/mm2src/trezor/src/transport/udp.rs +++ b/mm2src/trezor/src/transport/udp.rs @@ -61,7 +61,6 @@ async fn find_devices() -> TrezorResult> { let link = UdpLink::open(&dest).await?; if link.ping().await? { devices.push(UdpAvailableDevice { - // model: Model::TrezorEmulator, debug, transport: UdpTransport { protocol: ProtocolV1 { link },