diff --git a/Cargo.lock b/Cargo.lock index 1dffb90764..bd65fc42a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1097,7 +1097,7 @@ dependencies = [ "gstuff", "hex 0.4.2", "http 0.2.1", - "itertools 0.9.0", + "itertools 0.10.3", "js-sys", "jsonrpc-core 8.0.1", "keys", @@ -1215,7 +1215,7 @@ dependencies = [ "hyper", "hyper-rustls 0.22.1", "indexmap", - "itertools 0.8.2", + "itertools 0.10.3", "js-sys", "keys", "lazy_static", @@ -3201,15 +3201,6 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135" -[[package]] -name = "itertools" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.9.0" diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index 4cad7f95e2..859f69b3c1 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -45,7 +45,7 @@ futures = { version = "0.3", package = "futures", features = ["compat", "async-a gstuff = { version = "0.7", features = ["nightly"] } hex = "0.4.2" http = "0.2" -itertools = "0.9" +itertools = { version = "0.10", features = ["use_std"] } jsonrpc-core = "8.0.1" keys = { path = "../mm2_bitcoin/keys" } lazy_static = "1.4" diff --git a/mm2src/coins/coin_balance.rs b/mm2src/coins/coin_balance.rs index 8067e216fe..49b5f472e8 100644 --- a/mm2src/coins/coin_balance.rs +++ b/mm2src/coins/coin_balance.rs @@ -1,103 +1,19 @@ use crate::hd_pubkey::HDXPubExtractor; -use crate::hd_wallet::{AddressDerivingError, HDWalletCoinOps, InvalidBip44ChainError, NewAccountCreatingError}; -use crate::{lp_coinfind_or_err, BalanceError, BalanceResult, CoinBalance, CoinFindError, CoinWithDerivationMethod, - DerivationMethod, HDAddress, MarketCoinOps, MmCoinEnum, UnexpectedDerivationMethod}; +use crate::hd_wallet::{HDWalletCoinOps, NewAccountCreatingError}; +use crate::{BalanceError, BalanceResult, CoinBalance, CoinWithDerivationMethod, DerivationMethod, HDAddress, + MarketCoinOps}; use async_trait::async_trait; +use common::custom_iter::TryUnzip; use common::log::{debug, info}; -use common::mm_ctx::MmArc; use common::mm_error::prelude::*; -use common::{HttpStatusCode, PagingOptionsEnum}; use crypto::{Bip44Chain, RpcDerivationPath}; use derive_more::Display; use futures::compat::Future01CompatExt; -use http::StatusCode; use std::fmt; use std::ops::Range; pub type AddressIdRange = Range; -#[derive(Debug, Display, Serialize, SerializeErrorType)] -#[serde(tag = "error_type", content = "error_data")] -pub enum HDAccountBalanceRpcError { - #[display(fmt = "No such coin {}", coin)] - NoSuchCoin { coin: String }, - #[display(fmt = "Coin is expected to be activated with the HD wallet derivation method")] - CoinIsActivatedNotWithHDWallet, - #[display(fmt = "HD account '{}' is not activated", account_id)] - UnknownAccount { account_id: u32 }, - #[display(fmt = "Coin doesn't support the given BIP44 chain: {:?}", chain)] - InvalidBip44Chain { chain: Bip44Chain }, - #[display(fmt = "Error deriving an address: {}", _0)] - ErrorDerivingAddress(String), - #[display(fmt = "Wallet storage error: {}", _0)] - WalletStorageError(String), - #[display(fmt = "Electrum/Native RPC invalid response: {}", _0)] - RpcInvalidResponse(String), - #[display(fmt = "Transport: {}", _0)] - Transport(String), - #[display(fmt = "Internal: {}", _0)] - Internal(String), -} - -impl HttpStatusCode for HDAccountBalanceRpcError { - fn status_code(&self) -> StatusCode { - match self { - HDAccountBalanceRpcError::NoSuchCoin { .. } - | HDAccountBalanceRpcError::CoinIsActivatedNotWithHDWallet - | HDAccountBalanceRpcError::UnknownAccount { .. } - | HDAccountBalanceRpcError::InvalidBip44Chain { .. } - | HDAccountBalanceRpcError::ErrorDerivingAddress(_) => StatusCode::BAD_REQUEST, - HDAccountBalanceRpcError::Transport(_) - | HDAccountBalanceRpcError::WalletStorageError(_) - | HDAccountBalanceRpcError::RpcInvalidResponse(_) - | HDAccountBalanceRpcError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, - } - } -} - -impl From for HDAccountBalanceRpcError { - fn from(e: CoinFindError) -> Self { - match e { - CoinFindError::NoSuchCoin { coin } => HDAccountBalanceRpcError::NoSuchCoin { coin }, - } - } -} - -impl From for HDAccountBalanceRpcError { - fn from(e: UnexpectedDerivationMethod) -> Self { - match e { - UnexpectedDerivationMethod::HDWalletUnavailable => HDAccountBalanceRpcError::CoinIsActivatedNotWithHDWallet, - unexpected_error => HDAccountBalanceRpcError::Internal(unexpected_error.to_string()), - } - } -} - -impl From for HDAccountBalanceRpcError { - fn from(e: BalanceError) -> Self { - match e { - BalanceError::Transport(transport) => HDAccountBalanceRpcError::Transport(transport), - BalanceError::InvalidResponse(rpc) => HDAccountBalanceRpcError::RpcInvalidResponse(rpc), - BalanceError::UnexpectedDerivationMethod(der_method) => HDAccountBalanceRpcError::from(der_method), - BalanceError::WalletStorageError(e) => HDAccountBalanceRpcError::Internal(e), - BalanceError::Internal(internal) => HDAccountBalanceRpcError::Internal(internal), - } - } -} - -impl From for HDAccountBalanceRpcError { - fn from(e: InvalidBip44ChainError) -> Self { HDAccountBalanceRpcError::InvalidBip44Chain { chain: e.chain } } -} - -impl From for HDAccountBalanceRpcError { - fn from(e: AddressDerivingError) -> Self { - match e { - AddressDerivingError::Bip32Error(bip32) => { - HDAccountBalanceRpcError::ErrorDerivingAddress(bip32.to_string()) - }, - } - } -} - #[derive(Display)] pub enum EnableCoinBalanceError { NewAccountCreatingError(NewAccountCreatingError), @@ -146,56 +62,6 @@ pub struct HDAddressBalance { pub balance: CoinBalance, } -#[derive(Deserialize)] -pub struct HDAccountBalanceRequest { - coin: String, - #[serde(flatten)] - params: AccountBalanceParams, -} - -#[derive(Deserialize)] -pub struct AccountBalanceParams { - pub account_index: u32, - pub chain: Bip44Chain, - #[serde(default = "common::ten")] - pub limit: usize, - #[serde(default)] - pub paging_options: PagingOptionsEnum, -} - -#[derive(Deserialize)] -pub struct CheckHDAccountBalanceRequest { - coin: String, - #[serde(flatten)] - params: CheckHDAccountBalanceParams, -} - -#[derive(Deserialize)] -pub struct CheckHDAccountBalanceParams { - pub account_index: u32, - pub gap_limit: Option, -} - -#[derive(Debug, PartialEq, Serialize)] -pub struct HDAccountBalanceResponse { - pub account_index: u32, - pub derivation_path: RpcDerivationPath, - 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, PartialEq, Serialize)] -pub struct CheckHDAccountBalanceResponse { - pub account_index: u32, - pub derivation_path: RpcDerivationPath, - pub new_addresses: Vec, -} - #[derive(Clone, Copy, Debug, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum EnableCoinScanPolicy { @@ -299,27 +165,37 @@ pub trait HDWalletBalanceOps: HDWalletCoinOps { address_ids: Ids, ) -> BalanceResult> where - Self::Address: fmt::Display, + Self::Address: fmt::Display + Clone, Ids: Iterator + Send, { - let (lower, upper) = address_ids.size_hint(); - let max_addresses = upper.unwrap_or(lower); - - let mut balances = Vec::with_capacity(max_addresses); - for address_id in address_ids { - let HDAddress { - address, - derivation_path, - .. - } = self.derive_address(hd_account, chain, address_id)?; - let balance = self.known_address_balance(&address).await?; - balances.push(HDAddressBalance { + let (addresses, der_paths) = address_ids + .into_iter() + .map(|address_id| -> BalanceResult<_> { + let HDAddress { + address, + derivation_path, + .. + } = self.derive_address(hd_account, chain, address_id)?; + Ok((address, derivation_path)) + }) + // Try to unzip `Result<(Address, DerivationPath)>` elements into `Result<(Vec
, Vec)>`. + .try_unzip::, Vec<_>>()?; + + let balances = self + .known_addresses_balances(addresses) + .await? + .into_iter() + // [`HDWalletBalanceOps::known_addresses_balances`] returns pairs `(Address, CoinBalance)` + // that are guaranteed to be in the same order in which they were requested. + // 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(), derivation_path: RpcDerivationPath(derivation_path), chain, balance, - }); - } + }) + .collect(); Ok(balances) } @@ -328,6 +204,13 @@ pub trait HDWalletBalanceOps: HDWalletCoinOps { /// 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; + /// 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>; + /// 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( @@ -356,48 +239,9 @@ pub enum AddressBalanceStatus { NotUsed, } -#[async_trait] -pub trait HDWalletBalanceRpcOps { - async fn account_balance_rpc( - &self, - params: AccountBalanceParams, - ) -> MmResult; - - async fn scan_for_new_addresses_rpc( - &self, - params: CheckHDAccountBalanceParams, - ) -> MmResult; -} - -pub async fn account_balance( - ctx: MmArc, - req: HDAccountBalanceRequest, -) -> MmResult { - let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; - match coin { - MmCoinEnum::UtxoCoin(utxo) => utxo.account_balance_rpc(req.params).await, - MmCoinEnum::QtumCoin(qtum) => qtum.account_balance_rpc(req.params).await, - _ => MmError::err(HDAccountBalanceRpcError::CoinIsActivatedNotWithHDWallet), - } -} - -pub async fn scan_for_new_addresses( - ctx: MmArc, - req: CheckHDAccountBalanceRequest, -) -> MmResult { - let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; - match coin { - MmCoinEnum::UtxoCoin(utxo) => utxo.scan_for_new_addresses_rpc(req.params).await, - MmCoinEnum::QtumCoin(qtum) => qtum.scan_for_new_addresses_rpc(req.params).await, - _ => MmError::err(HDAccountBalanceRpcError::CoinIsActivatedNotWithHDWallet), - } -} - pub mod common_impl { use super::*; use crate::hd_wallet::{HDAccountOps, HDWalletOps}; - use common::calc_total_pages; - use std::ops::DerefMut; pub(crate) async fn enable_hd_account( coin: &Coin, @@ -483,80 +327,4 @@ pub mod common_impl { Ok(result) } - - pub async fn account_balance_rpc( - coin: &Coin, - params: AccountBalanceParams, - ) -> MmResult - where - Coin: HDWalletBalanceOps + CoinWithDerivationMethod::HDWallet> + Sync, - ::Address: fmt::Display, - { - let hd_wallet = coin.derivation_method().hd_wallet_or_err()?; - - let account_id = params.account_index; - let chain = params.chain; - let hd_account = hd_wallet - .get_account(account_id) - .await - .or_mm_err(|| HDAccountBalanceRpcError::UnknownAccount { account_id })?; - let total_addresses_number = hd_account.known_addresses_number(params.chain)?; - - let from_address_id = match params.paging_options { - PagingOptionsEnum::FromId(from_address_id) => from_address_id, - PagingOptionsEnum::PageNumber(page_number) => ((page_number.get() - 1) * params.limit) as u32, - }; - let to_address_id = std::cmp::min(from_address_id + params.limit as u32, total_addresses_number); - - let addresses = coin - .known_addresses_balances_with_ids(&hd_account, 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 result = HDAccountBalanceResponse { - account_index: params.account_index, - derivation_path: RpcDerivationPath(hd_account.account_derivation_path()), - addresses, - page_balance, - limit: params.limit, - skipped: std::cmp::min(from_address_id, total_addresses_number), - total: total_addresses_number, - total_pages: calc_total_pages(total_addresses_number as usize, params.limit), - paging_options: params.paging_options, - }; - - Ok(result) - } - - pub async fn scan_for_new_addresses_rpc( - coin: &Coin, - params: CheckHDAccountBalanceParams, - ) -> MmResult - where - Coin: CoinWithDerivationMethod::HDWallet> + HDWalletBalanceOps + Sync, - ::Address: fmt::Display, - { - let hd_wallet = coin.derivation_method().hd_wallet_or_err()?; - - let account_id = params.account_index; - let mut hd_account = hd_wallet - .get_account_mut(account_id) - .await - .or_mm_err(|| HDAccountBalanceRpcError::CoinIsActivatedNotWithHDWallet)?; - let account_derivation_path = hd_account.account_derivation_path(); - let address_scanner = coin.produce_hd_address_scanner().await?; - let gap_limit = params.gap_limit.unwrap_or_else(|| hd_wallet.gap_limit()); - - let new_addresses = coin - .scan_for_new_addresses(hd_wallet, hd_account.deref_mut(), &address_scanner, gap_limit) - .await?; - - Ok(CheckHDAccountBalanceResponse { - account_index: account_id, - derivation_path: RpcDerivationPath(account_derivation_path), - new_addresses, - }) - } } diff --git a/mm2src/coins/lightning.rs b/mm2src/coins/lightning.rs index 8a05148ac4..2ae45bac25 100644 --- a/mm2src/coins/lightning.rs +++ b/mm2src/coins/lightning.rs @@ -9,7 +9,7 @@ mod ln_utils; use super::{lp_coinfind_or_err, DerivationMethod, MmCoinEnum}; use crate::utxo::rpc_clients::UtxoRpcClientEnum; use crate::utxo::utxo_common::{big_decimal_from_sat_unsigned, UtxoTxBuilder}; -use crate::utxo::{sat_from_big_decimal, BlockchainNetwork, FeePolicy, UtxoCommonOps, UtxoTxGenerationOps}; +use crate::utxo::{sat_from_big_decimal, BlockchainNetwork, FeePolicy, GetUtxoListOps, UtxoTxGenerationOps}; use crate::{BalanceFut, CoinBalance, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MarketCoinOps, MmCoin, NegotiateSwapContractAddrErr, RawTransactionFut, RawTransactionRequest, SignatureError, SignatureResult, SwapOps, TradeFee, TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionEnum, @@ -822,7 +822,7 @@ 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.iguana_or_err()?; - let (unspents, _) = platform_coin.list_unspent_ordered(my_address).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), diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 4f1ab1153c..f21a6604c2 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -216,12 +216,11 @@ pub mod eth; pub mod hd_pubkey; pub mod hd_wallet; pub mod hd_wallet_storage; -pub mod init_create_account; -pub mod init_withdraw; #[cfg(not(target_arch = "wasm32"))] pub mod lightning; #[cfg_attr(target_arch = "wasm32", allow(dead_code, unused_imports))] pub mod my_tx_history_v2; pub mod qrc20; +pub mod rpc_command; #[cfg(not(target_arch = "wasm32"))] pub mod sql_tx_history_storage; #[doc(hidden)] @@ -244,11 +243,12 @@ pub mod utxo; use eth::{eth_coin_from_conf_and_request, EthCoin, EthTxFeeDetails, SignedEthTx}; use hd_wallet::{HDAddress, HDAddressId}; -use init_create_account::{CreateAccountTaskManager, CreateAccountTaskManagerShared}; -use init_withdraw::{WithdrawTaskManager, WithdrawTaskManagerShared}; use qrc20::Qrc20ActivationParams; use qrc20::{qrc20_coin_from_conf_and_params, Qrc20Coin, Qrc20FeeDetails}; use qtum::{Qrc20AddressError, ScriptHashTypeNotSupported}; +use rpc_command::init_create_account::{CreateAccountTaskManager, CreateAccountTaskManagerShared}; +use rpc_command::init_scan_for_new_addresses::{ScanAddressesTaskManager, ScanAddressesTaskManagerShared}; +use rpc_command::init_withdraw::{WithdrawTaskManager, WithdrawTaskManagerShared}; use utxo::bch::{bch_coin_from_conf_and_params, BchActivationRequest, BchCoin}; use utxo::qtum::{self, qtum_coin_with_priv_key, QtumCoin}; use utxo::qtum::{QtumDelegationOps, QtumDelegationRequest, QtumStakingInfosDetails}; @@ -1895,6 +1895,7 @@ pub struct CoinsContext { balance_update_handlers: AsyncMutex>>, withdraw_task_manager: WithdrawTaskManagerShared, create_account_manager: CreateAccountTaskManagerShared, + scan_addresses_manager: ScanAddressesTaskManagerShared, #[cfg(target_arch = "wasm32")] tx_history_db: SharedDb, #[cfg(target_arch = "wasm32")] @@ -1920,6 +1921,7 @@ impl CoinsContext { balance_update_handlers: AsyncMutex::new(vec![]), withdraw_task_manager: WithdrawTaskManager::new_shared(), create_account_manager: CreateAccountTaskManager::new_shared(), + scan_addresses_manager: ScanAddressesTaskManager::new_shared(), #[cfg(target_arch = "wasm32")] tx_history_db: ConstructibleDb::new_shared(ctx), #[cfg(target_arch = "wasm32")] diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index b3adf2841e..7bf69b646d 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -4,13 +4,15 @@ use crate::qrc20::rpc_clients::{LogEntry, Qrc20ElectrumOps, Qrc20NativeOps, Qrc2 use crate::utxo::qtum::QtumBasedCoin; use crate::utxo::rpc_clients::{ElectrumClient, NativeClient, UnspentInfo, UtxoRpcClientEnum, UtxoRpcClientOps, UtxoRpcError, UtxoRpcFut, UtxoRpcResult}; +#[cfg(not(target_arch = "wasm32"))] +use crate::utxo::tx_cache::{UtxoVerboseCacheOps, UtxoVerboseCacheShared}; use crate::utxo::utxo_builder::{UtxoCoinBuildError, UtxoCoinBuildResult, UtxoCoinBuilderCommonOps, UtxoCoinWithIguanaPrivKeyBuilder, UtxoFieldsWithIguanaPrivKeyBuilder}; use crate::utxo::utxo_common::{self, big_decimal_from_sat, check_all_inputs_signed_by_pub, UtxoTxBuilder}; -use crate::utxo::{qtum, ActualTxFee, AdditionalTxData, BroadcastTxErr, FeePolicy, GenerateTxError, HistoryUtxoTx, - HistoryUtxoTxMap, RecentlySpentOutPoints, UtxoActivationParams, UtxoAddressFormat, UtxoCoinFields, - UtxoCommonOps, UtxoFromLegacyReqErr, UtxoTx, UtxoTxBroadcastOps, UtxoTxGenerationOps, - VerboseTransactionFrom, UTXO_LOCK}; +use crate::utxo::{qtum, ActualTxFee, AdditionalTxData, BroadcastTxErr, FeePolicy, GenerateTxError, GetUtxoListOps, + HistoryUtxoTx, HistoryUtxoTxMap, MatureUnspentList, RecentlySpentOutPointsGuard, + UtxoActivationParams, UtxoAddressFormat, UtxoCoinFields, UtxoCommonOps, UtxoFromLegacyReqErr, + UtxoTx, UtxoTxBroadcastOps, UtxoTxGenerationOps, VerboseTransactionFrom, UTXO_LOCK}; use crate::{BalanceError, BalanceFut, CoinBalance, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MarketCoinOps, MmCoin, NegotiateSwapContractAddrErr, PrivKeyNotAllowed, RawTransactionFut, RawTransactionRequest, SignatureResult, SwapOps, TradeFee, TradePreimageError, TradePreimageFut, TradePreimageResult, @@ -32,7 +34,6 @@ use derive_more::Display; use ethabi::{Function, Token}; use ethereum_types::{H160, U256}; use futures::compat::Future01CompatExt; -use futures::lock::MutexGuard as AsyncMutexGuard; use futures::{FutureExt, TryFutureExt}; use futures01::Future; use keys::bytes::Bytes as ScriptBytes; @@ -43,6 +44,7 @@ use script::{Builder as ScriptBuilder, Opcode, Script, TransactionInputSigner}; use script_pubkey::generate_contract_call_script_pubkey; use serde_json::{self as json, Value as Json}; use serialization::{deserialize, serialize, CoinVariant}; +use std::collections::{HashMap, HashSet}; use std::ops::{Deref, Neg}; #[cfg(not(target_arch = "wasm32"))] use std::path::PathBuf; use std::str::FromStr; @@ -241,6 +243,15 @@ impl<'a> UtxoCoinBuilderCommonOps for Qrc20CoinBuilder<'a> { } true } + + /// Override [`UtxoCoinBuilderCommonOps::tx_cache`] to initialize TX cache with the platform ticker. + /// Please note the method is overridden for Native mode only. + #[inline] + #[cfg(not(target_arch = "wasm32"))] + fn tx_cache(&self) -> UtxoVerboseCacheShared { + crate::utxo::tx_cache::fs_tx_cache::FsVerboseCache::new(self.platform.clone(), self.tx_cache_path()) + .into_shared() + } } #[async_trait] @@ -467,7 +478,7 @@ impl Qrc20Coin { contract_outputs: Vec, ) -> Result> { let my_address = self.utxo.derivation_method.iguana_or_err()?; - let (unspents, _) = self.list_unspent_ordered(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()); @@ -570,6 +581,31 @@ impl UtxoTxGenerationOps for Qrc20Coin { } } +#[async_trait] +#[cfg_attr(test, mockable)] +impl GetUtxoListOps for Qrc20Coin { + async fn get_unspent_ordered_list( + &self, + address: &Address, + ) -> UtxoRpcResult<(Vec, RecentlySpentOutPointsGuard<'_>)> { + utxo_common::get_unspent_ordered_list(self, address).await + } + + async fn get_all_unspent_ordered_list( + &self, + address: &Address, + ) -> UtxoRpcResult<(Vec, RecentlySpentOutPointsGuard<'_>)> { + utxo_common::get_all_unspent_ordered_list(self, address).await + } + + async fn get_mature_unspent_ordered_list( + &self, + address: &Address, + ) -> UtxoRpcResult<(MatureUnspentList, RecentlySpentOutPointsGuard<'_>)> { + utxo_common::get_mature_unspent_ordered_list(self, address).await + } +} + #[async_trait] #[cfg_attr(test, mockable)] impl UtxoCommonOps for Qrc20Coin { @@ -639,37 +675,15 @@ impl UtxoCommonOps for Qrc20Coin { .await } - async fn list_all_unspent_ordered<'a>( - &'a self, - address: &Address, - ) -> UtxoRpcResult<(Vec, AsyncMutexGuard<'a, RecentlySpentOutPoints>)> { - utxo_common::list_all_unspent_ordered(self, address).await - } - - async fn list_mature_unspent_ordered<'a>( - &'a self, - address: &Address, - ) -> UtxoRpcResult<(Vec, AsyncMutexGuard<'a, RecentlySpentOutPoints>)> { - utxo_common::list_mature_unspent_ordered(self, address).await - } - - fn get_verbose_transaction_from_cache_or_rpc(&self, txid: H256Json) -> UtxoRpcFut { + fn get_verbose_transactions_from_cache_or_rpc( + &self, + tx_ids: HashSet, + ) -> UtxoRpcFut> { let selfi = self.clone(); - let fut = async move { utxo_common::get_verbose_transaction_from_cache_or_rpc(&selfi.utxo, txid).await }; + let fut = async move { utxo_common::get_verbose_transactions_from_cache_or_rpc(&selfi.utxo, tx_ids).await }; Box::new(fut.boxed().compat()) } - async fn cache_transaction_if_possible(&self, tx: &RpcTransaction) -> Result<(), String> { - utxo_common::cache_transaction_if_possible(&self.utxo, tx).await - } - - async fn list_unspent_ordered<'a>( - &'a self, - address: &Address, - ) -> UtxoRpcResult<(Vec, AsyncMutexGuard<'a, RecentlySpentOutPoints>)> { - utxo_common::list_unspent_ordered(self, address).await - } - async fn preimage_trade_fee_required_to_send_outputs( &self, outputs: Vec, diff --git a/mm2src/coins/qrc20/history.rs b/mm2src/coins/qrc20/history.rs index cfabc9e956..c7a76d25ea 100644 --- a/mm2src/coins/qrc20/history.rs +++ b/mm2src/coins/qrc20/history.rs @@ -358,7 +358,9 @@ impl Qrc20Coin { } } }, - JsonRpcErrorType::Transport(err) | JsonRpcErrorType::Parse(_, err) => { + JsonRpcErrorType::InvalidRequest(err) + | JsonRpcErrorType::Transport(err) + | JsonRpcErrorType::Parse(_, err) => { return RequestTxHistoryResult::Retry { error: ERRL!("Error {} on blockchain_contract_event_get_history", err), }; diff --git a/mm2src/coins/qrc20/qrc20_tests.rs b/mm2src/coins/qrc20/qrc20_tests.rs index 2def78c339..653d2c6905 100644 --- a/mm2src/coins/qrc20/qrc20_tests.rs +++ b/mm2src/coins/qrc20/qrc20_tests.rs @@ -1,4 +1,5 @@ use super::*; +use crate::utxo::rpc_clients::UnspentInfo; use crate::TxFeeDetails; use bigdecimal::Zero; use chain::OutPoint; @@ -56,7 +57,7 @@ fn check_tx_fee(coin: &Qrc20Coin, expected_tx_fee: ActualTxFee) { #[test] fn test_withdraw_impl_fee_details() { - Qrc20Coin::list_mature_unspent_ordered.mock_safe(|coin, _| { + Qrc20Coin::get_unspent_ordered_list.mock_safe(|coin, _| { let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); let unspents = vec![UnspentInfo { outpoint: OutPoint { diff --git a/mm2src/coins/rpc_command/account_balance.rs b/mm2src/coins/rpc_command/account_balance.rs new file mode 100644 index 0000000000..e13a926abd --- /dev/null +++ b/mm2src/coins/rpc_command/account_balance.rs @@ -0,0 +1,111 @@ +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 async_trait::async_trait; +use common::mm_ctx::MmArc; +use common::mm_error::prelude::*; +use common::PagingOptionsEnum; +use crypto::{Bip44Chain, RpcDerivationPath}; +use std::fmt; + +#[derive(Deserialize)] +pub struct HDAccountBalanceRequest { + coin: String, + #[serde(flatten)] + params: AccountBalanceParams, +} + +#[derive(Deserialize)] +pub struct AccountBalanceParams { + pub account_index: u32, + pub chain: Bip44Chain, + #[serde(default = "common::ten")] + pub limit: usize, + #[serde(default)] + pub paging_options: PagingOptionsEnum, +} + +#[derive(Debug, PartialEq, Serialize)] +pub struct HDAccountBalanceResponse { + pub account_index: u32, + pub derivation_path: RpcDerivationPath, + pub addresses: Vec, + pub page_balance: CoinBalance, + pub limit: usize, + pub skipped: u32, + pub total: u32, + pub total_pages: usize, + pub paging_options: PagingOptionsEnum, +} + +#[async_trait] +pub trait AccountBalanceRpcOps { + async fn account_balance_rpc( + &self, + params: AccountBalanceParams, + ) -> MmResult; +} + +pub async fn account_balance( + ctx: MmArc, + req: HDAccountBalanceRequest, +) -> 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, + _ => MmError::err(HDAccountBalanceRpcError::CoinIsActivatedNotWithHDWallet), + } +} + +pub mod common_impl { + use super::*; + use crate::coin_balance::HDWalletBalanceOps; + use crate::hd_wallet::{HDAccountOps, HDWalletOps}; + use common::calc_total_pages; + + pub async fn account_balance_rpc( + coin: &Coin, + params: AccountBalanceParams, + ) -> MmResult + where + Coin: HDWalletBalanceOps + CoinWithDerivationMethod::HDWallet> + Sync, + ::Address: fmt::Display + Clone, + { + let account_id = params.account_index; + let hd_account = coin + .derivation_method() + .hd_wallet_or_err()? + .get_account(account_id) + .await + .or_mm_err(|| HDAccountBalanceRpcError::UnknownAccount { account_id })?; + let total_addresses_number = hd_account.known_addresses_number(params.chain)?; + + let from_address_id = match params.paging_options { + PagingOptionsEnum::FromId(from_address_id) => from_address_id + 1, + PagingOptionsEnum::PageNumber(page_number) => ((page_number.get() - 1) * params.limit) as u32, + }; + let to_address_id = std::cmp::min(from_address_id + params.limit as u32, total_addresses_number); + + 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 result = HDAccountBalanceResponse { + account_index: account_id, + derivation_path: RpcDerivationPath(hd_account.account_derivation_path()), + addresses, + page_balance, + limit: params.limit, + skipped: std::cmp::min(from_address_id, total_addresses_number), + total: total_addresses_number, + total_pages: calc_total_pages(total_addresses_number as usize, params.limit), + paging_options: params.paging_options, + }; + + Ok(result) + } +} diff --git a/mm2src/coins/rpc_command/hd_account_balance_rpc_error.rs b/mm2src/coins/rpc_command/hd_account_balance_rpc_error.rs new file mode 100644 index 0000000000..851d2b42d8 --- /dev/null +++ b/mm2src/coins/rpc_command/hd_account_balance_rpc_error.rs @@ -0,0 +1,106 @@ +use crate::hd_wallet::{AddressDerivingError, InvalidBip44ChainError}; +use crate::{BalanceError, CoinFindError, UnexpectedDerivationMethod}; +use common::HttpStatusCode; +use crypto::Bip44Chain; +use derive_more::Display; +use http::StatusCode; +use rpc_task::RpcTaskError; +use std::time::Duration; + +#[derive(Clone, Debug, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum HDAccountBalanceRpcError { + #[display(fmt = "No such coin {}", coin)] + NoSuchCoin { coin: String }, + #[display(fmt = "RPC timed out {:?}", _0)] + Timeout(Duration), + #[display(fmt = "Coin is expected to be activated with the HD wallet derivation method")] + CoinIsActivatedNotWithHDWallet, + #[display(fmt = "HD account '{}' is not activated", account_id)] + UnknownAccount { account_id: u32 }, + #[display(fmt = "Coin doesn't support the given BIP44 chain: {:?}", chain)] + InvalidBip44Chain { chain: Bip44Chain }, + #[display(fmt = "Error deriving an address: {}", _0)] + ErrorDerivingAddress(String), + #[display(fmt = "Wallet storage error: {}", _0)] + WalletStorageError(String), + #[display(fmt = "Electrum/Native RPC invalid response: {}", _0)] + RpcInvalidResponse(String), + #[display(fmt = "Transport: {}", _0)] + Transport(String), + #[display(fmt = "Internal: {}", _0)] + Internal(String), +} + +impl HttpStatusCode for HDAccountBalanceRpcError { + fn status_code(&self) -> StatusCode { + match self { + HDAccountBalanceRpcError::NoSuchCoin { .. } + | HDAccountBalanceRpcError::CoinIsActivatedNotWithHDWallet + | HDAccountBalanceRpcError::UnknownAccount { .. } + | HDAccountBalanceRpcError::InvalidBip44Chain { .. } + | HDAccountBalanceRpcError::ErrorDerivingAddress(_) => StatusCode::BAD_REQUEST, + HDAccountBalanceRpcError::Timeout(_) => StatusCode::REQUEST_TIMEOUT, + HDAccountBalanceRpcError::Transport(_) + | HDAccountBalanceRpcError::WalletStorageError(_) + | HDAccountBalanceRpcError::RpcInvalidResponse(_) + | HDAccountBalanceRpcError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl From for HDAccountBalanceRpcError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => HDAccountBalanceRpcError::NoSuchCoin { coin }, + } + } +} + +impl From for HDAccountBalanceRpcError { + fn from(e: UnexpectedDerivationMethod) -> Self { + match e { + UnexpectedDerivationMethod::HDWalletUnavailable => HDAccountBalanceRpcError::CoinIsActivatedNotWithHDWallet, + unexpected_error => HDAccountBalanceRpcError::Internal(unexpected_error.to_string()), + } + } +} + +impl From for HDAccountBalanceRpcError { + fn from(e: BalanceError) -> Self { + match e { + BalanceError::Transport(transport) => HDAccountBalanceRpcError::Transport(transport), + BalanceError::InvalidResponse(rpc) => HDAccountBalanceRpcError::RpcInvalidResponse(rpc), + BalanceError::UnexpectedDerivationMethod(der_method) => HDAccountBalanceRpcError::from(der_method), + BalanceError::WalletStorageError(e) => HDAccountBalanceRpcError::Internal(e), + BalanceError::Internal(internal) => HDAccountBalanceRpcError::Internal(internal), + } + } +} + +impl From for HDAccountBalanceRpcError { + fn from(e: InvalidBip44ChainError) -> Self { HDAccountBalanceRpcError::InvalidBip44Chain { chain: e.chain } } +} + +impl From for HDAccountBalanceRpcError { + fn from(e: AddressDerivingError) -> Self { + match e { + AddressDerivingError::Bip32Error(bip32) => { + HDAccountBalanceRpcError::ErrorDerivingAddress(bip32.to_string()) + }, + } + } +} + +impl From for HDAccountBalanceRpcError { + fn from(e: RpcTaskError) -> Self { + match e { + RpcTaskError::Canceled => HDAccountBalanceRpcError::Internal("Canceled".to_owned()), + RpcTaskError::Timeout(timeout) => HDAccountBalanceRpcError::Timeout(timeout), + RpcTaskError::NoSuchTask(_) | RpcTaskError::UnexpectedTaskStatus { .. } => { + HDAccountBalanceRpcError::Internal(e.to_string()) + }, + RpcTaskError::Internal(internal) => HDAccountBalanceRpcError::Internal(internal), + } + } +} diff --git a/mm2src/coins/init_create_account.rs b/mm2src/coins/rpc_command/init_create_account.rs similarity index 100% rename from mm2src/coins/init_create_account.rs rename to mm2src/coins/rpc_command/init_create_account.rs diff --git a/mm2src/coins/rpc_command/init_scan_for_new_addresses.rs b/mm2src/coins/rpc_command/init_scan_for_new_addresses.rs new file mode 100644 index 0000000000..34ffcc7b90 --- /dev/null +++ b/mm2src/coins/rpc_command/init_scan_for_new_addresses.rs @@ -0,0 +1,153 @@ +use crate::coin_balance::HDAddressBalance; +use crate::rpc_command::hd_account_balance_rpc_error::HDAccountBalanceRpcError; +use crate::{lp_coinfind_or_err, CoinsContext, MmCoinEnum}; +use async_trait::async_trait; +use common::mm_ctx::MmArc; +use common::mm_error::prelude::*; +use crypto::RpcDerivationPath; +use rpc_task::rpc_common::{InitRpcTaskResponse, RpcTaskStatusError, RpcTaskStatusRequest}; +use rpc_task::{RpcTask, RpcTaskHandle, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, RpcTaskTypes}; + +pub type ScanAddressesTaskManager = RpcTaskManager; +pub type ScanAddressesTaskManagerShared = RpcTaskManagerShared; +pub type ScanAddressesTaskHandle = RpcTaskHandle; +pub type ScanAddressesRpcTaskStatus = RpcTaskStatus< + ScanAddressesResponse, + HDAccountBalanceRpcError, + ScanAddressesInProgressStatus, + ScanAddressesAwaitingStatus, +>; + +#[derive(Clone, Debug, PartialEq, Serialize)] +pub struct ScanAddressesResponse { + pub account_index: u32, + pub derivation_path: RpcDerivationPath, + pub new_addresses: Vec, +} + +#[derive(Deserialize)] +pub struct ScanAddressesRequest { + coin: String, + #[serde(flatten)] + params: ScanAddressesParams, +} + +#[derive(Deserialize)] +pub struct ScanAddressesParams { + pub account_index: u32, + pub gap_limit: Option, +} + +#[derive(Clone, Serialize)] +pub enum ScanAddressesInProgressStatus { + InProgress, +} + +/// We can't use `std::convert::Infallible` as [`RpcTaskTypes::UserAction`] because it doesn't implement `Serialize`. +/// Use `!` when it's stable. +#[derive(Clone, Serialize)] +pub enum ScanAddressesUserAction {} + +/// We can't use `std::convert::Infallible` as [`RpcTaskTypes::AwaitingStatus`] because it doesn't implement `Serialize`. +/// Use `!` when it's stable. +#[derive(Clone, Serialize)] +pub enum ScanAddressesAwaitingStatus {} + +#[async_trait] +pub trait InitScanAddressesRpcOps { + async fn init_scan_for_new_addresses_rpc( + &self, + params: ScanAddressesParams, + ) -> MmResult; +} + +pub struct InitScanAddressesTask { + req: ScanAddressesRequest, + coin: MmCoinEnum, +} + +impl RpcTaskTypes for InitScanAddressesTask { + type Item = ScanAddressesResponse; + type Error = HDAccountBalanceRpcError; + type InProgressStatus = ScanAddressesInProgressStatus; + type AwaitingStatus = ScanAddressesAwaitingStatus; + type UserAction = ScanAddressesUserAction; +} + +#[async_trait] +impl RpcTask for InitScanAddressesTask { + #[inline] + fn initial_status(&self) -> Self::InProgressStatus { ScanAddressesInProgressStatus::InProgress } + + async fn run(self, _task_handle: &ScanAddressesTaskHandle) -> Result> { + match self.coin { + MmCoinEnum::UtxoCoin(utxo) => utxo.init_scan_for_new_addresses_rpc(self.req.params).await, + MmCoinEnum::QtumCoin(qtum) => qtum.init_scan_for_new_addresses_rpc(self.req.params).await, + _ => MmError::err(HDAccountBalanceRpcError::CoinIsActivatedNotWithHDWallet), + } + } +} + +pub async fn init_scan_for_new_addresses( + ctx: MmArc, + req: ScanAddressesRequest, +) -> MmResult { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + let coins_ctx = CoinsContext::from_ctx(&ctx).map_to_mm(HDAccountBalanceRpcError::Internal)?; + let task = InitScanAddressesTask { req, coin }; + let task_id = ScanAddressesTaskManager::spawn_rpc_task(&coins_ctx.scan_addresses_manager, task)?; + Ok(InitRpcTaskResponse { task_id }) +} + +pub async fn init_scan_for_new_addresses_status( + ctx: MmArc, + req: RpcTaskStatusRequest, +) -> MmResult { + let coins_ctx = CoinsContext::from_ctx(&ctx).map_to_mm(RpcTaskStatusError::Internal)?; + let mut task_manager = coins_ctx + .scan_addresses_manager + .lock() + .map_to_mm(|e| RpcTaskStatusError::Internal(e.to_string()))?; + task_manager + .task_status(req.task_id, req.forget_if_finished) + .or_mm_err(|| RpcTaskStatusError::NoSuchTask(req.task_id)) +} + +pub mod common_impl { + use super::*; + use crate::coin_balance::HDWalletBalanceOps; + use crate::hd_wallet::{HDAccountOps, HDWalletCoinOps, HDWalletOps}; + use crate::CoinWithDerivationMethod; + use std::fmt; + use std::ops::DerefMut; + + pub async fn scan_for_new_addresses_rpc( + coin: &Coin, + params: ScanAddressesParams, + ) -> MmResult + where + Coin: CoinWithDerivationMethod::HDWallet> + HDWalletBalanceOps + Sync, + ::Address: fmt::Display, + { + let hd_wallet = coin.derivation_method().hd_wallet_or_err()?; + + let account_id = params.account_index; + let mut hd_account = hd_wallet + .get_account_mut(account_id) + .await + .or_mm_err(|| HDAccountBalanceRpcError::CoinIsActivatedNotWithHDWallet)?; + let account_derivation_path = hd_account.account_derivation_path(); + let address_scanner = coin.produce_hd_address_scanner().await?; + let gap_limit = params.gap_limit.unwrap_or_else(|| hd_wallet.gap_limit()); + + let new_addresses = coin + .scan_for_new_addresses(hd_wallet, hd_account.deref_mut(), &address_scanner, gap_limit) + .await?; + + Ok(ScanAddressesResponse { + account_index: account_id, + derivation_path: RpcDerivationPath(account_derivation_path), + new_addresses, + }) + } +} diff --git a/mm2src/coins/init_withdraw.rs b/mm2src/coins/rpc_command/init_withdraw.rs similarity index 100% rename from mm2src/coins/init_withdraw.rs rename to mm2src/coins/rpc_command/init_withdraw.rs diff --git a/mm2src/coins/rpc_command/mod.rs b/mm2src/coins/rpc_command/mod.rs new file mode 100644 index 0000000000..7a98d3fc2e --- /dev/null +++ b/mm2src/coins/rpc_command/mod.rs @@ -0,0 +1,5 @@ +pub mod account_balance; +pub mod hd_account_balance_rpc_error; +pub mod init_create_account; +pub mod init_scan_for_new_addresses; +pub mod init_withdraw; diff --git a/mm2src/coins/utxo.rs b/mm2src/coins/utxo.rs index 1a6e23063b..6c4e7900c3 100644 --- a/mm2src/coins/utxo.rs +++ b/mm2src/coins/utxo.rs @@ -75,8 +75,8 @@ use std::convert::TryInto; use std::hash::Hash; use std::num::NonZeroU64; use std::ops::Deref; -#[cfg(not(target_arch = "wasm32"))] use std::path::Path; -use std::path::PathBuf; +#[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}; @@ -86,25 +86,31 @@ use utxo_signer::with_key_pair::sign_tx; use utxo_signer::{TxProvider, TxProviderError, UtxoSignTxError, UtxoSignTxResult}; use self::rpc_clients::{electrum_script_hash, ElectrumClient, ElectrumRpcRequest, EstimateFeeMethod, EstimateFeeMode, - NativeClient, UnspentInfo, UtxoRpcClientEnum, UtxoRpcError, UtxoRpcFut, UtxoRpcResult}; -use super::{BalanceError, BalanceFut, BalanceResult, CoinsContext, DerivationMethod, FeeApproxStage, FoundSwapTxSpend, - HistorySyncState, KmdRewardsDetails, MarketCoinOps, MmCoin, NumConversError, NumConversResult, - PrivKeyActivationPolicy, PrivKeyNotAllowed, PrivKeyPolicy, RawTransactionFut, RawTransactionRequest, - RawTransactionResult, RpcTransportEventHandler, RpcTransportEventHandlerShared, TradeFee, - TradePreimageError, TradePreimageFut, TradePreimageResult, Transaction, TransactionDetails, - TransactionEnum, UnexpectedDerivationMethod, WithdrawError, WithdrawRequest}; + NativeClient, UnspentInfo, UnspentMap, UtxoRpcClientEnum, UtxoRpcError, UtxoRpcFut, + UtxoRpcResult}; +use super::{big_decimal_from_sat_unsigned, BalanceError, BalanceFut, BalanceResult, CoinBalance, CoinsContext, + DerivationMethod, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, KmdRewardsDetails, MarketCoinOps, + MmCoin, NumConversError, NumConversResult, PrivKeyActivationPolicy, PrivKeyNotAllowed, PrivKeyPolicy, + RawTransactionFut, RawTransactionRequest, RawTransactionResult, RpcTransportEventHandler, + RpcTransportEventHandlerShared, TradeFee, TradePreimageError, TradePreimageFut, TradePreimageResult, + Transaction, TransactionDetails, TransactionEnum, UnexpectedDerivationMethod, WithdrawError, + WithdrawRequest}; use crate::coin_balance::{EnableCoinScanPolicy, HDAddressBalanceScanner}; use crate::hd_wallet::{HDAccountOps, HDAccountsMutex, HDAddress, HDWalletCoinOps, HDWalletOps, InvalidBip44ChainError}; use crate::hd_wallet_storage::{HDAccountStorageItem, HDWalletCoinStorage, HDWalletStorageError, HDWalletStorageResult}; +use crate::utxo::tx_cache::UtxoVerboseCacheShared; use crate::utxo::utxo_block_header_storage::BlockHeaderStorageError; use crate::TransactionErr; use utxo_block_header_storage::BlockHeaderStorage; -#[cfg(not(target_arch = "wasm32"))] pub mod tx_cache; + +pub mod tx_cache; #[cfg(target_arch = "wasm32")] pub mod utxo_indexedb_block_header_storage; #[cfg(not(target_arch = "wasm32"))] pub mod utxo_sql_block_header_storage; +#[cfg(any(test, target_arch = "wasm32"))] +pub mod utxo_common_tests; #[cfg(test)] pub mod utxo_tests; #[cfg(target_arch = "wasm32")] pub mod utxo_wasm_tests; @@ -125,6 +131,8 @@ 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>; #[cfg(windows)] #[cfg(not(target_arch = "wasm32"))] @@ -520,8 +528,8 @@ pub struct UtxoCoinFields { /// Either an Iguana address or an info about last derived account/address. pub derivation_method: DerivationMethod, pub history_sync_state: Mutex, - /// Path to the TX cache directory - pub tx_cache_directory: Option, + /// The cache of verbose transactions. + pub tx_cache: UtxoVerboseCacheShared, pub block_headers_storage: Option, /// The cache of recently send transactions used to track the spent UTXOs and replace them with new outputs /// The daemon needs some time to update the listunspent list for address which makes it return already spent UTXOs @@ -738,6 +746,43 @@ impl UtxoAddressScanner { } } +/// Contains lists of mature and immature UTXOs. +#[derive(Debug, Default)] +pub struct MatureUnspentList { + mature: Vec, + immature: Vec, +} + +impl MatureUnspentList { + #[inline] + pub fn with_capacity(capacity: usize) -> MatureUnspentList { + MatureUnspentList { + mature: Vec::with_capacity(capacity), + immature: Vec::with_capacity(capacity), + } + } + + #[inline] + pub fn new_mature(mature: Vec) -> MatureUnspentList { + MatureUnspentList { + mature, + immature: Vec::new(), + } + } + + #[inline] + pub fn only_mature(self) -> Vec { self.mature } + + #[inline] + pub fn to_coin_balance(&self, decimals: u8) -> CoinBalance { + let fold = |acc: BigDecimal, x: &UnspentInfo| acc + big_decimal_from_sat_unsigned(x.value, decimals); + CoinBalance { + spendable: self.mature.iter().fold(BigDecimal::default(), fold), + unspendable: self.immature.iter().fold(BigDecimal::default(), fold), + } + } +} + #[async_trait] #[cfg_attr(test, mockable)] pub trait UtxoCommonOps: @@ -790,34 +835,11 @@ pub trait UtxoCommonOps: keypair: &KeyPair, ) -> Result; - /// Returns available unspents in ascending order + RecentlySpentOutPoints MutexGuard for further interaction - /// (e.g. to add new transaction to it). - /// Please consider using [`UtxoCommonOps::list_unspent_ordered`] instead. - async fn list_all_unspent_ordered<'a>( - &'a self, - address: &Address, - ) -> UtxoRpcResult<(Vec, AsyncMutexGuard<'a, RecentlySpentOutPoints>)>; - - /// Returns available mature unspents ascending order + RecentlySpentOutPoints MutexGuard for further interaction - /// (e.g. to add new transaction to it). - /// Please consider using [`UtxoCommonOps::list_unspent_ordered`] instead. - async fn list_mature_unspent_ordered<'a>( - &'a self, - address: &Address, - ) -> UtxoRpcResult<(Vec, AsyncMutexGuard<'a, RecentlySpentOutPoints>)>; - - /// Try to load verbose transaction from cache or try to request it from Rpc client. - fn get_verbose_transaction_from_cache_or_rpc(&self, txid: H256Json) -> UtxoRpcFut; - - /// Cache transaction if the coin supports `TX_CACHE` and tx height is set and not zero. - async fn cache_transaction_if_possible(&self, tx: &RpcTransaction) -> Result<(), String>; - - /// Returns available unspents in ascending order + RecentlySpentOutPoints MutexGuard for further interaction - /// (e.g. to add new transaction to it). - async fn list_unspent_ordered( + /// Loads verbose transactions from cache or requests it using RPC client. + fn get_verbose_transactions_from_cache_or_rpc( &self, - address: &Address, - ) -> UtxoRpcResult<(Vec, AsyncMutexGuard<'_, RecentlySpentOutPoints>)>; + tx_ids: HashSet, + ) -> UtxoRpcFut>; async fn preimage_trade_fee_required_to_send_outputs( &self, @@ -845,6 +867,80 @@ pub trait UtxoCommonOps: } } +#[async_trait] +#[cfg_attr(test, mockable)] +pub trait GetUtxoListOps { + /// Returns available unspents in ascending order + /// + `RecentlySpentOutPoints` MutexGuard for further interaction (e.g. to add new transaction to it). + /// The function uses either [`GetUtxoListOps::get_all_unspent_ordered_list`] or [`GetUtxoListOps::get_mature_unspent_ordered_list`] + /// depending on the coin configuration. + async fn get_unspent_ordered_list( + &self, + address: &Address, + ) -> UtxoRpcResult<(Vec, RecentlySpentOutPointsGuard<'_>)>; + + /// Returns available unspents in ascending order + /// + `RecentlySpentOutPoints` MutexGuard for further interaction (e.g. to add new transaction to it). + /// + /// # Important + /// + /// The function doesn't check if the unspents are mature or immature. + /// Consider using [`GetUtxoListOps::get_unspent_ordered_list`] instead. + async fn get_all_unspent_ordered_list( + &self, + address: &Address, + ) -> UtxoRpcResult<(Vec, RecentlySpentOutPointsGuard<'_>)>; + + /// Returns available mature and immature unspents in ascending order + /// + `RecentlySpentOutPoints` MutexGuard for further interaction (e.g. to add new transaction to it). + /// + /// # Important + /// + /// The function may request extra data using RPC to check each unspent output whether it's mature or not. + /// It may be overhead in some cases, so consider using [`GetUtxoListOps::get_unspent_ordered_list`] instead. + async fn get_mature_unspent_ordered_list( + &self, + address: &Address, + ) -> UtxoRpcResult<(MatureUnspentList, RecentlySpentOutPointsGuard<'_>)>; +} + +#[async_trait] +#[cfg_attr(test, mockable)] +pub trait GetUtxoMapOps { + /// Returns available unspents in ascending order for every given `addresses` + /// + `RecentlySpentOutPoints` MutexGuard for further interaction (e.g. to add new transaction to it). + /// The function uses either [`GetUtxoMapOps::get_all_unspent_ordered_map`] or [`GetUtxoMapOps::get_mature_unspent_ordered_map`] + /// depending on the coin configuration. + async fn get_unspent_ordered_map( + &self, + addresses: Vec
, + ) -> UtxoRpcResult<(UnspentMap, RecentlySpentOutPointsGuard<'_>)>; + + /// Returns available unspents in ascending order for every given `addresses` + /// + `RecentlySpentOutPoints` MutexGuard for further interaction (e.g. to add new transaction to it). + /// + /// # Important + /// + /// The function doesn't check if the unspents are mature or immature. + /// Consider using [`GetUtxoMapOps::get_unspent_ordered_map`] instead. + async fn get_all_unspent_ordered_map( + &self, + addresses: Vec
, + ) -> UtxoRpcResult<(UnspentMap, RecentlySpentOutPointsGuard<'_>)>; + + /// Returns available mature and immature unspents in ascending order for every given `addresses` + /// + `RecentlySpentOutPoints` MutexGuard for further interaction (e.g. to add new transaction to it). + /// + /// # Important + /// + /// The function may request extra data using RPC to check each unspent output whether it's mature or not. + /// It may be overhead in some cases, so consider using [`GetUtxoMapOps::get_unspent_ordered_map`] instead. + async fn get_mature_unspent_ordered_map( + &self, + addresses: Vec
, + ) -> UtxoRpcResult<(MatureUnspentMap, RecentlySpentOutPointsGuard<'_>)>; +} + #[async_trait] pub trait UtxoStandardOps { /// Gets tx details by hash requesting the coin RPC if required. @@ -972,13 +1068,15 @@ pub enum RequestTxHistoryResult { CriticalError(String), } +#[derive(Clone)] pub enum VerboseTransactionFrom { Cache(RpcTransaction), Rpc(RpcTransaction), } impl VerboseTransactionFrom { - fn into_inner(self) -> RpcTransaction { + #[inline] + fn to_inner(&self) -> &RpcTransaction { match self { VerboseTransactionFrom::Rpc(tx) | VerboseTransactionFrom::Cache(tx) => tx, } @@ -1090,9 +1188,9 @@ impl RpcTransportEventHandler for ElectrumProtoVerifier { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct UtxoMergeParams { pub merge_at: usize, - #[serde(default = "ten_f64")] + #[serde(default = "common::ten_f64")] pub check_every: f64, - #[serde(default = "one_hundred")] + #[serde(default = "common::one_hundred")] pub max_merge_at_once: usize, } @@ -1413,9 +1511,8 @@ pub async fn kmd_rewards_info(coin: &T) -> Result( outputs: Vec, ) -> Result where - T: UtxoCommonOps, + T: UtxoCommonOps + GetUtxoListOps, { let my_address = try_tx_s!(coin.as_ref().derivation_method.iguana_or_err()); - let (unspents, recently_sent_txs) = try_tx_s!(coin.list_unspent_ordered(my_address).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 } @@ -1489,7 +1586,7 @@ async fn generate_and_send_tx( unspents: Vec, required_inputs: Option>, fee_policy: FeePolicy, - mut recently_spent: AsyncMutexGuard<'_, RecentlySpentOutPoints>, + mut recently_spent: RecentlySpentOutPointsGuard<'_>, outputs: Vec, ) -> Result where @@ -1511,7 +1608,7 @@ where .inputs .iter() .map(|input| UnspentInfo { - outpoint: input.previous_output.clone(), + outpoint: input.previous_output, value: input.amount, height: None, }) @@ -1595,10 +1692,6 @@ fn parse_hex_encoded_u32(hex_encoded: &str) -> Result> { Ok(u32::from_be_bytes(be_bytes)) } -fn ten_f64() -> f64 { 10. } - -fn one_hundred() -> usize { 100 } - #[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 a05d426d62..c59000a072 100644 --- a/mm2src/coins/utxo/bch.rs +++ b/mm2src/coins/utxo/bch.rs @@ -22,6 +22,8 @@ use serde_json::{self as json, Value as Json}; use serialization::{deserialize, CoinVariant}; use std::sync::MutexGuard; +pub type BchUnspentMap = HashMap; + #[derive(Clone, Debug, Deserialize, Serialize)] pub struct BchActivationRequest { #[serde(default)] @@ -187,19 +189,41 @@ impl BchCoin { async fn utxos_into_bch_unspents(&self, utxos: Vec) -> UtxoRpcResult { let mut result = BchUnspents::default(); - for unspent in utxos { - if unspent.outpoint.index == 0 { - // zero output is reserved for OP_RETURN of specific protocols - // so if we get it we can safely consider this as standard BCH UTXO - result.add_standard(unspent); - continue; - } + let mut temporary_undetermined = Vec::new(); + + let to_verbose: HashSet = utxos + .into_iter() + .filter_map(|unspent| { + if unspent.outpoint.index == 0 { + // Zero output is reserved for OP_RETURN of specific protocols + // so if we get it we can safely consider this as standard BCH UTXO. + // There is no need to request verbose transaction for such UTXO. + result.add_standard(unspent); + None + } else { + let hash = unspent.outpoint.hash.reversed().into(); + temporary_undetermined.push(unspent); + Some(hash) + } + }) + .collect(); - let prev_tx_bytes = self - .get_verbose_transaction_from_cache_or_rpc(unspent.outpoint.hash.reversed().into()) - .compat() - .await? - .into_inner(); + let verbose_txs = self + .get_verbose_transactions_from_cache_or_rpc(to_verbose) + .compat() + .await?; + + for unspent in temporary_undetermined { + let prev_tx_hash = unspent.outpoint.hash.reversed().into(); + let prev_tx_bytes = verbose_txs + .get(&prev_tx_hash) + .or_mm_err(|| { + UtxoRpcError::Internal(format!( + "'get_verbose_transactions_from_cache_or_rpc' should have returned '{:?}'", + prev_tx_hash + )) + })? + .to_inner(); let prev_tx: UtxoTx = match deserialize(prev_tx_bytes.hex.as_slice()) { Ok(b) => b, Err(e) => { @@ -290,8 +314,8 @@ impl BchCoin { pub async fn bch_unspents_for_spend( &self, address: &Address, - ) -> UtxoRpcResult<(BchUnspents, AsyncMutexGuard<'_, RecentlySpentOutPoints>)> { - let (all_unspents, recently_spent) = utxo_common::list_unspent_ordered(self, address).await?; + ) -> UtxoRpcResult<(BchUnspents, RecentlySpentOutPointsGuard<'_>)> { + let (all_unspents, recently_spent) = utxo_common::get_unspent_ordered_list(self, address).await?; let result = self.utxos_into_bch_unspents(all_unspents).await?; Ok((result, recently_spent)) @@ -300,11 +324,7 @@ impl BchCoin { pub async fn get_token_utxos_for_spend( &self, token_id: &H256, - ) -> UtxoRpcResult<( - Vec, - Vec, - AsyncMutexGuard<'_, RecentlySpentOutPoints>, - )> { + ) -> UtxoRpcResult<(Vec, Vec, RecentlySpentOutPointsGuard<'_>)> { let my_address = self .as_ref() .derivation_method @@ -695,6 +715,33 @@ impl UtxoTxGenerationOps for BchCoin { } } +#[async_trait] +#[cfg_attr(test, mockable)] +impl GetUtxoListOps for BchCoin { + async fn get_unspent_ordered_list( + &self, + address: &Address, + ) -> UtxoRpcResult<(Vec, RecentlySpentOutPointsGuard<'_>)> { + let (bch_unspents, recently_spent) = self.bch_unspents_for_spend(address).await?; + Ok((bch_unspents.standard, recently_spent)) + } + + async fn get_all_unspent_ordered_list( + &self, + address: &Address, + ) -> UtxoRpcResult<(Vec, RecentlySpentOutPointsGuard<'_>)> { + utxo_common::get_all_unspent_ordered_list(self, address).await + } + + async fn get_mature_unspent_ordered_list( + &self, + address: &Address, + ) -> UtxoRpcResult<(MatureUnspentList, RecentlySpentOutPointsGuard<'_>)> { + let (unspents, recently_spent) = utxo_common::get_all_unspent_ordered_list(self, address).await?; + Ok((MatureUnspentList::new_mature(unspents), recently_spent)) + } +} + // 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)] @@ -761,38 +808,15 @@ impl UtxoCommonOps for BchCoin { .await } - async fn list_all_unspent_ordered<'a>( - &'a self, - address: &Address, - ) -> UtxoRpcResult<(Vec, AsyncMutexGuard<'a, RecentlySpentOutPoints>)> { - utxo_common::list_all_unspent_ordered(self, address).await - } - - async fn list_mature_unspent_ordered<'a>( - &'a self, - address: &Address, - ) -> UtxoRpcResult<(Vec, AsyncMutexGuard<'a, RecentlySpentOutPoints>)> { - utxo_common::list_all_unspent_ordered(self, address).await - } - - fn get_verbose_transaction_from_cache_or_rpc(&self, txid: H256Json) -> UtxoRpcFut { + fn get_verbose_transactions_from_cache_or_rpc( + &self, + tx_ids: HashSet, + ) -> UtxoRpcFut> { let selfi = self.clone(); - let fut = async move { utxo_common::get_verbose_transaction_from_cache_or_rpc(&selfi.utxo_arc, txid).await }; + let fut = async move { utxo_common::get_verbose_transactions_from_cache_or_rpc(&selfi.utxo_arc, tx_ids).await }; Box::new(fut.boxed().compat()) } - async fn cache_transaction_if_possible(&self, tx: &RpcTransaction) -> Result<(), String> { - utxo_common::cache_transaction_if_possible(&self.utxo_arc, tx).await - } - - async fn list_unspent_ordered<'a>( - &'a self, - address: &Address, - ) -> UtxoRpcResult<(Vec, AsyncMutexGuard<'a, RecentlySpentOutPoints>)> { - let (bch_unspents, recently_spent) = self.bch_unspents_for_spend(address).await?; - Ok((bch_unspents.standard, recently_spent)) - } - async fn preimage_trade_fee_required_to_send_outputs( &self, outputs: Vec, diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index fabddd15fc..f93e92eb6f 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -1,15 +1,17 @@ use super::*; -use crate::coin_balance::{self, AccountBalanceParams, CheckHDAccountBalanceParams, CheckHDAccountBalanceResponse, - EnableCoinBalanceError, HDAccountBalance, HDAccountBalanceResponse, - HDAccountBalanceRpcError, HDAddressBalance, HDWalletBalance, HDWalletBalanceOps, - HDWalletBalanceRpcOps}; +use crate::coin_balance::{self, EnableCoinBalanceError, HDAccountBalance, HDAddressBalance, HDWalletBalance, + HDWalletBalanceOps}; use crate::hd_pubkey::{ExtractExtendedPubkey, HDExtractPubkeyError, HDXPubExtractor}; use crate::hd_wallet::{self, AccountUpdatingError, AddressDerivingError, GetNewHDAddressParams, GetNewHDAddressResponse, HDAccountMut, HDWalletRpcError, HDWalletRpcOps, NewAccountCreatingError}; use crate::hd_wallet_storage::HDWalletCoinWithStorageOps; -use crate::init_create_account::{self, CreateNewAccountParams, InitCreateHDAccountRpcOps}; -use crate::init_withdraw::{InitWithdrawCoin, WithdrawTaskHandle}; +use crate::rpc_command::account_balance::{self, AccountBalanceParams, AccountBalanceRpcOps, HDAccountBalanceResponse}; +use crate::rpc_command::hd_account_balance_rpc_error::HDAccountBalanceRpcError; +use crate::rpc_command::init_create_account::{self, CreateNewAccountParams, InitCreateHDAccountRpcOps}; +use crate::rpc_command::init_scan_for_new_addresses::{self, InitScanAddressesRpcOps, ScanAddressesParams, + ScanAddressesResponse}; +use crate::rpc_command::init_withdraw::{InitWithdrawCoin, WithdrawTaskHandle}; use crate::utxo::utxo_builder::{MergeUtxoArcOps, UtxoCoinBuildError, UtxoCoinBuilder, UtxoCoinBuilderCommonOps, UtxoFieldsWithHardwareWalletBuilder, UtxoFieldsWithIguanaPrivKeyBuilder}; use crate::{eth, CanRefundHtlc, CoinBalance, CoinWithDerivationMethod, DelegationError, DelegationFut, @@ -316,6 +318,56 @@ impl UtxoTxGenerationOps for QtumCoin { } } +#[async_trait] +#[cfg_attr(test, mockable)] +impl GetUtxoListOps for QtumCoin { + async fn get_unspent_ordered_list( + &self, + address: &Address, + ) -> UtxoRpcResult<(Vec, RecentlySpentOutPointsGuard<'_>)> { + utxo_common::get_unspent_ordered_list(self, address).await + } + + async fn get_all_unspent_ordered_list( + &self, + address: &Address, + ) -> UtxoRpcResult<(Vec, RecentlySpentOutPointsGuard<'_>)> { + utxo_common::get_all_unspent_ordered_list(self, address).await + } + + async fn get_mature_unspent_ordered_list( + &self, + address: &Address, + ) -> UtxoRpcResult<(MatureUnspentList, RecentlySpentOutPointsGuard<'_>)> { + utxo_common::get_mature_unspent_ordered_list(self, address).await + } +} + +#[async_trait] +#[cfg_attr(test, mockable)] +impl GetUtxoMapOps for QtumCoin { + 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 + } +} + #[async_trait] #[cfg_attr(test, mockable)] impl UtxoCommonOps for QtumCoin { @@ -385,37 +437,15 @@ impl UtxoCommonOps for QtumCoin { .await } - async fn list_all_unspent_ordered<'a>( - &'a self, - address: &Address, - ) -> UtxoRpcResult<(Vec, AsyncMutexGuard<'a, RecentlySpentOutPoints>)> { - utxo_common::list_all_unspent_ordered(self, address).await - } - - async fn list_mature_unspent_ordered<'a>( - &'a self, - address: &Address, - ) -> UtxoRpcResult<(Vec, AsyncMutexGuard<'a, RecentlySpentOutPoints>)> { - utxo_common::list_mature_unspent_ordered(self, address).await - } - - fn get_verbose_transaction_from_cache_or_rpc(&self, txid: H256Json) -> UtxoRpcFut { + fn get_verbose_transactions_from_cache_or_rpc( + &self, + tx_ids: HashSet, + ) -> UtxoRpcFut> { let selfi = self.clone(); - let fut = async move { utxo_common::get_verbose_transaction_from_cache_or_rpc(&selfi.utxo_arc, txid).await }; + let fut = async move { utxo_common::get_verbose_transactions_from_cache_or_rpc(&selfi.utxo_arc, tx_ids).await }; Box::new(fut.boxed().compat()) } - async fn cache_transaction_if_possible(&self, tx: &RpcTransaction) -> Result<(), String> { - utxo_common::cache_transaction_if_possible(&self.utxo_arc, tx).await - } - - async fn list_unspent_ordered<'a>( - &'a self, - address: &Address, - ) -> UtxoRpcResult<(Vec, AsyncMutexGuard<'a, RecentlySpentOutPoints>)> { - utxo_common::list_unspent_ordered(self, address).await - } - async fn preimage_trade_fee_required_to_send_outputs( &self, outputs: Vec, @@ -970,6 +1000,13 @@ impl HDWalletBalanceOps for QtumCoin { async fn known_address_balance(&self, address: &Self::Address) -> BalanceResult { utxo_common::address_balance(self, address).await } + + async fn known_addresses_balances( + &self, + addresses: Vec, + ) -> BalanceResult> { + utxo_common::addresses_balances(self, addresses).await + } } impl HDWalletCoinWithStorageOps for QtumCoin { @@ -989,19 +1026,22 @@ impl HDWalletRpcOps for QtumCoin { } #[async_trait] -impl HDWalletBalanceRpcOps for QtumCoin { +impl AccountBalanceRpcOps for QtumCoin { async fn account_balance_rpc( &self, params: AccountBalanceParams, ) -> MmResult { - coin_balance::common_impl::account_balance_rpc(self, params).await + account_balance::common_impl::account_balance_rpc(self, params).await } +} - async fn scan_for_new_addresses_rpc( +#[async_trait] +impl InitScanAddressesRpcOps for QtumCoin { + async fn init_scan_for_new_addresses_rpc( &self, - params: CheckHDAccountBalanceParams, - ) -> MmResult { - coin_balance::common_impl::scan_for_new_addresses_rpc(self, params).await + params: ScanAddressesParams, + ) -> MmResult { + init_scan_for_new_addresses::common_impl::scan_for_new_addresses_rpc(self, params).await } } diff --git a/mm2src/coins/utxo/qtum_delegation.rs b/mm2src/coins/utxo/qtum_delegation.rs index 805275b672..6a8d844ca2 100644 --- a/mm2src/coins/utxo/qtum_delegation.rs +++ b/mm2src/coins/utxo/qtum_delegation.rs @@ -5,7 +5,7 @@ use crate::qrc20::{contract_addr_into_rpc_format, ContractCallOutput, GenerateQr use crate::utxo::qtum::{QtumBasedCoin, QtumCoin, QtumDelegationOps, QtumDelegationRequest, QtumStakingInfosDetails}; use crate::utxo::rpc_clients::UtxoRpcClientEnum; use crate::utxo::utxo_common::{big_decimal_from_sat_unsigned, UtxoTxBuilder}; -use crate::utxo::{qtum, utxo_common, Address, UtxoCommonOps}; +use crate::utxo::{qtum, utxo_common, Address, GetUtxoListOps, UtxoCommonOps}; use crate::utxo::{PrivKeyNotAllowed, UTXO_LOCK}; use crate::{DelegationError, DelegationFut, DelegationResult, MarketCoinOps, StakingInfos, StakingInfosError, StakingInfosFut, StakingInfosResult, TransactionDetails, TransactionType}; @@ -205,7 +205,7 @@ impl QtumCoin { let my_address = coin.derivation_method.iguana_or_err()?; let staker = self.am_i_currently_staking().await?; - let (unspents, _) = self.list_unspent_ordered(my_address).await?; + let (unspents, _) = self.get_unspent_ordered_list(my_address).await?; let lower_bound = QTUM_LOWER_BOUND_DELEGATION_AMOUNT.into(); let mut amount = BigDecimal::zero(); if staker.is_some() { @@ -273,7 +273,7 @@ impl QtumCoin { let key_pair = utxo.priv_key_policy.key_pair_or_err()?; let my_address = utxo.derivation_method.iguana_or_err()?; - let (unspents, _) = self.list_unspent_ordered(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 { diff --git a/mm2src/coins/utxo/rpc_clients.rs b/mm2src/coins/utxo/rpc_clients.rs index 737d025437..c291e28965 100644 --- a/mm2src/coins/utxo/rpc_clients.rs +++ b/mm2src/coins/utxo/rpc_clients.rs @@ -2,14 +2,16 @@ #![cfg_attr(target_arch = "wasm32", allow(dead_code))] use crate::utxo::{output_script, sat_from_big_decimal}; -use crate::{NumConversError, RpcTransportEventHandler, RpcTransportEventHandlerShared}; +use crate::{big_decimal_from_sat_unsigned, NumConversError, RpcTransportEventHandler, RpcTransportEventHandlerShared}; use async_trait::async_trait; use bigdecimal::BigDecimal; use chain::{BlockHeader, BlockHeaderBits, BlockHeaderNonce, OutPoint, Transaction as UtxoTx}; use common::custom_futures::{select_ok_sequential, FutureTimerExt}; +use common::custom_iter::{CollectInto, TryIntoGroupMap}; use common::executor::{spawn, Timer}; -use common::jsonrpc_client::{JsonRpcClient, JsonRpcError, JsonRpcErrorType, JsonRpcMultiClient, JsonRpcRemoteAddr, - JsonRpcRequest, JsonRpcResponse, JsonRpcResponseFut, RpcRes}; +use common::jsonrpc_client::{JsonRpcBatchClient, JsonRpcBatchResponse, JsonRpcClient, JsonRpcError, JsonRpcErrorType, + JsonRpcId, JsonRpcMultiClient, JsonRpcRemoteAddr, JsonRpcRequest, JsonRpcRequestEnum, + JsonRpcResponse, JsonRpcResponseEnum, JsonRpcResponseFut, RpcRes}; use common::log::{error, info, warn}; use common::mm_error::prelude::*; use common::mm_number::{BigInt, MmNumber}; @@ -24,6 +26,7 @@ use futures01::future::select_ok; use futures01::sync::{mpsc, oneshot}; use futures01::{Future, Sink, Stream}; use http::Uri; +use itertools::Itertools; use keys::hash::H256; use keys::{Address, Type as ScriptType}; #[cfg(test)] use mocktopus::macros::*; @@ -32,7 +35,8 @@ use serde_json::{self as json, Value as Json}; use serialization::{deserialize, serialize, serialize_with_flags, CoinVariant, CompactInteger, Reader, SERIALIZE_TRANSACTION_WITNESS}; use sha2::{Digest, Sha256}; -use std::collections::hash_map::{Entry, HashMap}; +use std::collections::hash_map::Entry; +use std::collections::HashMap; use std::fmt; use std::io; use std::net::{SocketAddr, ToSocketAddrs}; @@ -57,6 +61,12 @@ cfg_native! { } pub type AddressesByLabelResult = HashMap; +pub type JsonRpcPendingRequestsShared = Arc>; +pub type JsonRpcPendingRequests = HashMap>; +pub type UnspentMap = HashMap>; + +type ElectrumScriptHash = String; +type ScriptHashUnspents = Vec; #[derive(Debug, Deserialize)] #[allow(dead_code)] @@ -243,6 +253,7 @@ pub enum UtxoRpcError { impl From for UtxoRpcError { fn from(e: JsonRpcError) -> Self { match e.error { + JsonRpcErrorType::InvalidRequest(_) => UtxoRpcError::Internal(e.to_string()), JsonRpcErrorType::Transport(_) => UtxoRpcError::Transport(e), JsonRpcErrorType::Parse(_, _) | JsonRpcErrorType::Response(_, _) => UtxoRpcError::ResponseParseError(e), } @@ -260,21 +271,38 @@ impl From for UtxoRpcError { /// Common operations that both types of UTXO clients have but implement them differently #[async_trait] pub trait UtxoRpcClientOps: fmt::Debug + Send + Sync + 'static { + /// Returns available unspents for the given `address`. fn list_unspent(&self, address: &Address, decimals: u8) -> UtxoRpcFut>; + /// Returns available unspents for every given `addresses`. + fn list_unspent_group(&self, addresses: Vec
, decimals: u8) -> UtxoRpcFut; + + /// Submits the given `tx` transaction to blockchain network. fn send_transaction(&self, tx: &UtxoTx) -> UtxoRpcFut; + /// Submits the raw `tx` transaction (serialized, hex-encoded) to blockchain network. fn send_raw_transaction(&self, tx: BytesJson) -> UtxoRpcFut; + /// Returns raw transaction (serialized, hex-encoded) by the given `txid`. fn get_transaction_bytes(&self, txid: &H256Json) -> UtxoRpcFut; + /// Returns verbose transaction by the given `txid`. fn get_verbose_transaction(&self, txid: &H256Json) -> UtxoRpcFut; + /// Returns verbose transactions in the same order they were requested. + fn get_verbose_transactions(&self, tx_ids: &[H256Json]) -> UtxoRpcFut>; + + /// Returns the height of the most-work fully-validated chain. fn get_block_count(&self) -> UtxoRpcFut; + /// Requests balance of the given `address`. fn display_balance(&self, address: Address, decimals: u8) -> RpcRes; - /// returns fee estimation per KByte in satoshis + /// Requests balances of the given `addresses`. + /// The pairs `(Address, BigDecimal)` are guaranteed to be in the same order in which they were requested. + fn display_balances(&self, addresses: Vec
, decimals: u8) -> UtxoRpcFut>; + + /// Returns fee estimation per KByte in satoshis. fn estimate_fee_sat( &self, decimals: u8, @@ -283,8 +311,10 @@ pub trait UtxoRpcClientOps: fmt::Debug + Send + Sync + 'static { n_blocks: u32, ) -> UtxoRpcFut; + /// Returns the minimum fee a low-priority transaction must pay in order to be accepted to the daemon’s memory pool. fn get_relay_fee(&self) -> RpcRes; + /// Tries to find a transaction that spends the specified `vout` output of the `tx_hash` transaction. fn find_output_spend( &self, tx_hash: H256, @@ -301,10 +331,12 @@ pub trait UtxoRpcClientOps: fmt::Debug + Send + Sync + 'static { coin_variant: CoinVariant, ) -> UtxoRpcFut; + /// Returns block time in seconds since epoch (Jan 1 1970 GMT). async fn get_block_timestamp(&self, height: u64) -> Result>; } #[derive(Clone, Deserialize, Debug)] +#[cfg_attr(test, derive(Default))] pub struct NativeUnspent { pub txid: H256Json, pub vout: u32, @@ -565,14 +597,14 @@ impl JsonRpcClient for NativeClientImpl { fn client_info(&self) -> String { UtxoJsonRpcClientInfo::client_info(self) } #[cfg(target_arch = "wasm32")] - fn transport(&self, _request: JsonRpcRequest) -> JsonRpcResponseFut { + fn transport(&self, _request: JsonRpcRequestEnum) -> JsonRpcResponseFut { Box::new(futures01::future::err(ERRL!( "'NativeClientImpl' must be used in native mode only" ))) } #[cfg(not(target_arch = "wasm32"))] - fn transport(&self, request: JsonRpcRequest) -> JsonRpcResponseFut { + fn transport(&self, request: JsonRpcRequestEnum) -> JsonRpcResponseFut { use common::transport::slurp_req; let request_body = try_fus!(json::to_string(&request)); @@ -589,7 +621,7 @@ impl JsonRpcClient for NativeClientImpl { let event_handles = self.event_handlers.clone(); Box::new(slurp_req(http_request).boxed().compat().then( - move |result| -> Result<(JsonRpcRemoteAddr, JsonRpcResponse), String> { + move |result| -> Result<(JsonRpcRemoteAddr, JsonRpcResponseEnum), String> { let res = try_s!(result); // measure now only body length, because the `hyper` crate doesn't allow to get total HTTP packet length event_handles.on_incoming_response(&res.2); @@ -612,6 +644,8 @@ impl JsonRpcClient for NativeClientImpl { } } +impl JsonRpcBatchClient for NativeClientImpl {} + // 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)] @@ -639,6 +673,45 @@ impl UtxoRpcClientOps for NativeClient { Box::new(fut) } + fn list_unspent_group(&self, addresses: Vec
, decimals: u8) -> UtxoRpcFut { + let mut addresses_str = Vec::with_capacity(addresses.len()); + let mut addresses_map = HashMap::with_capacity(addresses.len()); + for addr in addresses { + let addr_str = addr.to_string(); + addresses_str.push(addr_str.clone()); + addresses_map.insert(addr_str, addr); + } + + let fut = self + .list_unspent_impl(0, std::i32::MAX, addresses_str) + .map_to_mm_fut(UtxoRpcError::from) + .and_then(move |unspents| { + unspents + .into_iter() + // Convert `Vec` into `UnspentMap`. + .map(|unspent| { + let orig_address = addresses_map + .get(&unspent.address) + .or_mm_err(|| { + UtxoRpcError::InvalidResponse(format!("Unexpected address '{}'", unspent.address)) + })? + .clone(); + let unspent_info = UnspentInfo { + outpoint: OutPoint { + hash: unspent.txid.reversed().into(), + index: unspent.vout, + }, + value: sat_from_big_decimal(&unspent.amount.to_decimal(), decimals)?, + height: None, + }; + Ok((orig_address, unspent_info)) + }) + // Collect `(Address, UnspentInfo)` items into `HashMap>` grouped by the addresses. + .try_into_group_map() + }); + Box::new(fut) + } + fn send_transaction(&self, tx: &UtxoTx) -> UtxoRpcFut { let tx_bytes = if tx.has_witness() { BytesJson::from(serialize_with_flags(tx, SERIALIZE_TRANSACTION_WITNESS)) @@ -661,6 +734,13 @@ impl UtxoRpcClientOps for NativeClient { Box::new(self.get_raw_transaction_verbose(txid).map_to_mm_fut(UtxoRpcError::from)) } + fn get_verbose_transactions(&self, tx_ids: &[H256Json]) -> UtxoRpcFut> { + Box::new( + self.get_raw_transaction_verbose_batch(tx_ids) + .map_to_mm_fut(UtxoRpcError::from), + ) + } + fn get_block_count(&self) -> UtxoRpcFut { Box::new(self.0.get_block_count().map_to_mm_fut(UtxoRpcError::from)) } @@ -676,6 +756,22 @@ impl UtxoRpcClientOps for NativeClient { ) } + fn display_balances(&self, addresses: Vec
, decimals: u8) -> UtxoRpcFut> { + let this = self.clone(); + let fut = async move { + let unspent_map = this.list_unspent_group(addresses.clone(), decimals).compat().await?; + let balances = addresses + .into_iter() + .map(|address| { + let balance = address_balance_from_unspent_map(&address, &unspent_map, decimals); + (address, balance) + }) + .collect(); + Ok(balances) + }; + Box::new(fut.boxed().compat()) + } + fn estimate_fee_sat( &self, decimals: u8, @@ -875,6 +971,16 @@ impl NativeClientImpl { rpc_func!(self, "getrawtransaction", txid, verbose) } + /// https://developer.bitcoin.org/reference/rpc/getrawtransaction.html + /// Always returns verbose transactions in the same order they were requested. + fn get_raw_transaction_verbose_batch(&self, tx_ids: &[H256Json]) -> RpcRes> { + let verbose = 1; + let requests = tx_ids + .iter() + .map(|txid| rpc_req!(self, "getrawtransaction", txid, verbose)); + self.batch_rpc(requests) + } + /// https://developer.bitcoin.org/reference/rpc/getrawtransaction.html /// Always returns transaction bytes pub fn get_raw_transaction_bytes(&self, txid: &H256Json) -> RpcRes { @@ -1076,12 +1182,14 @@ impl ElectrumBlockHeaderV12 { } } + #[inline] pub fn as_hex(&self) -> String { let block_header = self.as_block_header(); let serialized = serialize(&block_header); hex::encode(serialized) } + #[inline] pub fn hash(&self) -> H256Json { let block_header = self.as_block_header(); BlockHeader::hash(&block_header).into() @@ -1166,12 +1274,22 @@ pub struct ElectrumBalance { pub(crate) unconfirmed: i128, } +impl ElectrumBalance { + #[inline] + pub fn to_big_decimal(&self, decimals: u8) -> BigDecimal { + let balance_sat = BigInt::from(self.confirmed) + BigInt::from(self.unconfirmed); + BigDecimal::from(balance_sat) / BigDecimal::from(10u64.pow(decimals as u32)) + } +} + +#[inline] fn sha_256(input: &[u8]) -> Vec { let mut sha = Sha256::new(); sha.input(input); sha.result().to_vec() } +#[inline] pub fn electrum_script_hash(script: &[u8]) -> Vec { let mut result = sha_256(script); result.reverse(); @@ -1326,7 +1444,7 @@ pub struct ElectrumConnection { /// The Sender used to shutdown the background connection loop when ElectrumConnection is dropped shutdown_tx: Option>, /// Responses are stored here - responses: Arc>>>, + responses: JsonRpcPendingRequestsShared, /// Selected protocol version. The value is initialized after the server.version RPC call. protocol_version: AsyncMutex>, } @@ -1419,8 +1537,8 @@ pub struct ElectrumClientImpl { async fn electrum_request_multi( client: ElectrumClient, - request: JsonRpcRequest, -) -> Result<(JsonRpcRemoteAddr, JsonRpcResponse), String> { + request: JsonRpcRequestEnum, +) -> Result<(JsonRpcRemoteAddr, JsonRpcResponseEnum), String> { let mut futures = vec![]; let connections = client.connections.lock().await; for (i, connection) in connections.iter().enumerate() { @@ -1443,31 +1561,32 @@ async fn electrum_request_multi( if futures.is_empty() { return ERR!("All electrums are currently disconnected"); } - if request.method != "server.ping" { - match select_ok_sequential(futures).compat().await { - Ok((res, no_of_failed_requests)) => { - client.clone().rotate_servers(no_of_failed_requests).await; - Ok(res) - }, - Err(e) => return ERR!("{:?}", e), - } - } else { - // server.ping must be sent to all servers to keep all connections alive - Ok(try_s!( - select_ok(futures) + + match request { + JsonRpcRequestEnum::Single(single) if single.method == "server.ping" => { + // server.ping must be sent to all servers to keep all connections alive + return select_ok(futures) .map(|(result, _)| result) .map_err(|e| ERRL!("{:?}", e)) .compat() - .await - )) + .await; + }, + _ => (), } + + let (res, no_of_failed_requests) = select_ok_sequential(futures) + .compat() + .await + .map_err(|e| ERRL!("{:?}", e))?; + client.rotate_servers(no_of_failed_requests).await; + Ok(res) } async fn electrum_request_to( client: ElectrumClient, - request: JsonRpcRequest, + request: JsonRpcRequestEnum, to_addr: String, -) -> Result<(JsonRpcRemoteAddr, JsonRpcResponse), String> { +) -> Result<(JsonRpcRemoteAddr, JsonRpcResponseEnum), String> { let (tx, responses) = { let connections = client.connections.lock().await; let connection = connections @@ -1576,13 +1695,15 @@ impl JsonRpcClient for ElectrumClient { fn client_info(&self) -> String { UtxoJsonRpcClientInfo::client_info(self) } - fn transport(&self, request: JsonRpcRequest) -> JsonRpcResponseFut { + fn transport(&self, request: JsonRpcRequestEnum) -> JsonRpcResponseFut { Box::new(electrum_request_multi(self.clone(), request).boxed().compat()) } } +impl JsonRpcBatchClient for ElectrumClient {} + impl JsonRpcMultiClient for ElectrumClient { - fn transport_exact(&self, to_addr: String, request: JsonRpcRequest) -> JsonRpcResponseFut { + fn transport_exact(&self, to_addr: String, request: JsonRpcRequestEnum) -> JsonRpcResponseFut { Box::new(electrum_request_to(self.clone(), request, to_addr).boxed().compat()) } } @@ -1628,6 +1749,27 @@ impl ElectrumClient { Box::new(fut.boxed().compat()) } + /// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-scripthash-listunspent + /// It can return duplicates sometimes: https://github.com/artemii235/SuperNET/issues/269 + /// We should remove them to build valid transactions. + /// Please note the function returns `ScriptHashUnspents` elements in the same order in which they were requested. + pub fn scripthash_list_unspent_batch(&self, hashes: Vec) -> RpcRes> { + let requests = hashes + .iter() + .map(|hash| rpc_req!(self, "blockchain.scripthash.listunspent", hash)); + Box::new(self.batch_rpc(requests).map(move |unspents: Vec| { + unspents + .into_iter() + .map(|hash_unspents| { + hash_unspents + .into_iter() + .unique_by(|unspent| (unspent.tx_hash, unspent.tx_pos)) + .collect::>() + }) + .collect() + })) + } + /// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-scripthash-get-history pub fn scripthash_get_history(&self, hash: &str) -> RpcRes> { rpc_func!(self, "blockchain.scripthash.get_history", hash) @@ -1644,6 +1786,18 @@ impl ElectrumClient { Box::new(fut.boxed().compat()) } + /// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-scripthash-gethistory + /// Requests balances in a batch and returns them in the same order they were requested. + pub fn scripthash_get_balances(&self, hashes: I) -> RpcRes> + where + I: IntoIterator, + { + let requests = hashes + .into_iter() + .map(|hash| rpc_req!(self, "blockchain.scripthash.get_balance", &hash)); + self.batch_rpc(requests) + } + /// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-headers-subscribe pub fn blockchain_headers_subscribe(&self) -> RpcRes { rpc_func!(self, "blockchain.headers.subscribe") @@ -1752,6 +1906,33 @@ impl UtxoRpcClientOps for ElectrumClient { ) } + fn list_unspent_group(&self, addresses: Vec
, _decimals: u8) -> UtxoRpcFut { + let script_hashes = addresses + .iter() + .map(|addr| { + let script = output_script(addr, ScriptType::P2PKH); + let script_hash = electrum_script_hash(&script); + hex::encode(script_hash) + }) + .collect(); + + let this = self.clone(); + let fut = async move { + let unspents = this.scripthash_list_unspent_batch(script_hashes).compat().await?; + + let unspent_map = addresses + .into_iter() + // `scripthash_list_unspent_batch` returns `ScriptHashUnspents` elements in the same order in which they were requested. + // So we can zip `addresses` and `unspents` into one iterator. + .zip(unspents) + // Map `(Address, Vec)` pairs into `(Address, Vec)`. + .map(|(address, electrum_unspents)| (address, electrum_unspents.collect_into())) + .collect(); + Ok(unspent_map) + }; + Box::new(fut.boxed().compat()) + } + fn send_transaction(&self, tx: &UtxoTx) -> UtxoRpcFut { let bytes = if tx.has_witness() { BytesJson::from(serialize_with_flags(tx, SERIALIZE_TRANSACTION_WITNESS)) @@ -1785,6 +1966,16 @@ impl UtxoRpcClientOps for ElectrumClient { Box::new(rpc_func!(self, "blockchain.transaction.get", txid, verbose).map_to_mm_fut(UtxoRpcError::from)) } + /// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-transaction-get + /// Returns verbose transactions in a batch. + fn get_verbose_transactions(&self, tx_ids: &[H256Json]) -> UtxoRpcFut> { + let verbose = true; + let requests = tx_ids + .iter() + .map(|txid| rpc_req!(self, "blockchain.transaction.get", txid, verbose)); + Box::new(self.batch_rpc(requests).map_to_mm_fut(UtxoRpcError::from)) + } + fn get_block_count(&self) -> UtxoRpcFut { Box::new( self.blockchain_headers_subscribe() @@ -1796,10 +1987,32 @@ impl UtxoRpcClientOps for ElectrumClient { fn display_balance(&self, address: Address, decimals: u8) -> RpcRes { let hash = electrum_script_hash(&output_script(&address, ScriptType::P2PKH)); let hash_str = hex::encode(hash); - Box::new(self.scripthash_get_balance(&hash_str).map(move |result| { - let balance_sat = BigInt::from(result.confirmed) + BigInt::from(result.unconfirmed); - BigDecimal::from(balance_sat) / BigDecimal::from(10u64.pow(decimals as u32)) - })) + Box::new( + self.scripthash_get_balance(&hash_str) + .map(move |electrum_balance| electrum_balance.to_big_decimal(decimals)), + ) + } + + fn display_balances(&self, addresses: Vec
, decimals: u8) -> UtxoRpcFut> { + let this = self.clone(); + let fut = async move { + let hashes = addresses.iter().map(|address| { + let hash = electrum_script_hash(&output_script(address, ScriptType::P2PKH)); + hex::encode(hash) + }); + + let electrum_balances = this.scripthash_get_balances(hashes).compat().await?; + let balances = electrum_balances + .into_iter() + // `scripthash_get_balances` returns `ElectrumBalance` elements in the same order in which they were requested. + // So we can zip `addresses` and the balances into one iterator. + .zip(addresses) + .map(|(electrum_balance, address)| (address, electrum_balance.to_big_decimal(decimals))) + .collect(); + Ok(balances) + }; + + Box::new(fut.boxed().compat()) } fn estimate_fee_sat( @@ -1927,62 +2140,56 @@ fn rx_to_stream(rx: mpsc::Receiver>) -> impl Stream, Erro rx.map_err(|_| panic!("errors not possible on rx")) } -async fn electrum_process_json( - raw_json: Json, - arc: &Arc>>>, -) { +async fn electrum_process_json(raw_json: Json, arc: &JsonRpcPendingRequestsShared) { // detect if we got standard JSONRPC response or subscription response as JSONRPC request - if raw_json["method"].is_null() && raw_json["params"].is_null() { - let response: JsonRpcResponse = match json::from_value(raw_json) { - Ok(res) => res, - Err(e) => { - error!("{}", e); - return; - }, - }; - let mut resp = arc.lock().await; - // the corresponding sender may not exist, receiver may be dropped - // these situations are not considered as errors so we just silently skip them - if let Some(tx) = resp.remove(&response.id.to_string()) { - tx.send(response).unwrap_or(()) - } - drop(resp); - } else { - let request: JsonRpcRequest = match json::from_value(raw_json) { - Ok(res) => res, - Err(e) => { - error!("{}", e); - return; - }, - }; - let id = match request.method.as_ref() { - BLOCKCHAIN_HEADERS_SUB_ID => BLOCKCHAIN_HEADERS_SUB_ID, - _ => { - error!("Couldn't get id of request {:?}", request); - return; - }, - }; + #[derive(Deserialize)] + #[serde(untagged)] + enum ElectrumRpcResponseEnum { + /// The standard JSONRPC single response. + SingleResponse(JsonRpcResponse), + /// The batch of standard JSONRPC responses. + BatchResponses(JsonRpcBatchResponse), + /// The subscription response as JSONRPC request. + SubscriptionNotification(JsonRpcRequest), + } + + let response: ElectrumRpcResponseEnum = match json::from_value(raw_json) { + Ok(res) => res, + Err(e) => { + error!("{}", e); + return; + }, + }; - let response = JsonRpcResponse { - id: id.into(), - jsonrpc: "2.0".into(), - result: request.params[0].clone(), - error: Json::Null, - }; - let mut resp = arc.lock().await; - // the corresponding sender may not exist, receiver may be dropped - // these situations are not considered as errors so we just silently skip them - if let Some(tx) = resp.remove(&response.id.to_string()) { - tx.send(response).unwrap_or(()) - } - drop(resp); + let response = match response { + ElectrumRpcResponseEnum::SingleResponse(single) => JsonRpcResponseEnum::Single(single), + ElectrumRpcResponseEnum::BatchResponses(batch) => JsonRpcResponseEnum::Batch(batch), + ElectrumRpcResponseEnum::SubscriptionNotification(req) => { + let id = match req.method.as_ref() { + BLOCKCHAIN_HEADERS_SUB_ID => BLOCKCHAIN_HEADERS_SUB_ID, + _ => { + error!("Couldn't get id of request {:?}", req); + return; + }, + }; + JsonRpcResponseEnum::Single(JsonRpcResponse { + id: id.into(), + jsonrpc: "2.0".into(), + result: req.params[0].clone(), + error: Json::Null, + }) + }, + }; + + // the corresponding sender may not exist, receiver may be dropped + // these situations are not considered as errors so we just silently skip them + let mut pending = arc.lock().await; + if let Some(tx) = pending.remove(&response.rpc_id()) { + tx.send(response).ok(); } } -async fn electrum_process_chunk( - chunk: &[u8], - arc: &Arc>>>, -) { +async fn electrum_process_chunk(chunk: &[u8], arc: &JsonRpcPendingRequestsShared) { // we should split the received chunk because we can get several responses in 1 chunk. let split = chunk.split(|item| *item == b'\n'); for chunk in split { @@ -2091,7 +2298,7 @@ async fn electrum_last_chunk_loop(last_chunk: Arc) { async fn connect_loop( config: ElectrumConfig, addr: String, - responses: Arc>>>, + responses: JsonRpcPendingRequestsShared, connection_tx: Arc>>>>, event_handlers: Vec, ) -> Result<(), ()> { @@ -2212,7 +2419,7 @@ async fn connect_loop( async fn connect_loop( _config: ElectrumConfig, addr: String, - responses: Arc>>>, + responses: JsonRpcPendingRequestsShared, connection_tx: Arc>>>>, event_handlers: Vec, ) -> Result<(), ()> { @@ -2319,7 +2526,7 @@ fn electrum_connect( event_handlers: Vec, ) -> ElectrumConnection { let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>(); - let responses = Arc::new(AsyncMutex::new(HashMap::new())); + let responses = Arc::new(AsyncMutex::new(JsonRpcPendingRequests::default())); let tx = Arc::new(AsyncMutex::new(None)); let connect_loop = connect_loop( @@ -2343,11 +2550,11 @@ fn electrum_connect( } fn electrum_request( - request: JsonRpcRequest, + request: JsonRpcRequestEnum, tx: mpsc::Sender>, - responses: Arc>>>, + responses: JsonRpcPendingRequestsShared, timeout: u64, -) -> Box + Send + 'static> { +) -> Box + Send + 'static> { let send_fut = async move { let mut json = try_s!(json::to_string(&request)); #[cfg(not(target_arch = "wasm"))] @@ -2357,12 +2564,11 @@ fn electrum_request( json.push('\n'); } - let request_id = request.get_id().to_string(); let (req_tx, resp_rx) = async_oneshot::channel(); - responses.lock().await.insert(request_id, req_tx); + responses.lock().await.insert(request.rpc_id(), req_tx); try_s!(tx.send(json.into_bytes()).compat().await); - let response = try_s!(resp_rx.await); - Ok(response) + let resps = try_s!(resp_rx.await); + Ok(resps) }; let send_fut = send_fut .boxed() @@ -2375,3 +2581,15 @@ fn electrum_request( .map_err(|e| ERRL!("{}", e)); Box::new(send_fut) } + +fn address_balance_from_unspent_map(address: &Address, unspent_map: &UnspentMap, decimals: u8) -> BigDecimal { + let unspents = match unspent_map.get(address) { + Some(unspents) => unspents, + // If `balances` doesn't contain `address`, there are no unspents related to the address. + // Consider the balance of that address equal to 0. + None => return BigDecimal::from(0), + }; + unspents.iter().fold(BigDecimal::from(0), |sum, unspent| { + sum + big_decimal_from_sat_unsigned(unspent.value, decimals) + }) +} diff --git a/mm2src/coins/utxo/slp.rs b/mm2src/coins/utxo/slp.rs index 3399e16822..b742de25a1 100644 --- a/mm2src/coins/utxo/slp.rs +++ b/mm2src/coins/utxo/slp.rs @@ -8,8 +8,8 @@ use crate::utxo::bchd_grpc::{check_slp_transaction, validate_slp_utxos, Validate use crate::utxo::rpc_clients::{UnspentInfo, UtxoRpcClientEnum, UtxoRpcError, UtxoRpcResult}; use crate::utxo::utxo_common::{self, big_decimal_from_sat_unsigned, payment_script, UtxoTxBuilder}; use crate::utxo::{generate_and_send_tx, sat_from_big_decimal, ActualTxFee, AdditionalTxData, BroadcastTxErr, - FeePolicy, GenerateTxError, RecentlySpentOutPoints, UtxoCoinConf, UtxoCoinFields, UtxoCommonOps, - UtxoTx, UtxoTxBroadcastOps, UtxoTxGenerationOps}; + FeePolicy, GenerateTxError, RecentlySpentOutPointsGuard, UtxoCoinConf, UtxoCoinFields, + UtxoCommonOps, UtxoTx, UtxoTxBroadcastOps, UtxoTxGenerationOps}; use crate::{BalanceFut, CoinBalance, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MarketCoinOps, MmCoin, NegotiateSwapContractAddrErr, NumConversError, PrivKeyNotAllowed, RawTransactionFut, RawTransactionRequest, SignatureResult, SwapOps, TradeFee, TradePreimageError, TradePreimageFut, @@ -28,7 +28,6 @@ use common::now_ms; use common::privkey::key_pair_from_secret; use derive_more::Display; use futures::compat::Future01CompatExt; -use futures::lock::MutexGuard as AsyncMutexGuard; use futures::{FutureExt, TryFutureExt}; use futures01::Future; use hex::FromHexError; @@ -332,11 +331,7 @@ impl SlpToken { /// Returns unspents of the SLP token plus plain BCH UTXOs plus RecentlySpentOutPoints mutex guard async fn slp_unspents_for_spend( &self, - ) -> UtxoRpcResult<( - Vec, - Vec, - AsyncMutexGuard<'_, RecentlySpentOutPoints>, - )> { + ) -> UtxoRpcResult<(Vec, Vec, RecentlySpentOutPointsGuard<'_>)> { self.platform_coin.get_token_utxos_for_spend(&self.conf.token_id).await } @@ -350,7 +345,7 @@ impl SlpToken { async fn generate_slp_tx_preimage( &self, slp_outputs: Vec, - ) -> Result<(SlpTxPreimage, AsyncMutexGuard<'_, RecentlySpentOutPoints>), MmError> { + ) -> Result<(SlpTxPreimage, RecentlySpentOutPointsGuard<'_>), MmError> { // the limit is 19, but we may require the change to be added if slp_outputs.len() > 18 { return MmError::err(GenSlpSpendErr::TooManyOutputs); @@ -1821,6 +1816,7 @@ pub fn slp_addr_from_pubkey_str(pubkey: &str, prefix: &str) -> Result = AsyncMutex::new(()); -} - -/// Try load transaction from cache. -/// Note: tx.confirmations can be out-of-date. -pub async fn load_transaction_from_cache( - tx_cache_path: &Path, - txid: &H256Json, -) -> Result, String> { - let _lock = TX_CACHE_LOCK.lock().await; - - let path = cached_transaction_path(tx_cache_path, txid); - let data = try_s!(safe_slurp(&path)); - if data.is_empty() { - // couldn't find corresponding file - return Ok(None); - } - - let data = try_s!(String::from_utf8(data)); - serde_json::from_str(&data).map(Some).map_err(|e| ERRL!("{}", e)) -} - -/// Upload transaction to cache. -pub async fn cache_transaction(tx_cache_path: &Path, tx: &RpcTransaction) -> Result<(), String> { - let _lock = TX_CACHE_LOCK.lock().await; - let path = cached_transaction_path(tx_cache_path, &tx.txid); - let tmp_path = format!("{}.tmp", path.display()); - - let content = try_s!(serde_json::to_string(tx)); - - try_s!(std::fs::write(&tmp_path, content)); - try_s!(std::fs::rename(tmp_path, path)); - Ok(()) -} - -fn cached_transaction_path(tx_cache_path: &Path, txid: &H256Json) -> PathBuf { - tx_cache_path.join(format!("{:?}", txid)) -} diff --git a/mm2src/coins/utxo/tx_cache/dummy_tx_cache.rs b/mm2src/coins/utxo/tx_cache/dummy_tx_cache.rs new file mode 100644 index 0000000000..e18244ca39 --- /dev/null +++ b/mm2src/coins/utxo/tx_cache/dummy_tx_cache.rs @@ -0,0 +1,20 @@ +use crate::utxo::tx_cache::{TxCacheResult, UtxoVerboseCacheOps}; +use async_trait::async_trait; +use rpc::v1::types::{Transaction as RpcTransaction, H256 as H256Json}; +use std::collections::{HashMap, HashSet}; + +/// The dummy TX cache. +#[derive(Debug, Default)] +pub struct DummyVerboseCache; + +#[async_trait] +impl UtxoVerboseCacheOps for DummyVerboseCache { + async fn load_transactions_from_cache_concurrently( + &self, + tx_ids: HashSet, + ) -> HashMap>> { + tx_ids.into_iter().map(|txid| (txid, Ok(None))).collect() + } + + async fn cache_transactions_concurrently(&self, _txs: &HashMap) {} +} diff --git a/mm2src/coins/utxo/tx_cache/fs_tx_cache.rs b/mm2src/coins/utxo/tx_cache/fs_tx_cache.rs new file mode 100644 index 0000000000..6cee3e5ee9 --- /dev/null +++ b/mm2src/coins/utxo/tx_cache/fs_tx_cache.rs @@ -0,0 +1,111 @@ +use crate::utxo::tx_cache::{TxCacheError, TxCacheResult, UtxoVerboseCacheOps}; +use async_trait::async_trait; +use common::fs::{read_json, write_json, FsJsonError}; +use common::log::LogOnError; +use common::mm_error::prelude::*; +use futures::lock::Mutex as AsyncMutex; +use futures::FutureExt; +use parking_lot::Mutex as PaMutex; +use rpc::v1::types::{Transaction as RpcTransaction, H256 as H256Json}; +use std::collections::hash_map::RawEntryMut; +use std::collections::{HashMap, HashSet}; +use std::path::PathBuf; +use std::sync::Arc; + +lazy_static! { + static ref TX_CACHE_LOCK: TxCacheLock = TxCacheLock::default(); +} + +impl From for TxCacheError { + fn from(e: FsJsonError) -> Self { + match e { + FsJsonError::IoReading(loading) => TxCacheError::ErrorLoading(loading.to_string()), + FsJsonError::IoWriting(writing) => TxCacheError::ErrorSaving(writing.to_string()), + FsJsonError::Serializing(ser) => TxCacheError::ErrorSerializing(ser.to_string()), + FsJsonError::Deserializing(de) => TxCacheError::ErrorDeserializing(de.to_string()), + } + } +} + +/// The cache lock is used to avoid reading and writing the same files at the same time. +#[derive(Default)] +struct TxCacheLock { + /// The collection of `Ticker -> Mutex` pairs. + mutexes: PaMutex>>>, +} + +impl TxCacheLock { + /// Get the mutex corresponding to the specified `ticker`. + pub fn mutex_by_ticker(&self, ticker: &str) -> Arc> { + let mut locks = self.mutexes.lock(); + + match locks.raw_entry_mut().from_key(ticker) { + RawEntryMut::Occupied(mutex) => mutex.get().clone(), + RawEntryMut::Vacant(vacant_mutex) => { + let (_key, mutex) = vacant_mutex.insert(ticker.to_owned(), Arc::new(AsyncMutex::new(()))); + mutex.clone() + }, + } + } +} + +/// The cache instance that assigned to a specified coin. +/// +/// Please note [`UtxoVerboseCache::ticker`] may not equal to [`Coin::ticker`]. +/// In particular, `QRC20` tokens have the same transactions as `Qtum` coin, +/// so [`Qrc20Coin::platform_ticker`] is used as [`UtxoVerboseCache::ticker`]. +#[derive(Debug)] +pub struct FsVerboseCache { + ticker: String, + tx_cache_path: PathBuf, +} + +#[async_trait] +impl UtxoVerboseCacheOps for FsVerboseCache { + async fn load_transactions_from_cache_concurrently( + &self, + tx_ids: HashSet, + ) -> HashMap>> { + let mutex = TX_CACHE_LOCK.mutex_by_ticker(&self.ticker); + let _lock = mutex.lock().await; + + let it = tx_ids + .into_iter() + .map(|txid| self.load_transaction_from_cache(txid).map(move |res| (txid, res))); + futures::future::join_all(it).await.into_iter().collect() + } + + async fn cache_transactions_concurrently(&self, txs: &HashMap) { + let mutex = TX_CACHE_LOCK.mutex_by_ticker(&self.ticker); + let _lock = mutex.lock().await; + + let it = txs.iter().map(|(_txid, tx)| self.cache_transaction(tx)); + futures::future::join_all(it) + .await + .into_iter() + .for_each(|tx| tx.error_log()); + } +} + +impl FsVerboseCache { + #[inline] + pub fn new(ticker: String, tx_cache_path: PathBuf) -> FsVerboseCache { FsVerboseCache { ticker, tx_cache_path } } + + /// Tries to load transaction from cache. + /// Note: `tx.confirmations` can be out-of-date. + async fn load_transaction_from_cache(&self, txid: H256Json) -> TxCacheResult> { + let path = self.cached_transaction_path(&txid); + read_json(&path).await.mm_err(TxCacheError::from) + } + + /// Uploads transaction to cache. + async fn cache_transaction(&self, tx: &RpcTransaction) -> TxCacheResult<()> { + const USE_TMP_FILE: bool = true; + + let path = self.cached_transaction_path(&tx.txid); + write_json(tx, &path, USE_TMP_FILE).await.mm_err(TxCacheError::from) + } + + #[inline] + fn cached_transaction_path(&self, txid: &H256Json) -> PathBuf { self.tx_cache_path.join(format!("{:?}", txid)) } +} diff --git a/mm2src/coins/utxo/tx_cache/mod.rs b/mm2src/coins/utxo/tx_cache/mod.rs new file mode 100644 index 0000000000..98c33e5868 --- /dev/null +++ b/mm2src/coins/utxo/tx_cache/mod.rs @@ -0,0 +1,47 @@ +use async_trait::async_trait; +use common::mm_error::prelude::*; +use derive_more::Display; +use rpc::v1::types::{Transaction as RpcTransaction, H256 as H256Json}; +use std::collections::{HashMap, HashSet}; +use std::fmt; +use std::sync::Arc; + +pub mod dummy_tx_cache; +#[cfg(not(target_arch = "wasm32"))] pub mod fs_tx_cache; + +#[cfg(target_arch = "wasm32")] +pub mod wasm_tx_cache { + pub type WasmVerboseCache = crate::utxo::tx_cache::dummy_tx_cache::DummyVerboseCache; +} + +pub type TxCacheResult = MmResult; +pub type UtxoVerboseCacheShared = Arc; + +#[derive(Debug, Display)] +pub enum TxCacheError { + ErrorLoading(String), + ErrorSaving(String), + ErrorDeserializing(String), + ErrorSerializing(String), +} + +#[async_trait] +pub trait UtxoVerboseCacheOps: fmt::Debug { + #[inline] + fn into_shared(self) -> UtxoVerboseCacheShared + where + Self: Sized + Send + Sync + 'static, + { + Arc::new(self) + } + + /// Tries to load transactions from cache concurrently. + /// Please note `tx.confirmations` can be out-of-date. + async fn load_transactions_from_cache_concurrently( + &self, + tx_ids: HashSet, + ) -> HashMap>>; + + /// Uploads transactions to cache concurrently. + async fn cache_transactions_concurrently(&self, txs: &HashMap); +} diff --git a/mm2src/coins/utxo/utxo_builder/utxo_arc_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_arc_builder.rs index 9e70474b98..8d13d26b4a 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_arc_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_arc_builder.rs @@ -2,7 +2,7 @@ use crate::utxo::utxo_block_header_storage::BlockHeaderStorage; use crate::utxo::utxo_builder::{UtxoCoinBuildError, UtxoCoinBuilder, UtxoCoinBuilderCommonOps, UtxoFieldsWithHardwareWalletBuilder, UtxoFieldsWithIguanaPrivKeyBuilder}; use crate::utxo::utxo_common::{block_header_utxo_loop, merge_utxo_loop}; -use crate::utxo::{UtxoArc, UtxoCommonOps, UtxoWeak}; +use crate::utxo::{GetUtxoListOps, UtxoArc, UtxoCommonOps, UtxoWeak}; use crate::{PrivKeyBuildPolicy, UtxoActivationParams}; use async_trait::async_trait; use common::executor::spawn; @@ -75,7 +75,7 @@ impl<'a, F, T> UtxoFieldsWithHardwareWalletBuilder for UtxoArcBuilder<'a, F, T> impl<'a, F, T> UtxoCoinBuilder for UtxoArcBuilder<'a, F, T> where F: Fn(UtxoArc) -> T + Clone + Send + Sync + 'static, - T: UtxoCommonOps, + T: UtxoCommonOps + GetUtxoListOps, { type ResultCoin = T; type Error = UtxoCoinBuildError; @@ -103,7 +103,7 @@ where impl<'a, F, T> MergeUtxoArcOps for UtxoArcBuilder<'a, F, T> where F: Fn(UtxoArc) -> T + Send + Sync + 'static, - T: UtxoCommonOps, + T: UtxoCommonOps + GetUtxoListOps, { } @@ -114,7 +114,7 @@ where { } -pub trait MergeUtxoArcOps: UtxoCoinBuilderCommonOps { +pub trait MergeUtxoArcOps: UtxoCoinBuilderCommonOps { fn spawn_merge_utxo_loop_if_required(&self, weak: UtxoWeak, constructor: F) where F: Fn(UtxoArc) -> T + Send + Sync + 'static, diff --git a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs index a0fa074b15..46ae1989f0 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs @@ -2,6 +2,7 @@ use crate::hd_wallet::{HDAccountsMap, HDAccountsMutex}; use crate::hd_wallet_storage::{HDWalletCoinStorage, HDWalletStorageError}; 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, InitBlockHeaderStorageOps}; use crate::utxo::utxo_builder::utxo_conf_builder::{UtxoConfBuilder, UtxoConfError, UtxoConfResult}; use crate::utxo::{output_script, utxo_common, ElectrumBuilderArgs, ElectrumProtoVerifier, RecentlySpentOutPoints, @@ -158,9 +159,9 @@ pub trait UtxoFieldsWithIguanaPrivKeyBuilder: UtxoCoinBuilderCommonOps { let dust_amount = self.dust_amount(); let initial_history_state = self.initial_history_state(); - let tx_cache_directory = Some(self.ctx().dbdir().join("TX_CACHE")); let tx_hash_algo = self.tx_hash_algo(); let check_utxo_maturity = self.check_utxo_maturity(); + let tx_cache = self.tx_cache(); let block_headers_storage = self.block_headers_storage()?; let coin = UtxoCoinFields { @@ -171,7 +172,7 @@ pub trait UtxoFieldsWithIguanaPrivKeyBuilder: UtxoCoinBuilderCommonOps { priv_key_policy, derivation_method, history_sync_state: Mutex::new(initial_history_state), - tx_cache_directory, + tx_cache, block_headers_storage, recently_spent_outpoints: AsyncMutex::new(RecentlySpentOutPoints::new(my_script_pubkey)), tx_fee, @@ -221,9 +222,9 @@ pub trait UtxoFieldsWithHardwareWalletBuilder: UtxoCoinBuilderCommonOps { let dust_amount = self.dust_amount(); let initial_history_state = self.initial_history_state(); - let tx_cache_directory = Some(self.ctx().dbdir().join("TX_CACHE")); let tx_hash_algo = self.tx_hash_algo(); let check_utxo_maturity = self.check_utxo_maturity(); + let tx_cache = self.tx_cache(); let block_headers_storage = self.block_headers_storage()?; let coin = UtxoCoinFields { @@ -235,7 +236,7 @@ pub trait UtxoFieldsWithHardwareWalletBuilder: UtxoCoinBuilderCommonOps { derivation_method: DerivationMethod::HDWallet(hd_wallet), history_sync_state: Mutex::new(initial_history_state), block_headers_storage, - tx_cache_directory, + tx_cache, recently_spent_outpoints, tx_fee, tx_hash_algo, @@ -254,6 +255,7 @@ pub trait UtxoFieldsWithHardwareWalletBuilder: UtxoCoinBuilderCommonOps { .mm_err(UtxoCoinBuildError::from) } + #[inline] fn derivation_path(&self) -> UtxoConfResult { if self.conf()["derivation_path"].is_null() { return MmError::err(UtxoConfError::DerivationPathIsNotSet); @@ -262,10 +264,13 @@ pub trait UtxoFieldsWithHardwareWalletBuilder: UtxoCoinBuilderCommonOps { .map_to_mm(|e| UtxoConfError::ErrorDeserializingDerivationPath(e.to_string())) } + #[inline] fn gap_limit(&self) -> u32 { self.activation_params().gap_limit.unwrap_or(DEFAULT_GAP_LIMIT) } + #[inline] fn supports_trezor(&self, conf: &UtxoCoinConf) -> bool { conf.trezor_coin.is_some() } + #[inline] fn check_if_trezor_is_initialized(&self) -> UtxoCoinBuildResult<()> { let crypto_ctx = CryptoCtx::from_ctx(self.ctx())?; let hw_ctx = crypto_ctx @@ -287,6 +292,7 @@ pub trait UtxoCoinBuilderCommonOps { fn ticker(&self) -> &str; + #[inline] fn block_headers_storage(&self) -> UtxoCoinBuildResult> { let params: Option<_> = json::from_value(self.conf()["block_header_params"].clone()) .map_to_mm(|e| UtxoConfError::InvalidBlockHeaderParams(e.to_string()))?; @@ -336,6 +342,7 @@ pub trait UtxoCoinBuilderCommonOps { Ok(address_format) } + #[inline] fn pub_addr_prefix(&self) -> u8 { let pubtype = self.conf()["pubtype"] .as_u64() @@ -343,14 +350,17 @@ pub trait UtxoCoinBuilderCommonOps { pubtype as u8 } + #[inline] fn p2sh_address_prefix(&self) -> u8 { self.conf()["p2shtype"] .as_u64() .unwrap_or(if self.ticker() == "BTC" { 5 } else { 85 }) as u8 } + #[inline] fn dust_amount(&self) -> u64 { json::from_value(self.conf()["dust"].clone()).unwrap_or(UTXO_DUST_AMOUNT) } + #[inline] fn network(&self) -> UtxoCoinBuildResult { let conf = self.conf(); if !conf["network"].is_null() { @@ -360,6 +370,7 @@ pub trait UtxoCoinBuilderCommonOps { Ok(BlockchainNetwork::Mainnet) } + #[inline] async fn decimals(&self, _rpc_client: &UtxoRpcClientEnum) -> UtxoCoinBuildResult { Ok(self.conf()["decimals"].as_u64().unwrap_or(8) as u8) } @@ -383,6 +394,7 @@ pub trait UtxoCoinBuilderCommonOps { Ok(tx_fee) } + #[inline] fn initial_history_state(&self) -> HistorySyncState { if self.activation_params().tx_history { HistorySyncState::NotStarted @@ -549,6 +561,7 @@ pub trait UtxoCoinBuilderCommonOps { } } + #[inline] fn tx_hash_algo(&self) -> TxHashAlgo { if self.ticker() == "GRS" { TxHashAlgo::SHA256 @@ -557,9 +570,28 @@ pub trait UtxoCoinBuilderCommonOps { } } + #[inline] fn check_utxo_maturity(&self) -> bool { self.activation_params().check_utxo_maturity.unwrap_or_default() } + #[inline] fn is_hw_coin(&self, conf: &UtxoCoinConf) -> bool { conf.trezor_coin.is_some() } + + #[inline] + #[cfg(target_arch = "wasm32")] + fn tx_cache(&self) -> UtxoVerboseCacheShared { + crate::utxo::tx_cache::wasm_tx_cache::WasmVerboseCache::default().into_shared() + } + + #[inline] + #[cfg(not(target_arch = "wasm32"))] + fn tx_cache(&self) -> UtxoVerboseCacheShared { + crate::utxo::tx_cache::fs_tx_cache::FsVerboseCache::new(self.ticker().to_owned(), self.tx_cache_path()) + .into_shared() + } + + #[inline] + #[cfg(not(target_arch = "wasm32"))] + fn tx_cache_path(&self) -> PathBuf { self.ctx().dbdir().join("TX_CACHE") } } /// Attempts to parse native daemon conf file and return rpcport, rpcuser and rpcpassword diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index c95f7617f1..9bb7ef7554 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -5,15 +5,16 @@ use crate::hd_pubkey::{ExtractExtendedPubkey, HDExtractPubkeyError, HDXPubExtrac use crate::hd_wallet::{AccountUpdatingError, AddressDerivingError, HDAccountMut, HDAccountsMap, NewAccountCreatingError}; use crate::hd_wallet_storage::{HDWalletCoinWithStorageOps, HDWalletStorageResult}; -use crate::init_withdraw::WithdrawTaskHandle; -use crate::utxo::rpc_clients::{electrum_script_hash, BlockHashOrHeight, UnspentInfo, UtxoRpcClientEnum, +use crate::rpc_command::init_withdraw::WithdrawTaskHandle; +use crate::utxo::rpc_clients::{electrum_script_hash, BlockHashOrHeight, UnspentInfo, UnspentMap, UtxoRpcClientEnum, UtxoRpcClientOps, UtxoRpcResult}; +use crate::utxo::tx_cache::TxCacheResult; use crate::utxo::utxo_withdraw::{InitUtxoWithdraw, StandardUtxoWithdraw, UtxoWithdraw}; use crate::{CanRefundHtlc, CoinBalance, CoinWithDerivationMethod, GetWithdrawSenderAddress, HDAddressId, RawTransactionError, RawTransactionRequest, RawTransactionRes, SignatureError, SignatureResult, TradePreimageValue, TransactionFut, TxFeeDetails, ValidateAddressResult, ValidatePaymentInput, VerificationError, VerificationResult, WithdrawFrom, WithdrawResult, WithdrawSenderAddress}; -use bigdecimal::{BigDecimal, Zero}; +use bigdecimal::BigDecimal; use bitcrypto::dhash256; pub use bitcrypto::{dhash160, sha256, ChecksumType}; use chain::constants::SEQUENCE_FINAL; @@ -31,6 +32,7 @@ use crypto::{Bip32DerPathOps, Bip44Chain, Bip44DerPathError, Bip44DerivationPath use futures::compat::Future01CompatExt; use futures::future::{FutureExt, TryFutureExt}; use futures01::future::Either; +use itertools::Itertools; use keys::bytes::Bytes; use keys::{Address, AddressFormat as UtxoAddressFormat, AddressHashEnum, CompactSignature, Public, SegwitAddress, Type as ScriptType}; @@ -341,7 +343,7 @@ pub async fn all_known_addresses_balances( ) -> BalanceResult> where T: HDWalletBalanceOps + Sync, - T::Address: std::fmt::Display, + T::Address: std::fmt::Display + Clone, { let external_addresses = hd_account .known_addresses_number(Bip44Chain::External) @@ -386,10 +388,16 @@ pub async fn load_hd_accounts_from_storage( } } +/// Requests balance of the given `address`. pub async fn address_balance(coin: &T, address: &Address) -> BalanceResult where - T: UtxoCommonOps + MarketCoinOps, + T: UtxoCommonOps + GetUtxoListOps + MarketCoinOps, { + if coin.as_ref().check_utxo_maturity { + let (unspents, _) = coin.get_mature_unspent_ordered_list(address).await?; + return Ok(unspents.to_coin_balance(coin.as_ref().decimals)); + } + let balance = coin .as_ref() .rpc_client @@ -397,16 +405,46 @@ where .compat() .await?; - if !coin.as_ref().check_utxo_maturity { - return Ok(CoinBalance { - spendable: balance, - unspendable: BigDecimal::from(0), - }); - } + Ok(CoinBalance { + spendable: balance, + unspendable: BigDecimal::from(0), + }) +} - let unspendable = address_unspendable_balance(coin, address, &balance).await?; - let spendable = &balance - &unspendable; - Ok(CoinBalance { spendable, unspendable }) +/// Requests balances of the given `addresses`. +/// The pairs `(Address, CoinBalance)` are guaranteed to be in the same order in which they were requested. +pub async fn addresses_balances(coin: &T, addresses: Vec
) -> BalanceResult> +where + T: UtxoCommonOps + GetUtxoMapOps + MarketCoinOps, +{ + if coin.as_ref().check_utxo_maturity { + let (unspents_map, _) = coin.get_mature_unspent_ordered_map(addresses.clone()).await?; + addresses + .into_iter() + .map(|address| { + let unspents = unspents_map.get(&address).or_mm_err(|| { + let error = format!("'get_mature_unspent_ordered_map' should have returned '{}'", address); + BalanceError::Internal(error) + })?; + let balance = unspents.to_coin_balance(coin.as_ref().decimals); + Ok((address, balance)) + }) + .collect() + } else { + Ok(coin + .as_ref() + .rpc_client + .display_balances(addresses.clone(), coin.as_ref().decimals) + .compat() + .await? + .into_iter() + .map(|(address, spendable)| { + let unspendable = BigDecimal::from(0); + let balance = CoinBalance { spendable, unspendable }; + (address, balance) + }) + .collect()) + } } pub fn derivation_method(coin: &UtxoCoinFields) -> &DerivationMethod { &coin.derivation_method } @@ -549,7 +587,10 @@ pub async fn get_current_mtp(coin: &UtxoCoinFields, coin_variant: CoinVariant) - .await } -pub fn send_outputs_from_my_address(coin: T, outputs: Vec) -> TransactionFut { +pub fn send_outputs_from_my_address(coin: T, outputs: Vec) -> TransactionFut +where + T: UtxoCommonOps + GetUtxoListOps, +{ let fut = send_outputs_from_my_address_impl(coin, outputs); Box::new(fut.boxed().compat().map(|tx| tx.into())) } @@ -791,7 +832,7 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { for utxo in self.available_inputs.clone() { self.tx.inputs.push(UnsignedTransactionInput { - previous_output: utxo.outpoint.clone(), + previous_output: utxo.outpoint, sequence: SEQUENCE_FINAL, amount: utxo.value, witness: vec![], @@ -995,7 +1036,7 @@ pub async fn p2sh_spending_tx( pub fn send_taker_fee(coin: T, fee_pub_key: &[u8], amount: BigDecimal) -> TransactionFut where - T: AsRef + UtxoCommonOps + Send + Sync + 'static, + T: UtxoCommonOps + GetUtxoListOps, { let address = try_tx_fus!(address_from_raw_pubkey( fee_pub_key, @@ -1013,14 +1054,17 @@ where send_outputs_from_my_address(coin, vec![output]) } -pub fn send_maker_payment( +pub fn send_maker_payment( coin: T, time_lock: u32, maker_pub: &[u8], taker_pub: &[u8], secret_hash: &[u8], amount: BigDecimal, -) -> TransactionFut { +) -> TransactionFut +where + T: UtxoCommonOps + GetUtxoListOps, +{ let SwapPaymentOutputsResult { payment_address, outputs, @@ -1047,14 +1091,17 @@ pub fn send_maker_payment( Box::new(send_fut) } -pub fn send_taker_payment( +pub fn send_taker_payment( coin: T, time_lock: u32, taker_pub: &[u8], maker_pub: &[u8], secret_hash: &[u8], amount: BigDecimal, -) -> TransactionFut { +) -> TransactionFut +where + T: UtxoCommonOps + GetUtxoListOps, +{ let SwapPaymentOutputsResult { payment_address, outputs, @@ -1680,7 +1727,7 @@ pub fn verify_message( pub fn my_balance(coin: T) -> BalanceFut where - T: UtxoCommonOps + MarketCoinOps, + T: UtxoCommonOps + GetUtxoListOps + MarketCoinOps, { let my_address = try_f!(coin .as_ref() @@ -1828,7 +1875,7 @@ pub async fn get_raw_transaction(coin: &UtxoCoinFields, req: RawTransactionReque pub async fn withdraw(coin: T, req: WithdrawRequest) -> WithdrawResult where - T: UtxoCommonOps + MarketCoinOps, + T: UtxoCommonOps + GetUtxoListOps + MarketCoinOps, { StandardUtxoWithdraw::new(coin, req)?.build().await } @@ -1841,6 +1888,7 @@ pub async fn init_withdraw( ) -> WithdrawResult where T: UtxoCommonOps + + GetUtxoListOps + UtxoSignerOps + CoinWithDerivationMethod + GetWithdrawSenderAddress
, @@ -2267,7 +2315,9 @@ where let electrum_history = match client.scripthash_get_history(&hex::encode(script_hash)).compat().await { Ok(value) => value, Err(e) => match &e.error { - JsonRpcErrorType::Transport(e) | JsonRpcErrorType::Parse(_, e) => { + JsonRpcErrorType::InvalidRequest(e) + | JsonRpcErrorType::Transport(e) + | JsonRpcErrorType::Parse(_, e) => { return RequestTxHistoryResult::Retry { error: ERRL!("Error {} on scripthash_get_history", e), }; @@ -2585,13 +2635,16 @@ pub fn get_trade_fee(coin: T) -> Box get_sender_trade_fee(TradePreimageValue::Exact(10000))`. /// So we should always return a fee as if a transaction includes the change output. -pub async fn preimage_trade_fee_required_to_send_outputs( +pub async fn preimage_trade_fee_required_to_send_outputs( coin: &T, outputs: Vec, fee_policy: FeePolicy, gas_fee: Option, stage: &FeeApproxStage, -) -> TradePreimageResult { +) -> TradePreimageResult +where + T: UtxoCommonOps + GetUtxoListOps, +{ let ticker = coin.as_ref().conf.ticker.clone(); let decimals = coin.as_ref().decimals; let tx_fee = coin.get_tx_fee().await?; @@ -2606,7 +2659,7 @@ pub async fn preimage_trade_fee_required_to_send_outputs( let dynamic_fee = coin.increase_dynamic_fee_by_stage(fee, stage); let outputs_count = outputs.len(); - let (unspents, _recently_sent_txs) = coin.list_unspent_ordered(my_address).await?; + let (unspents, _recently_sent_txs) = coin.get_unspent_ordered_list(my_address).await?; let actual_tx_fee = ActualTxFee::Dynamic(dynamic_fee); @@ -2635,7 +2688,7 @@ pub async fn preimage_trade_fee_required_to_send_outputs( }, ActualTxFee::FixedPerKb(fee) => { let outputs_count = outputs.len(); - let (unspents, _recently_sent_txs) = coin.list_unspent_ordered(my_address).await?; + let (unspents, _recently_sent_txs) = coin.get_unspent_ordered_list(my_address).await?; let mut tx_builder = UtxoTxBuilder::new(coin) .add_available_inputs(unspents) @@ -2777,15 +2830,58 @@ pub fn is_coin_protocol_supported(coin: &T, info: &Option( +/// [`GetUtxoListOps::get_mature_unspent_ordered_list`] implementation. +/// Returns available mature and immature unspents in ascending order +/// + `RecentlySpentOutPoints` MutexGuard for further interaction (e.g. to add new transaction to it). +pub async fn get_mature_unspent_ordered_list<'a, T>( coin: &'a T, address: &Address, -) -> UtxoRpcResult<(Vec, AsyncMutexGuard<'a, RecentlySpentOutPoints>)> { +) -> UtxoRpcResult<(MatureUnspentList, RecentlySpentOutPointsGuard<'a>)> +where + T: UtxoCommonOps + GetUtxoListOps, +{ + let (unspents, recently_spent) = coin.get_all_unspent_ordered_list(address).await?; + let mature_unspents = identify_mature_unspents(coin, unspents).await?; + Ok((mature_unspents, recently_spent)) +} + +/// [`GetUtxoMapOps::get_mature_unspent_ordered_map`] implementation. +/// Returns available mature and immature unspents in ascending order for every given `addresses` +/// + `RecentlySpentOutPoints` MutexGuard for further interaction (e.g. to add new transaction to it). +pub async fn get_mature_unspent_ordered_map( + coin: &T, + addresses: Vec
, +) -> UtxoRpcResult<(MatureUnspentMap, RecentlySpentOutPointsGuard<'_>)> +where + T: UtxoCommonOps + GetUtxoMapOps, +{ + let (unspents_map, recently_spent) = coin.get_all_unspent_ordered_map(addresses).await?; + // Get an iterator of futures: `Future>` + let fut_it = unspents_map.into_iter().map(|(address, unspents)| { + identify_mature_unspents(coin, unspents).map(|res| -> UtxoRpcResult<(Address, MatureUnspentList)> { + let mature_unspents = res?; + Ok((address, mature_unspents)) + }) + }); + // Poll the `fut_it` futures concurrently. + let result_map = futures::future::try_join_all(fut_it).await?.into_iter().collect(); + Ok((result_map, recently_spent)) +} + +/// Splits the given `unspents` outputs into mature and immature. +pub async fn identify_mature_unspents(coin: &T, unspents: Vec) -> UtxoRpcResult +where + T: UtxoCommonOps, +{ + /// Returns `true` if the given transaction has a known non-zero height. + fn can_tx_be_cached(tx: &RpcTransaction) -> bool { tx.height > Some(0) } + + /// Calculates actual confirmations number of the given `tx` transaction loaded from cache. fn calc_actual_cached_tx_confirmations(tx: &RpcTransaction, block_count: u64) -> UtxoRpcResult { let tx_height = tx.height.or_mm_err(|| { UtxoRpcError::Internal(format!(r#"Warning, height of cached "{:?}" tx is unknown"#, tx.txid)) })?; - // utxo_common::cache_transaction_if_possible() shouldn't cache transaction with height == 0 + // There shouldn't be cached transactions with height == 0 if tx_height == 0 { let error = format!( r#"Warning, height of cached "{:?}" tx is expected to be non-zero"#, @@ -2805,20 +2901,31 @@ pub async fn list_mature_unspent_ordered<'a, T: UtxoCommonOps>( Ok(confirmations as u32) } - let (unspents, recently_spent) = coin.list_all_unspent_ordered(address).await?; let block_count = coin.as_ref().rpc_client.get_block_count().compat().await?; - let mut result = Vec::with_capacity(unspents.len()); + let to_verbose: HashSet = unspents + .iter() + .map(|unspent| unspent.outpoint.hash.reversed().into()) + .collect(); + let verbose_txs = coin + .get_verbose_transactions_from_cache_or_rpc(to_verbose) + .compat() + .await?; + // Transactions that should be cached. + let mut txs_to_cache = HashMap::with_capacity(verbose_txs.len()); + + let mut result = MatureUnspentList::with_capacity(unspents.len()); for unspent in unspents { let tx_hash: H256Json = unspent.outpoint.hash.reversed().into(); - let tx_info = match coin.get_verbose_transaction_from_cache_or_rpc(tx_hash).compat().await { - Ok(x) => x, - Err(err) => { - log!("Error " [err] " getting the transaction " [tx_hash] ", skip the unspent output"); - continue; - }, - }; - + let tx_info = verbose_txs + .get(&tx_hash) + .or_mm_err(|| { + UtxoRpcError::Internal(format!( + "'get_verbose_transactions_from_cache_or_rpc' should have returned '{:?}'", + tx_hash + )) + })? + .clone(); let tx_info = match tx_info { VerboseTransactionFrom::Cache(mut tx) => { if unspent.height.is_some() { @@ -2828,7 +2935,7 @@ pub async fn list_mature_unspent_ordered<'a, T: UtxoCommonOps>( Ok(conf) => tx.confirmations = conf, // do not skip the transaction with unknown confirmations, // because the transaction can be matured - Err(e) => log!((e)), + Err(e) => error!("{}", e), } tx }, @@ -2836,19 +2943,25 @@ pub async fn list_mature_unspent_ordered<'a, T: UtxoCommonOps>( if tx.height.is_none() { tx.height = unspent.height; } - if let Err(e) = coin.cache_transaction_if_possible(&tx).await { - log!((e)); + if can_tx_be_cached(&tx) { + txs_to_cache.insert(tx_hash, tx.clone()); } tx }, }; if coin.is_unspent_mature(&tx_info) { - result.push(unspent); + result.mature.push(unspent); + } else { + result.immature.push(unspent); } } - Ok((result, recently_spent)) + coin.as_ref() + .tx_cache + .cache_transactions_concurrently(&txs_to_cache) + .await; + Ok(result) } pub fn is_unspent_mature(mature_confirmations: u32, output: &RpcTransaction) -> bool { @@ -2856,101 +2969,57 @@ pub fn is_unspent_mature(mature_confirmations: u32, output: &RpcTransaction) -> !output.is_coinbase() || output.confirmations >= mature_confirmations } -#[cfg(not(target_arch = "wasm32"))] -pub async fn get_verbose_transaction_from_cache_or_rpc( +/// [`UtxoCommonOps::get_verbose_transactions_from_cache_or_rpc`] implementation. +/// Loads verbose transactions from cache or requests it using RPC client. +pub async fn get_verbose_transactions_from_cache_or_rpc( coin: &UtxoCoinFields, - txid: H256Json, -) -> UtxoRpcResult { - let tx_cache_path = match &coin.tx_cache_directory { - Some(p) => p.clone(), - _ => { - // the coin doesn't support TX local cache, don't try to load from cache and don't cache it - let tx = coin.rpc_client.get_verbose_transaction(&txid).compat().await?; - return Ok(VerboseTransactionFrom::Rpc(tx)); - }, - }; - - match tx_cache::load_transaction_from_cache(&tx_cache_path, &txid).await { - Ok(Some(tx)) => return Ok(VerboseTransactionFrom::Cache(tx)), - Err(err) => log!("Error " [err] " loading the " [txid] " transaction. Try request tx using Rpc client"), - // txid just not found - _ => (), + tx_ids: HashSet, +) -> UtxoRpcResult> { + /// Determines whether the transaction is needed to be requested through RPC or not. + /// Puts the inner `RpcTransaction` transaction into `result_map` if it has been loaded successfully, + /// otherwise puts `txid` into `to_request`. + fn on_cached_transaction_result( + result_map: &mut HashMap, + to_request: &mut Vec, + txid: H256Json, + res: TxCacheResult>, + ) { + match res { + Ok(Some(tx)) => { + result_map.insert(txid, VerboseTransactionFrom::Cache(tx)); + }, + // txid not found + Ok(None) => { + to_request.push(txid); + }, + Err(err) => { + error!( + "Error loading the {:?} transaction: {:?}. Trying to request tx using RPC client", + err, txid + ); + to_request.push(txid); + }, + } } - let tx = coin.rpc_client.get_verbose_transaction(&txid).compat().await?; - Ok(VerboseTransactionFrom::Rpc(tx)) -} - -#[cfg(target_arch = "wasm32")] -pub async fn get_verbose_transaction_from_cache_or_rpc( - coin: &UtxoCoinFields, - txid: H256Json, -) -> UtxoRpcResult { - let tx = coin.rpc_client.get_verbose_transaction(&txid).compat().await?; - Ok(VerboseTransactionFrom::Rpc(tx)) -} - -#[cfg(not(target_arch = "wasm32"))] -pub async fn cache_transaction_if_possible(coin: &UtxoCoinFields, tx: &RpcTransaction) -> Result<(), String> { - let tx_cache_path = match &coin.tx_cache_directory { - Some(p) => p.clone(), - _ => { - return Ok(()); - }, - }; - // check if the transaction height is set and not zero - match tx.height { - Some(0) => return Ok(()), - Some(_) => (), - None => return Ok(()), - } + let mut result_map = HashMap::with_capacity(tx_ids.len()); + let mut to_request = Vec::with_capacity(tx_ids.len()); - tx_cache::cache_transaction(&tx_cache_path, tx) + coin.tx_cache + .load_transactions_from_cache_concurrently(tx_ids) .await - .map_err(|e| ERRL!("Error {:?} on caching transaction {:?}", e, tx.txid)) -} - -#[cfg(target_arch = "wasm32")] -pub async fn cache_transaction_if_possible(_coin: &UtxoCoinFields, _tx: &RpcTransaction) -> Result<(), String> { - Ok(()) -} - -pub async fn address_unspendable_balance( - coin: &T, - address: &Address, - total_balance: &BigDecimal, -) -> BalanceResult -where - T: UtxoCommonOps + MarketCoinOps, -{ - let mut attempts = 0i32; - loop { - let (mature_unspents, _) = coin.list_mature_unspent_ordered(address).await?; - let spendable_balance = mature_unspents.iter().fold(BigDecimal::zero(), |acc, x| { - acc + big_decimal_from_sat(x.value as i64, coin.as_ref().decimals) - }); - if total_balance >= &spendable_balance { - return Ok(total_balance - spendable_balance); - } - - if attempts == 2 { - let error = format!( - "Spendable balance {} greater than total balance {}", - spendable_balance, total_balance - ); - return MmError::err(BalanceError::Internal(error)); - } - - warn!( - "Attempt N{}: spendable balance {} greater than total balance {}", - attempts, spendable_balance, total_balance - ); + .into_iter() + .for_each(|(txid, res)| on_cached_transaction_result(&mut result_map, &mut to_request, txid, res)); - // the balance could be changed by other instance between my_balance() and list_mature_unspent_ordered() calls - // try again - attempts += 1; - Timer::sleep(0.3).await; - } + result_map.extend( + coin.rpc_client + .get_verbose_transactions(&to_request) + .compat() + .await? + .into_iter() + .map(|tx| (tx.txid, VerboseTransactionFrom::Rpc(tx))), + ); + Ok(result_map) } /// Swap contract address is not used by standard UTXO coins. @@ -3367,47 +3436,92 @@ pub fn dex_fee_script(uuid: [u8; 16], time_lock: u32, watcher_pub: &Public, send .into_script() } -pub async fn list_unspent_ordered<'a, T: UtxoCommonOps>( +/// [`GetUtxoListOps::get_unspent_ordered_list`] implementation. +/// Returns available unspents in ascending order +/// + `RecentlySpentOutPoints` MutexGuard for further interaction (e.g. to add new transaction to it). +pub async fn get_unspent_ordered_list<'a, T>( coin: &'a T, address: &Address, -) -> UtxoRpcResult<(Vec, AsyncMutexGuard<'a, RecentlySpentOutPoints>)> { +) -> UtxoRpcResult<(Vec, RecentlySpentOutPointsGuard<'a>)> +where + T: UtxoCommonOps + GetUtxoListOps, +{ if coin.as_ref().check_utxo_maturity { - coin.list_mature_unspent_ordered(address).await + coin.get_mature_unspent_ordered_list(address) + .await + // Convert `MatureUnspentList` into `Vec` by discarding immature unspents. + .map(|(mature_unspents, recently_spent)| (mature_unspents.only_mature(), recently_spent)) } else { - coin.list_all_unspent_ordered(address).await + coin.get_all_unspent_ordered_list(address).await } } -pub async fn list_all_unspent_ordered<'a, T>( - coin: &'a T, - address: &Address, -) -> UtxoRpcResult<(Vec, AsyncMutexGuard<'a, RecentlySpentOutPoints>)> +/// [`GetUtxoMapOps::get_unspent_ordered_map`] implementation. +/// Returns available unspents in ascending order + `RecentlySpentOutPoints` MutexGuard for further interaction +/// (e.g. to add new transaction to it). +pub async fn get_unspent_ordered_map( + coin: &T, + addresses: Vec
, +) -> UtxoRpcResult<(UnspentMap, RecentlySpentOutPointsGuard<'_>)> where - T: AsRef, + T: UtxoCommonOps + GetUtxoMapOps, { + if coin.as_ref().check_utxo_maturity { + coin.get_mature_unspent_ordered_map(addresses) + .await + // Convert `MatureUnspentMap` into `UnspentMap` by discarding immature unspents. + .map(|(mature_unspents_map, recently_spent)| { + let unspents_map = mature_unspents_map + .into_iter() + .map(|(address, unspents)| (address, unspents.only_mature())) + .collect(); + (unspents_map, recently_spent) + }) + } else { + coin.get_all_unspent_ordered_map(addresses).await + } +} + +/// [`GetUtxoListOps::get_all_unspent_ordered_list`] implementation. +/// Returns available mature and immature unspents in ascending +/// + `RecentlySpentOutPoints` MutexGuard for further interaction (e.g. to add new transaction to it). +pub async fn get_all_unspent_ordered_list<'a, T: UtxoCommonOps>( + coin: &'a T, + address: &Address, +) -> UtxoRpcResult<(Vec, RecentlySpentOutPointsGuard<'a>)> { let decimals = coin.as_ref().decimals; - let mut unspents = coin + let unspents = coin .as_ref() .rpc_client .list_unspent(address, decimals) .compat() .await?; let recently_spent = coin.as_ref().recently_spent_outpoints.lock().await; - unspents = recently_spent - .replace_spent_outputs_with_cache(unspents.into_iter().collect()) - .into_iter() - .collect(); - unspents.sort_unstable_by(|a, b| { - if a.value < b.value { - Ordering::Less - } else { - Ordering::Greater - } - }); - // dedup just in case we add duplicates of same unspent out - // all duplicates will be removed because vector in sorted before dedup - unspents.dedup_by(|one, another| one.outpoint == another.outpoint); - Ok((unspents, recently_spent)) + let unordered_unspents = recently_spent.replace_spent_outputs_with_cache(unspents.into_iter().collect()); + let ordered_unspents = sort_dedup_unspents(unordered_unspents); + Ok((ordered_unspents, recently_spent)) +} + +/// [`GetUtxoMapOps::get_all_unspent_ordered_map`] implementation. +/// Returns available mature and immature unspents in ascending order for every given `addresses` +/// + `RecentlySpentOutPoints` MutexGuard for further interaction (e.g. to add new transaction to it). +pub async fn get_all_unspent_ordered_map( + coin: &T, + addresses: Vec
, +) -> UtxoRpcResult<(UnspentMap, RecentlySpentOutPointsGuard<'_>)> { + let decimals = coin.as_ref().decimals; + let mut unspents_map = coin + .as_ref() + .rpc_client + .list_unspent_group(addresses, decimals) + .compat() + .await?; + let recently_spent = coin.as_ref().recently_spent_outpoints.lock().await; + for (_address, unspents) in unspents_map.iter_mut() { + let unordered_unspents = recently_spent.replace_spent_outputs_with_cache(unspents.iter().cloned().collect()); + *unspents = sort_dedup_unspents(unordered_unspents); + } + Ok((unspents_map, recently_spent)) } /// Increase the given `dynamic_fee` according to the fee approximation `stage` using the [`UtxoCoinFields::tx_fee_volatility_percent`]. @@ -3560,13 +3674,15 @@ pub async fn block_header_utxo_loop(weak: UtxoWeak, constructo } } -pub async fn merge_utxo_loop( +pub async fn merge_utxo_loop( weak: UtxoWeak, merge_at: usize, check_every: f64, max_merge_at_once: usize, constructor: impl Fn(UtxoArc) -> T, -) { +) where + T: UtxoCommonOps + GetUtxoListOps, +{ loop { Timer::sleep(check_every).await; @@ -3584,10 +3700,10 @@ pub async fn merge_utxo_loop( }; let ticker = &coin.as_ref().conf.ticker; - let (unspents, recently_spent) = match coin.list_unspent_ordered(my_address).await { + let (unspents, recently_spent) = match coin.get_unspent_ordered_list(my_address).await { Ok((unspents, recently_spent)) => (unspents, recently_spent), Err(e) => { - error!("Error {} on list_unspent_ordered of coin {}", e, ticker); + error!("Error {} on get_unspent_ordered_list of coin {}", e, ticker); continue; }, }; @@ -3733,6 +3849,25 @@ where } } +/// Sorts and deduplicates the given `unspents` in ascending order. +fn sort_dedup_unspents(unspents: I) -> Vec +where + I: IntoIterator, +{ + unspents + .into_iter() + // dedup just in case we add duplicates of same unspent out + .unique_by(|unspent| unspent.outpoint) + .sorted_unstable_by(|a, b| { + if a.value < b.value { + Ordering::Less + } else { + Ordering::Greater + } + }) + .collect() +} + #[test] fn test_increase_by_percent() { assert_eq!(increase_by_percent(4300, 1.), 4343); diff --git a/mm2src/coins/utxo/utxo_common_tests.rs b/mm2src/coins/utxo/utxo_common_tests.rs new file mode 100644 index 0000000000..db334a705d --- /dev/null +++ b/mm2src/coins/utxo/utxo_common_tests.rs @@ -0,0 +1,40 @@ +use super::*; +use crate::utxo::rpc_clients::{ElectrumClient, UtxoRpcClientOps}; +use common::jsonrpc_client::JsonRpcErrorType; + +pub async fn test_electrum_display_balances(rpc_client: &ElectrumClient) { + let addresses = vec![ + "RG278CfeNPFtNztFZQir8cgdWexVhViYVy".into(), + "RYPz6Lr4muj4gcFzpMdv3ks1NCGn3mkDPN".into(), + "RJeDDtDRtKUoL8BCKdH7TNCHqUKr7kQRsi".into(), + "RQHn9VPHBqNjYwyKfJbZCiaxVrWPKGQjeF".into(), + ]; + let actual = rpc_client.display_balances(addresses, 8).compat().await.unwrap(); + + let expected: Vec<(Address, BigDecimal)> = vec![ + ("RG278CfeNPFtNztFZQir8cgdWexVhViYVy".into(), BigDecimal::from(5.77699)), + ("RYPz6Lr4muj4gcFzpMdv3ks1NCGn3mkDPN".into(), BigDecimal::from(0)), + ("RJeDDtDRtKUoL8BCKdH7TNCHqUKr7kQRsi".into(), BigDecimal::from(0.77699)), + ("RQHn9VPHBqNjYwyKfJbZCiaxVrWPKGQjeF".into(), BigDecimal::from(16.55398)), + ]; + assert_eq!(actual, expected); + + let invalid_hashes = vec![ + "0128a4ea8c5775039d39a192f8490b35b416f2f194cb6b6ee91a41d01233c3b5".to_owned(), + "!INVALID!".to_owned(), + "457206aa039ed77b223e4623c19152f9aa63aa7845fe93633920607500766931".to_owned(), + ]; + + let rpc_err = rpc_client + .scripthash_get_balances(invalid_hashes) + .compat() + .await + .unwrap_err(); + match rpc_err.error { + JsonRpcErrorType::Response(_, json_err) => { + let expected = json!({"code": 1, "message": "!INVALID! is not a valid script hash"}); + assert_eq!(json_err, expected); + }, + ekind => panic!("Unexpected `JsonRpcErrorType`: {:?}", ekind), + } +} diff --git a/mm2src/coins/utxo/utxo_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index 6ae8654dd4..120a08544d 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -1,15 +1,17 @@ use super::*; -use crate::coin_balance::{self, AccountBalanceParams, CheckHDAccountBalanceParams, CheckHDAccountBalanceResponse, - EnableCoinBalanceError, HDAccountBalance, HDAccountBalanceResponse, - HDAccountBalanceRpcError, HDAddressBalance, HDWalletBalance, HDWalletBalanceOps, - HDWalletBalanceRpcOps}; +use crate::coin_balance::{self, EnableCoinBalanceError, HDAccountBalance, HDAddressBalance, HDWalletBalance, + HDWalletBalanceOps}; use crate::hd_pubkey::{ExtractExtendedPubkey, HDExtractPubkeyError, HDXPubExtractor}; use crate::hd_wallet::{self, AccountUpdatingError, AddressDerivingError, GetNewHDAddressParams, GetNewHDAddressResponse, HDAccountMut, HDWalletRpcError, HDWalletRpcOps, NewAccountCreatingError}; use crate::hd_wallet_storage::HDWalletCoinWithStorageOps; -use crate::init_create_account::{self, CreateNewAccountParams, InitCreateHDAccountRpcOps}; -use crate::init_withdraw::{InitWithdrawCoin, WithdrawTaskHandle}; +use crate::rpc_command::account_balance::{self, AccountBalanceParams, AccountBalanceRpcOps, HDAccountBalanceResponse}; +use crate::rpc_command::hd_account_balance_rpc_error::HDAccountBalanceRpcError; +use crate::rpc_command::init_create_account::{self, CreateNewAccountParams, InitCreateHDAccountRpcOps}; +use crate::rpc_command::init_scan_for_new_addresses::{self, InitScanAddressesRpcOps, ScanAddressesParams, + ScanAddressesResponse}; +use crate::rpc_command::init_withdraw::{InitWithdrawCoin, WithdrawTaskHandle}; use crate::utxo::utxo_builder::{UtxoArcBuilder, UtxoCoinBuilder}; use crate::{CanRefundHtlc, CoinBalance, CoinWithDerivationMethod, GetWithdrawSenderAddress, NegotiateSwapContractAddrErr, PrivKeyBuildPolicy, SignatureResult, SwapOps, TradePreimageValue, @@ -88,6 +90,56 @@ impl UtxoTxGenerationOps for UtxoStandardCoin { } } +#[async_trait] +#[cfg_attr(test, mockable)] +impl GetUtxoListOps for UtxoStandardCoin { + async fn get_unspent_ordered_list( + &self, + address: &Address, + ) -> UtxoRpcResult<(Vec, RecentlySpentOutPointsGuard<'_>)> { + utxo_common::get_unspent_ordered_list(self, address).await + } + + async fn get_all_unspent_ordered_list( + &self, + address: &Address, + ) -> UtxoRpcResult<(Vec, RecentlySpentOutPointsGuard<'_>)> { + utxo_common::get_all_unspent_ordered_list(self, address).await + } + + async fn get_mature_unspent_ordered_list( + &self, + address: &Address, + ) -> UtxoRpcResult<(MatureUnspentList, RecentlySpentOutPointsGuard<'_>)> { + utxo_common::get_mature_unspent_ordered_list(self, address).await + } +} + +#[async_trait] +#[cfg_attr(test, mockable)] +impl GetUtxoMapOps for UtxoStandardCoin { + 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)] @@ -154,37 +206,15 @@ impl UtxoCommonOps for UtxoStandardCoin { .await } - async fn list_all_unspent_ordered<'a>( - &'a self, - address: &Address, - ) -> UtxoRpcResult<(Vec, AsyncMutexGuard<'a, RecentlySpentOutPoints>)> { - utxo_common::list_all_unspent_ordered(self, address).await - } - - async fn list_mature_unspent_ordered<'a>( - &'a self, - address: &Address, - ) -> UtxoRpcResult<(Vec, AsyncMutexGuard<'a, RecentlySpentOutPoints>)> { - utxo_common::list_mature_unspent_ordered(self, address).await - } - - fn get_verbose_transaction_from_cache_or_rpc(&self, txid: H256Json) -> UtxoRpcFut { + fn get_verbose_transactions_from_cache_or_rpc( + &self, + tx_ids: HashSet, + ) -> UtxoRpcFut> { let selfi = self.clone(); - let fut = async move { utxo_common::get_verbose_transaction_from_cache_or_rpc(&selfi.utxo_arc, txid).await }; + let fut = async move { utxo_common::get_verbose_transactions_from_cache_or_rpc(&selfi.utxo_arc, tx_ids).await }; Box::new(fut.boxed().compat()) } - async fn cache_transaction_if_possible(&self, tx: &RpcTransaction) -> Result<(), String> { - utxo_common::cache_transaction_if_possible(&self.utxo_arc, tx).await - } - - async fn list_unspent_ordered<'a>( - &'a self, - address: &Address, - ) -> UtxoRpcResult<(Vec, AsyncMutexGuard<'a, RecentlySpentOutPoints>)> { - utxo_common::list_unspent_ordered(self, address).await - } - async fn preimage_trade_fee_required_to_send_outputs( &self, outputs: Vec, @@ -758,6 +788,13 @@ impl HDWalletBalanceOps for UtxoStandardCoin { async fn known_address_balance(&self, address: &Self::Address) -> BalanceResult { utxo_common::address_balance(self, address).await } + + async fn known_addresses_balances( + &self, + addresses: Vec, + ) -> BalanceResult> { + utxo_common::addresses_balances(self, addresses).await + } } impl HDWalletCoinWithStorageOps for UtxoStandardCoin { @@ -767,19 +804,22 @@ impl HDWalletCoinWithStorageOps for UtxoStandardCoin { } #[async_trait] -impl HDWalletBalanceRpcOps for UtxoStandardCoin { +impl AccountBalanceRpcOps for UtxoStandardCoin { async fn account_balance_rpc( &self, params: AccountBalanceParams, ) -> MmResult { - coin_balance::common_impl::account_balance_rpc(self, params).await + account_balance::common_impl::account_balance_rpc(self, params).await } +} - async fn scan_for_new_addresses_rpc( +#[async_trait] +impl InitScanAddressesRpcOps for UtxoStandardCoin { + async fn init_scan_for_new_addresses_rpc( &self, - params: CheckHDAccountBalanceParams, - ) -> MmResult { - coin_balance::common_impl::scan_for_new_addresses_rpc(self, params).await + params: ScanAddressesParams, + ) -> MmResult { + init_scan_for_new_addresses::common_impl::scan_for_new_addresses_rpc(self, params).await } } diff --git a/mm2src/coins/utxo/utxo_tests.rs b/mm2src/coins/utxo/utxo_tests.rs index 3783d55d70..b99ae0190c 100644 --- a/mm2src/coins/utxo/utxo_tests.rs +++ b/mm2src/coins/utxo/utxo_tests.rs @@ -1,14 +1,20 @@ use super::*; -use crate::coin_balance::{AccountBalanceParams, CheckHDAccountBalanceParams, CheckHDAccountBalanceResponse, - HDAccountBalanceResponse, HDAddressBalance, HDWalletBalanceRpcOps}; +use crate::coin_balance::HDAddressBalance; use crate::hd_wallet::HDAccountsMap; use crate::hd_wallet_storage::{HDWalletMockStorage, HDWalletStorageInternalOps}; +use crate::rpc_command::account_balance::{AccountBalanceParams, AccountBalanceRpcOps, HDAccountBalanceResponse}; +use crate::rpc_command::init_scan_for_new_addresses::{InitScanAddressesRpcOps, ScanAddressesParams, + ScanAddressesResponse}; use crate::utxo::qtum::{qtum_coin_with_priv_key, QtumCoin, QtumDelegationOps, QtumDelegationRequest}; use crate::utxo::rpc_clients::{BlockHashOrHeight, ElectrumBalance, ElectrumClient, ElectrumClientImpl, GetAddressInfoRes, ListSinceBlockRes, ListTransactionsItem, NativeClient, - NativeClientImpl, NetworkInfo, UtxoRpcClientOps, ValidateAddressRes, VerboseBlock}; + NativeClientImpl, NativeUnspent, NetworkInfo, UtxoRpcClientOps, ValidateAddressRes, + VerboseBlock}; +use crate::utxo::tx_cache::dummy_tx_cache::DummyVerboseCache; +use crate::utxo::tx_cache::UtxoVerboseCacheOps; use crate::utxo::utxo_builder::{UtxoArcBuilder, UtxoCoinBuilderCommonOps}; use crate::utxo::utxo_common::UtxoTxBuilder; +use crate::utxo::utxo_common_tests; use crate::utxo::utxo_standard::{utxo_standard_coin_with_priv_key, UtxoStandardCoin}; #[cfg(not(target_arch = "wasm32"))] use crate::WithdrawFee; use crate::{CoinBalance, PrivKeyBuildPolicy, StakingInfosDetails, SwapOps, TradePreimageValue, TxFeeDetails}; @@ -24,6 +30,7 @@ use futures::TryFutureExt; use mocktopus::mocking::*; use rpc::v1::types::H256 as H256Json; use serialization::{deserialize, CoinVariant}; +use std::iter; use std::mem::discriminant; use std::num::NonZeroUsize; @@ -156,7 +163,7 @@ fn utxo_coin_fields_for_test( priv_key_policy, derivation_method, history_sync_state: Mutex::new(HistorySyncState::NotEnabled), - tx_cache_directory: None, + tx_cache: DummyVerboseCache::default().into_shared(), block_headers_storage: None, recently_spent_outpoints: AsyncMutex::new(RecentlySpentOutPoints::new(my_script_pubkey)), tx_hash_algo: TxHashAlgo::DSHA256, @@ -544,7 +551,7 @@ fn test_search_for_swap_tx_spend_electrum_was_refunded() { #[test] #[cfg(not(target_arch = "wasm32"))] fn test_withdraw_impl_set_fixed_fee() { - UtxoStandardCoin::list_unspent_ordered.mock_safe(|coin, _| { + UtxoStandardCoin::get_unspent_ordered_list.mock_safe(|coin, _| { let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); let unspents = vec![UnspentInfo { outpoint: OutPoint { @@ -585,7 +592,7 @@ fn test_withdraw_impl_set_fixed_fee() { #[test] #[cfg(not(target_arch = "wasm32"))] fn test_withdraw_impl_sat_per_kb_fee() { - UtxoStandardCoin::list_unspent_ordered.mock_safe(|coin, _| { + UtxoStandardCoin::get_unspent_ordered_list.mock_safe(|coin, _| { let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); let unspents = vec![UnspentInfo { outpoint: OutPoint { @@ -629,7 +636,7 @@ fn test_withdraw_impl_sat_per_kb_fee() { #[test] #[cfg(not(target_arch = "wasm32"))] fn test_withdraw_impl_sat_per_kb_fee_amount_equal_to_max() { - UtxoStandardCoin::list_unspent_ordered.mock_safe(|coin, _| { + UtxoStandardCoin::get_unspent_ordered_list.mock_safe(|coin, _| { let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); let unspents = vec![UnspentInfo { outpoint: OutPoint { @@ -675,7 +682,7 @@ fn test_withdraw_impl_sat_per_kb_fee_amount_equal_to_max() { #[test] #[cfg(not(target_arch = "wasm32"))] fn test_withdraw_impl_sat_per_kb_fee_amount_equal_to_max_dust_included_to_fee() { - UtxoStandardCoin::list_unspent_ordered.mock_safe(|coin, _| { + UtxoStandardCoin::get_unspent_ordered_list.mock_safe(|coin, _| { let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); let unspents = vec![UnspentInfo { outpoint: OutPoint { @@ -721,7 +728,7 @@ fn test_withdraw_impl_sat_per_kb_fee_amount_equal_to_max_dust_included_to_fee() #[test] #[cfg(not(target_arch = "wasm32"))] fn test_withdraw_impl_sat_per_kb_fee_amount_over_max() { - UtxoStandardCoin::list_unspent_ordered.mock_safe(|coin, _| { + UtxoStandardCoin::get_unspent_ordered_list.mock_safe(|coin, _| { let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); let unspents = vec![UnspentInfo { outpoint: OutPoint { @@ -754,7 +761,7 @@ fn test_withdraw_impl_sat_per_kb_fee_amount_over_max() { #[test] #[cfg(not(target_arch = "wasm32"))] fn test_withdraw_impl_sat_per_kb_fee_max() { - UtxoStandardCoin::list_unspent_ordered.mock_safe(|coin, _| { + UtxoStandardCoin::get_unspent_ordered_list.mock_safe(|coin, _| { let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); let unspents = vec![UnspentInfo { outpoint: OutPoint { @@ -805,7 +812,7 @@ fn test_withdraw_kmd_rewards_impl( ) { let verbose: RpcTransaction = json::from_str(verbose_serialized).unwrap(); let unspent_height = verbose.height; - UtxoStandardCoin::list_unspent_ordered.mock_safe(move |coin, _| { + UtxoStandardCoin::get_unspent_ordered_list.mock_safe(move |coin, _| { let tx: UtxoTx = tx_hex.into(); let unspents = vec![UnspentInfo { outpoint: OutPoint { @@ -888,7 +895,7 @@ fn test_withdraw_rick_rewards_none() { // https://rick.explorer.dexstats.info/tx/7181400be323acc6b5f3164240e6c4601ff4c252f40ce7649f87e81634330209 const TX_HEX: &str = "0400008085202f8901df8119c507aa61d32332cd246dbfeb3818a4f96e76492454c1fbba5aa097977e000000004847304402205a7e229ea6929c97fd6dde254c19e4eb890a90353249721701ae7a1c477d99c402206a8b7c5bf42b5095585731d6b4c589ce557f63c20aed69ff242eca22ecfcdc7a01feffffff02d04d1bffbc050000232102afdbba3e3c90db5f0f4064118f79cf308f926c68afd64ea7afc930975663e4c4ac402dd913000000001976a9143e17014eca06281ee600adffa34b4afb0922a22288ac2bdab86035a00e000000000000000000000000"; - UtxoStandardCoin::list_unspent_ordered.mock_safe(move |coin, _| { + UtxoStandardCoin::get_unspent_ordered_list.mock_safe(move |coin, _| { let tx: UtxoTx = TX_HEX.into(); let unspents = vec![UnspentInfo { outpoint: OutPoint { @@ -923,25 +930,6 @@ fn test_withdraw_rick_rewards_none() { assert_eq!(tx_details.kmd_rewards, None); } -#[test] -fn test_list_mature_unspents_ordered_without_tx_cache() { - let client = electrum_client_for_test(RICK_ELECTRUM_ADDRS); - let coin = utxo_coin_for_test( - client.into(), - Some("spice describe gravity federal blast come thank unfair canal monkey style afraid"), - false, - ); - assert!(coin.as_ref().tx_cache_directory.is_none()); - assert_ne!( - coin.my_spendable_balance().wait().unwrap(), - 0.into(), - "The test address doesn't have unspent outputs" - ); - let (unspents, _) = - block_on(coin.list_mature_unspent_ordered(&Address::from("R9o9xTocqr6CeEDGDH6mEYpwLoMz6jNjMW"))).unwrap(); - assert!(!unspents.is_empty()); -} - #[test] fn test_utxo_lock() { // send several transactions concurrently to check that they are not using same inputs @@ -1583,77 +1571,6 @@ fn test_one_unavailable_electrum_proto_version() { assert!(coin.as_ref().rpc_client.get_block_count().wait().is_ok()); } -#[test] -fn test_qtum_unspendable_balance_failed_once() { - let mut unspents = vec![ - // spendable balance (69.0) > balance (68.0) - vec![ - UnspentInfo { - outpoint: OutPoint { - hash: 1.into(), - index: 0, - }, - value: 5000000000, - height: Default::default(), - }, - UnspentInfo { - outpoint: OutPoint { - hash: 1.into(), - index: 0, - }, - value: 1900000000, - height: Default::default(), - }, - ], - // spendable balance (68.0) == balance (68.0) - vec![ - UnspentInfo { - outpoint: OutPoint { - hash: 1.into(), - index: 0, - }, - value: 5000000000, - height: Default::default(), - }, - UnspentInfo { - outpoint: OutPoint { - hash: 1.into(), - index: 0, - }, - value: 1800000000, - height: Default::default(), - }, - ], - ]; - QtumCoin::list_mature_unspent_ordered.mock_safe(move |coin, _| { - let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); - let unspents = unspents.pop().unwrap(); - MockResult::Return(Box::pin(futures::future::ok((unspents, cache)))) - }); - - let conf = json!({"coin":"tQTUM","rpcport":13889,"pubtype":120,"p2shtype":110}); - let req = json!({ - "method": "electrum", - "servers": [{"url":"electrum1.cipig.net:10071"}, {"url":"electrum2.cipig.net:10071"}, {"url":"electrum3.cipig.net:10071"}], - }); - - let ctx = MmCtxBuilder::new().into_mm_arc(); - - let priv_key = [ - 184, 199, 116, 240, 113, 222, 8, 199, 253, 143, 98, 185, 127, 26, 87, 38, 246, 206, 159, 27, 207, 20, 27, 112, - 184, 102, 137, 37, 78, 214, 113, 78, - ]; - - let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); - let coin = block_on(qtum_coin_with_priv_key(&ctx, "tQTUM", &conf, ¶ms, &priv_key)).unwrap(); - - let CoinBalance { spendable, unspendable } = coin.my_balance().wait().unwrap(); - let expected_spendable = BigDecimal::from(68); - let expected_unspendable = BigDecimal::from(0); - assert_eq!(spendable, expected_spendable); - assert_eq!(unspendable, expected_unspendable); -} - #[test] fn test_qtum_generate_pod() { let priv_key = [ @@ -1799,60 +1716,12 @@ fn test_qtum_remove_delegation() { assert_eq!(res.is_err(), false); } -#[test] -fn test_qtum_unspendable_balance_failed() { - QtumCoin::list_mature_unspent_ordered.mock_safe(move |coin, _| { - let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); - // spendable balance (69.0) > balance (68.0) - let unspents = vec![ - UnspentInfo { - outpoint: OutPoint { - hash: 1.into(), - index: 0, - }, - value: 5000000000, - height: Default::default(), - }, - UnspentInfo { - outpoint: OutPoint { - hash: 1.into(), - index: 0, - }, - value: 1900000000, - height: Default::default(), - }, - ]; - MockResult::Return(Box::pin(futures::future::ok((unspents, cache)))) - }); - - let conf = json!({"coin":"tQTUM","rpcport":13889,"pubtype":120,"p2shtype":110}); - let req = json!({ - "method": "electrum", - "servers": [{"url":"electrum1.cipig.net:10071"}, {"url":"electrum2.cipig.net:10071"}, {"url":"electrum3.cipig.net:10071"}], - }); - - let ctx = MmCtxBuilder::new().into_mm_arc(); - - let priv_key = [ - 184, 199, 116, 240, 113, 222, 8, 199, 253, 143, 98, 185, 127, 26, 87, 38, 246, 206, 159, 27, 207, 20, 27, 112, - 184, 102, 137, 37, 78, 214, 113, 78, - ]; - - let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); - let coin = block_on(qtum_coin_with_priv_key(&ctx, "tQTUM", &conf, ¶ms, &priv_key)).unwrap(); - - let error = coin.my_balance().wait().err().unwrap(); - log!("error: "[error]); - let expected_error = BalanceError::Internal("Spendable balance 69 greater than total balance 68".to_owned()); - assert_eq!(error.get_inner(), &expected_error); -} - #[test] fn test_qtum_my_balance() { - QtumCoin::list_mature_unspent_ordered.mock_safe(move |coin, _| { + QtumCoin::get_mature_unspent_ordered_list.mock_safe(move |coin, _address| { let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); - // spendable balance (66.0) < balance (68.0), then unspendable balance is expected to be (2.0) - let unspents = vec![ + // spendable balance (66.0) + let mature = vec![ UnspentInfo { outpoint: OutPoint { hash: 1.into(), @@ -1870,7 +1739,19 @@ fn test_qtum_my_balance() { height: Default::default(), }, ]; - MockResult::Return(Box::pin(futures::future::ok((unspents, cache)))) + // unspendable (2.0) + let immature = vec![UnspentInfo { + outpoint: OutPoint { + hash: 1.into(), + index: 0, + }, + value: 200000000, + height: Default::default(), + }]; + MockResult::Return(Box::pin(futures::future::ok(( + MatureUnspentList { mature, immature }, + cache, + )))) }); let conf = json!({"coin":"tQTUM","rpcport":13889,"pubtype":120,"p2shtype":110}); @@ -1902,8 +1783,10 @@ fn test_qtum_my_balance_with_check_utxo_maturity_false() { ElectrumClient::display_balance.mock_safe(move |_, _, _| { MockResult::Return(Box::new(futures01::future::ok(BigDecimal::from(DISPLAY_BALANCE)))) }); - QtumCoin::list_all_unspent_ordered.mock_safe(move |_, _| { - panic!("'QtumCoin::list_all_unspent_ordered' is not expected to be called when `check_utxo_maturity` is false") + QtumCoin::get_all_unspent_ordered_list.mock_safe(move |_, _| { + panic!( + "'QtumCoin::get_all_unspent_ordered_list' is not expected to be called when `check_utxo_maturity` is false" + ) }); let conf = json!({"coin":"tQTUM","rpcport":13889,"pubtype":120,"p2shtype":110}); @@ -1930,7 +1813,7 @@ fn test_qtum_my_balance_with_check_utxo_maturity_false() { assert_eq!(unspendable, expected_unspendable); } -fn test_list_mature_unspents_ordered_from_cache_impl( +fn test_get_mature_unspent_ordered_map_from_cache_impl( unspent_height: Option, cached_height: Option, cached_confs: u32, @@ -1959,11 +1842,10 @@ fn test_list_mature_unspents_ordered_from_cache_impl( }); ElectrumClient::get_block_count .mock_safe(move |_| MockResult::Return(Box::new(futures01::future::ok(block_count)))); - UtxoStandardCoin::get_verbose_transaction_from_cache_or_rpc.mock_safe(move |_, txid| { - assert_eq!(txid, tx_hash); - MockResult::Return(Box::new(futures01::future::ok(VerboseTransactionFrom::Cache( - verbose.clone(), - )))) + UtxoStandardCoin::get_verbose_transactions_from_cache_or_rpc.mock_safe(move |_, tx_ids| { + itertools::assert_equal(tx_ids, iter::once(tx_hash)); + let result: HashMap<_, _> = iter::once((tx_hash, VerboseTransactionFrom::Cache(verbose.clone()))).collect(); + MockResult::Return(Box::new(futures01::future::ok(result))) }); static mut IS_UNSPENT_MATURE_CALLED: bool = false; UtxoStandardCoin::is_unspent_mature.mock_safe(move |_, tx: &RpcTransaction| { @@ -1977,22 +1859,23 @@ fn test_list_mature_unspents_ordered_from_cache_impl( // run test let coin = utxo_coin_for_test(UtxoRpcClientEnum::Electrum(client), None, false); let (unspents, _) = - block_on(coin.list_mature_unspent_ordered(&Address::from("R9o9xTocqr6CeEDGDH6mEYpwLoMz6jNjMW"))) + block_on(coin.get_mature_unspent_ordered_list(&Address::from("R9o9xTocqr6CeEDGDH6mEYpwLoMz6jNjMW"))) .expect("Expected an empty unspent list"); // unspents should be empty because `is_unspent_mature()` always returns false - assert!(unspents.is_empty()); assert!(unsafe { IS_UNSPENT_MATURE_CALLED == true }); + assert!(unspents.mature.is_empty()); + assert_eq!(unspents.immature.len(), 1); } #[test] -fn test_list_mature_unspents_ordered_from_cache() { +fn test_get_mature_unspents_ordered_map_from_cache() { let unspent_height = None; let cached_height = None; let cached_confs = 0; let block_count = 1000; let expected_height = None; // is unknown let expected_confs = 0; // is not changed because height is unknown - test_list_mature_unspents_ordered_from_cache_impl( + test_get_mature_unspent_ordered_map_from_cache_impl( unspent_height, cached_height, cached_confs, @@ -2007,7 +1890,7 @@ fn test_list_mature_unspents_ordered_from_cache() { let block_count = 1000; let expected_height = None; // is unknown let expected_confs = 5; // is not changed because height is unknown - test_list_mature_unspents_ordered_from_cache_impl( + test_get_mature_unspent_ordered_map_from_cache_impl( unspent_height, cached_height, cached_confs, @@ -2022,7 +1905,7 @@ fn test_list_mature_unspents_ordered_from_cache() { let block_count = 1000; let expected_height = Some(998); // as the unspent_height let expected_confs = 3; // 1000 - 998 + 1 - test_list_mature_unspents_ordered_from_cache_impl( + test_get_mature_unspent_ordered_map_from_cache_impl( unspent_height, cached_height, cached_confs, @@ -2037,7 +1920,7 @@ fn test_list_mature_unspents_ordered_from_cache() { let block_count = 1000; let expected_height = Some(998); // as the cached_height let expected_confs = 3; // 1000 - 998 + 1 - test_list_mature_unspents_ordered_from_cache_impl( + test_get_mature_unspent_ordered_map_from_cache_impl( unspent_height, cached_height, cached_confs, @@ -2052,7 +1935,7 @@ fn test_list_mature_unspents_ordered_from_cache() { let block_count = 1000; let expected_height = Some(998); // as the unspent_height let expected_confs = 3; // 1000 - 998 + 1 - test_list_mature_unspents_ordered_from_cache_impl( + test_get_mature_unspent_ordered_map_from_cache_impl( unspent_height, cached_height, cached_confs, @@ -2068,7 +1951,7 @@ fn test_list_mature_unspents_ordered_from_cache() { let block_count = 999; let expected_height = Some(1000); // as the cached_height let expected_confs = 1; // is not changed because height cannot be calculated - test_list_mature_unspents_ordered_from_cache_impl( + test_get_mature_unspent_ordered_map_from_cache_impl( unspent_height, cached_height, cached_confs, @@ -2084,7 +1967,7 @@ fn test_list_mature_unspents_ordered_from_cache() { let block_count = 1000; let expected_height = Some(1000); // as the cached_height let expected_confs = 1; // 1000 - 1000 + 1 - test_list_mature_unspents_ordered_from_cache_impl( + test_get_mature_unspent_ordered_map_from_cache_impl( unspent_height, cached_height, cached_confs, @@ -2100,7 +1983,7 @@ fn test_list_mature_unspents_ordered_from_cache() { let block_count = 1000; let expected_height = Some(0); // as the cached_height let expected_confs = 1; // is not changed because tx_height is expected to be not zero - test_list_mature_unspents_ordered_from_cache_impl( + test_get_mature_unspent_ordered_map_from_cache_impl( unspent_height, cached_height, cached_confs, @@ -2143,8 +2026,7 @@ fn test_native_client_unspents_filtered_using_tx_cache_single_tx_in_cache() { NativeClient::list_unspent .mock_safe(move |_, _, _| MockResult::Return(Box::new(futures01::future::ok(spent_by_tx.clone())))); - let address: Address = "RGfFZaaNV68uVe1uMf6Y37Y8E1i2SyYZBN".into(); - let (unspents_ordered, _) = block_on(coin.list_unspent_ordered(&address)).unwrap(); + let (unspents_ordered, _) = block_on(coin.get_unspent_ordered_list(&address)).unwrap(); // output 2 is change so it must be returned let expected_unspent = UnspentInfo { outpoint: OutPoint { @@ -2238,7 +2120,7 @@ fn test_native_client_unspents_filtered_using_tx_cache_single_several_chained_tx NativeClient::list_unspent .mock_safe(move |_, _, _| MockResult::Return(Box::new(futures01::future::ok(unspents_to_return.clone())))); - let (unspents_ordered, _) = block_on(coin.list_unspent_ordered(&address)).unwrap(); + let (unspents_ordered, _) = block_on(coin.get_unspent_ordered_list(&address)).unwrap(); // output 2 is change so it must be returned let expected_unspent = UnspentInfo { @@ -3152,7 +3034,7 @@ fn tbch_electroncash_verbose_tx_unconfirmed() { #[test] #[cfg(not(target_arch = "wasm32"))] fn test_withdraw_to_p2pkh() { - UtxoStandardCoin::list_unspent_ordered.mock_safe(|coin, _| { + UtxoStandardCoin::get_unspent_ordered_list.mock_safe(|coin, _| { let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); let unspents = vec![UnspentInfo { outpoint: OutPoint { @@ -3199,7 +3081,7 @@ fn test_withdraw_to_p2pkh() { #[test] #[cfg(not(target_arch = "wasm32"))] fn test_withdraw_to_p2sh() { - UtxoStandardCoin::list_unspent_ordered.mock_safe(|coin, _| { + UtxoStandardCoin::get_unspent_ordered_list.mock_safe(|coin, _| { let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); let unspents = vec![UnspentInfo { outpoint: OutPoint { @@ -3246,7 +3128,7 @@ fn test_withdraw_to_p2sh() { #[test] #[cfg(not(target_arch = "wasm32"))] fn test_withdraw_to_p2wpkh() { - UtxoStandardCoin::list_unspent_ordered.mock_safe(|coin, _| { + UtxoStandardCoin::get_unspent_ordered_list.mock_safe(|coin, _| { let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); let unspents = vec![UnspentInfo { outpoint: OutPoint { @@ -3294,13 +3176,13 @@ fn test_withdraw_to_p2wpkh() { /// https://github.com/KomodoPlatform/atomicDEX-API/issues/1181 #[test] fn test_utxo_standard_with_check_utxo_maturity_true() { - static mut LIST_MATURE_UNSPENT_ORDERED_CALLED: bool = false; + /// Whether [`UtxoStandardCoin::get_mature_unspent_ordered_list`] is called or not. + static mut GET_MATURE_UNSPENT_ORDERED_LIST_CALLED: bool = false; - UtxoStandardCoin::list_mature_unspent_ordered.mock_safe(|coin, _| { - unsafe { LIST_MATURE_UNSPENT_ORDERED_CALLED = true }; + UtxoStandardCoin::get_mature_unspent_ordered_list.mock_safe(|coin, _| { + unsafe { GET_MATURE_UNSPENT_ORDERED_LIST_CALLED = true }; let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); - let unspents = Vec::new(); - MockResult::Return(Box::pin(futures::future::ok((unspents, cache)))) + MockResult::Return(Box::pin(futures::future::ok((MatureUnspentList::default(), cache)))) }); let conf = json!({"coin":"RICK","asset":"RICK","rpcport":25435,"txversion":4,"overwintered":1,"mm2":1,"protocol":{"type":"UTXO"}}); @@ -3323,26 +3205,27 @@ fn test_utxo_standard_with_check_utxo_maturity_true() { .unwrap(); let address = Address::from("R9o9xTocqr6CeEDGDH6mEYpwLoMz6jNjMW"); - // Don't use `block_on` here because it's used within a mock of [`UtxoStandardCoin::list_mature_unspent_ordered`]. - coin.list_unspent_ordered(&address).compat().wait().unwrap(); - assert!(unsafe { LIST_MATURE_UNSPENT_ORDERED_CALLED }); + // Don't use `block_on` here because it's used within a mock of [`GetUtxoListOps::get_mature_unspent_ordered_list`]. + coin.get_unspent_ordered_list(&address).compat().wait().unwrap(); + assert!(unsafe { GET_MATURE_UNSPENT_ORDERED_LIST_CALLED }); } /// `UtxoStandardCoin` hasn't to check UTXO maturity if `check_utxo_maturity` is not set. /// https://github.com/KomodoPlatform/atomicDEX-API/issues/1181 #[test] fn test_utxo_standard_without_check_utxo_maturity() { - static mut LIST_ALL_UNSPENT_ORDERED_CALLED: bool = false; + /// Whether [`UtxoStandardCoin::get_all_unspent_ordered_list`] is called or not. + static mut GET_ALL_UNSPENT_ORDERED_LIST_CALLED: bool = false; - UtxoStandardCoin::list_all_unspent_ordered.mock_safe(|coin, _| { - unsafe { LIST_ALL_UNSPENT_ORDERED_CALLED = true }; + UtxoStandardCoin::get_all_unspent_ordered_list.mock_safe(|coin, _| { + unsafe { GET_ALL_UNSPENT_ORDERED_LIST_CALLED = true }; let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); let unspents = Vec::new(); MockResult::Return(Box::pin(futures::future::ok((unspents, cache)))) }); - UtxoStandardCoin::list_mature_unspent_ordered.mock_safe(|_, _| { - panic!("'UtxoStandardCoin::list_mature_unspent_ordered' is not expected to be called when `check_utxo_maturity` is not set") + UtxoStandardCoin::get_mature_unspent_ordered_list.mock_safe(|_, _| { + panic!("'UtxoStandardCoin::get_mature_unspent_ordered_list' is not expected to be called when `check_utxo_maturity` is not set") }); let conf = json!({"coin":"RICK","asset":"RICK","rpcport":25435,"txversion":4,"overwintered":1,"mm2":1,"protocol":{"type":"UTXO"}}); @@ -3364,22 +3247,22 @@ fn test_utxo_standard_without_check_utxo_maturity() { .unwrap(); let address = Address::from("R9o9xTocqr6CeEDGDH6mEYpwLoMz6jNjMW"); - // Don't use `block_on` here because it's used within a mock of [`UtxoStandardCoin::list_mature_unspent_ordered`]. - coin.list_unspent_ordered(&address).compat().wait().unwrap(); - assert!(unsafe { LIST_ALL_UNSPENT_ORDERED_CALLED }); + // Don't use `block_on` here because it's used within a mock of [`UtxoStandardCoin::get_all_unspent_ordered_list`]. + coin.get_unspent_ordered_list(&address).compat().wait().unwrap(); + assert!(unsafe { GET_ALL_UNSPENT_ORDERED_LIST_CALLED }); } /// `QtumCoin` has to check UTXO maturity if `check_utxo_maturity` is not set. /// https://github.com/KomodoPlatform/atomicDEX-API/issues/1181 #[test] fn test_qtum_without_check_utxo_maturity() { - static mut LIST_MATURE_UNSPENT_ORDERED_CALLED: bool = false; + /// Whether [`QtumCoin::get_mature_unspent_ordered_list`] is called or not. + static mut GET_MATURE_UNSPENT_ORDERED_LIST_CALLED: bool = false; - QtumCoin::list_mature_unspent_ordered.mock_safe(|coin, _| { - unsafe { LIST_MATURE_UNSPENT_ORDERED_CALLED = true }; + QtumCoin::get_mature_unspent_ordered_list.mock_safe(|coin, _| { + unsafe { GET_MATURE_UNSPENT_ORDERED_LIST_CALLED = true }; let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); - let unspents = Vec::new(); - MockResult::Return(Box::pin(futures::future::ok((unspents, cache)))) + MockResult::Return(Box::pin(futures::future::ok((MatureUnspentList::default(), cache)))) }); let conf = json!({"coin":"tQTUM","rpcport":13889,"pubtype":120,"p2shtype":110}); @@ -3398,26 +3281,27 @@ fn test_qtum_without_check_utxo_maturity() { let coin = block_on(qtum_coin_with_priv_key(&ctx, "QTUM", &conf, ¶ms, &[1u8; 32])).unwrap(); let address = Address::from("qcyBHeSct7Wr4mAw18iuQ1zW5mMFYmtmBE"); - // Don't use `block_on` here because it's used within a mock of [`QtumCoin::list_mature_unspent_ordered`]. - coin.list_unspent_ordered(&address).compat().wait().unwrap(); - assert!(unsafe { LIST_MATURE_UNSPENT_ORDERED_CALLED }); + // Don't use `block_on` here because it's used within a mock of [`QtumCoin::get_mature_unspent_ordered_list`]. + coin.get_unspent_ordered_list(&address).compat().wait().unwrap(); + assert!(unsafe { GET_MATURE_UNSPENT_ORDERED_LIST_CALLED }); } /// `QtumCoin` hasn't to check UTXO maturity if `check_utxo_maturity` is `false`. /// https://github.com/KomodoPlatform/atomicDEX-API/issues/1181 #[test] fn test_qtum_with_check_utxo_maturity_false() { - static mut LIST_ALL_UNSPENT_ORDERED_CALLED: bool = false; + /// Whether [`QtumCoin::get_all_unspent_ordered_list`] is called or not. + static mut GET_ALL_UNSPENT_ORDERED_LIST_CALLED: bool = false; - QtumCoin::list_all_unspent_ordered.mock_safe(|coin, _| { - unsafe { LIST_ALL_UNSPENT_ORDERED_CALLED = true }; + QtumCoin::get_all_unspent_ordered_list.mock_safe(|coin, _address| { + unsafe { GET_ALL_UNSPENT_ORDERED_LIST_CALLED = true }; let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); let unspents = Vec::new(); MockResult::Return(Box::pin(futures::future::ok((unspents, cache)))) }); - QtumCoin::list_mature_unspent_ordered.mock_safe(|_, _| { + QtumCoin::get_mature_unspent_ordered_list.mock_safe(|_, _| { panic!( - "'QtumCoin::list_mature_unspent_ordered' is not expected to be called when `check_utxo_maturity` is false" + "'QtumCoin::get_mature_unspent_ordered_list' is not expected to be called when `check_utxo_maturity` is false" ) }); @@ -3438,9 +3322,9 @@ fn test_qtum_with_check_utxo_maturity_false() { let coin = block_on(qtum_coin_with_priv_key(&ctx, "QTUM", &conf, ¶ms, &[1u8; 32])).unwrap(); let address = Address::from("qcyBHeSct7Wr4mAw18iuQ1zW5mMFYmtmBE"); - // Don't use `block_on` here because it's used within a mock of [`QtumCoin::list_mature_unspent_ordered`]. - coin.list_unspent_ordered(&address).compat().wait().unwrap(); - assert!(unsafe { LIST_ALL_UNSPENT_ORDERED_CALLED }); + // Don't use `block_on` here because it's used within a mock of [`QtumCoin::get_all_unspent_ordered_list`]. + coin.get_unspent_ordered_list(&address).compat().wait().unwrap(); + assert!(unsafe { GET_ALL_UNSPENT_ORDERED_LIST_CALLED }); } #[test] @@ -3486,12 +3370,18 @@ fn test_account_balance_rpc() { known_address!("m/44'/141'/1'/1/0", "RGo7sYzivPtzv8aRQ4A6vRJDxoqkRRBRhZ", Bip44Chain::Internal, balance = 0); } - NativeClient::display_balance.mock_safe(move |_, address: Address, _| { - let address = address.to_string(); - let balance = addresses_map - .remove(&address) - .expect(&format!("Unexpected address: {}", address)); - MockResult::Return(Box::new(futures01::future::ok(BigDecimal::from(balance)))) + NativeClient::display_balances.mock_safe(move |_, addresses: Vec
, _| { + let result: Vec<_> = addresses + .into_iter() + .map(|address| { + let address_str = address.to_string(); + let balance = addresses_map + .remove(&address_str) + .expect(&format!("Unexpected address: {}", address_str)); + (address, BigDecimal::from(balance)) + }) + .collect(); + MockResult::Return(Box::new(futures01::future::ok(result))) }); let client = NativeClient(Arc::new(NativeClientImpl::default())); @@ -3608,13 +3498,13 @@ fn test_account_balance_rpc() { }; assert_eq!(actual, expected); - // Request a balance of Account#0, internal addresses, starting from idx=1 + // Request a balance of Account#0, internal addresses, where idx > 0 let params = AccountBalanceParams { account_index: 0, chain: Bip44Chain::Internal, limit: 3, - paging_options: PagingOptionsEnum::FromId(1), + paging_options: PagingOptionsEnum::FromId(0), }; let actual = block_on(coin.account_balance_rpc(params)).expect("!account_balance_rpc"); let expected = HDAccountBalanceResponse { @@ -3626,7 +3516,7 @@ fn test_account_balance_rpc() { skipped: 1, total: 3, total_pages: 1, - paging_options: PagingOptionsEnum::FromId(1), + paging_options: PagingOptionsEnum::FromId(0), }; assert_eq!(actual, expected); @@ -3674,13 +3564,13 @@ fn test_account_balance_rpc() { }; assert_eq!(actual, expected); - // Request a balance of Account#1, external addresses, starting from idx=1 (out of bound) + // Request a balance of Account#1, external addresses, where idx > 0 (out of bound) let params = AccountBalanceParams { account_index: 1, chain: Bip44Chain::Internal, limit: 3, - paging_options: PagingOptionsEnum::FromId(1), + paging_options: PagingOptionsEnum::FromId(0), }; let actual = block_on(coin.account_balance_rpc(params)).expect("!account_balance_rpc"); let expected = HDAccountBalanceResponse { @@ -3692,7 +3582,7 @@ fn test_account_balance_rpc() { skipped: 1, total: 1, total_pages: 1, - paging_options: PagingOptionsEnum::FromId(1), + paging_options: PagingOptionsEnum::FromId(0), }; assert_eq!(actual, expected); } @@ -3842,12 +3732,12 @@ fn test_scan_for_new_addresses() { NEW_INTERNAL_ADDRESSES_NUMBER = 4; } - let params = CheckHDAccountBalanceParams { + let params = ScanAddressesParams { account_index: 0, gap_limit: Some(3), }; - let actual = block_on(coin.scan_for_new_addresses_rpc(params)).expect("!account_balance_rpc"); - let expected = CheckHDAccountBalanceResponse { + let actual = block_on(coin.init_scan_for_new_addresses_rpc(params)).expect("!account_balance_rpc"); + let expected = ScanAddressesResponse { account_index: 0, derivation_path: DerivationPath::from_str("m/44'/141'/0'").unwrap().into(), new_addresses: get_balances!( @@ -3867,12 +3757,12 @@ fn test_scan_for_new_addresses() { NEW_INTERNAL_ADDRESSES_NUMBER = 2; } - let params = CheckHDAccountBalanceParams { + let params = ScanAddressesParams { account_index: 1, gap_limit: None, }; - let actual = block_on(coin.scan_for_new_addresses_rpc(params)).expect("!account_balance_rpc"); - let expected = CheckHDAccountBalanceResponse { + let actual = block_on(coin.init_scan_for_new_addresses_rpc(params)).expect("!account_balance_rpc"); + let expected = ScanAddressesResponse { account_index: 1, derivation_path: DerivationPath::from_str("m/44'/141'/1'").unwrap().into(), new_addresses: get_balances!( @@ -3909,6 +3799,62 @@ fn test_electrum_balance_deserializing() { assert_eq!(actual.unconfirmed, i128::MAX); } +#[test] +fn test_electrum_display_balances() { + let rpc_client = electrum_client_for_test(RICK_ELECTRUM_ADDRS); + block_on(utxo_common_tests::test_electrum_display_balances(&rpc_client)); +} + +#[test] +fn test_native_display_balances() { + let unspents = vec![ + NativeUnspent { + address: "RG278CfeNPFtNztFZQir8cgdWexVhViYVy".to_owned(), + amount: "4.77699".into(), + ..NativeUnspent::default() + }, + NativeUnspent { + address: "RJeDDtDRtKUoL8BCKdH7TNCHqUKr7kQRsi".to_owned(), + amount: "0.77699".into(), + ..NativeUnspent::default() + }, + NativeUnspent { + address: "RQHn9VPHBqNjYwyKfJbZCiaxVrWPKGQjeF".to_owned(), + amount: "0.99998".into(), + ..NativeUnspent::default() + }, + NativeUnspent { + address: "RG278CfeNPFtNztFZQir8cgdWexVhViYVy".to_owned(), + amount: "1".into(), + ..NativeUnspent::default() + }, + ]; + + NativeClient::list_unspent_impl + .mock_safe(move |_, _, _, _| MockResult::Return(Box::new(futures01::future::ok(unspents.clone())))); + + let rpc_client = native_client_for_test(); + + let addresses = vec![ + "RG278CfeNPFtNztFZQir8cgdWexVhViYVy".into(), + "RYPz6Lr4muj4gcFzpMdv3ks1NCGn3mkDPN".into(), + "RJeDDtDRtKUoL8BCKdH7TNCHqUKr7kQRsi".into(), + "RQHn9VPHBqNjYwyKfJbZCiaxVrWPKGQjeF".into(), + ]; + let actual = rpc_client + .display_balances(addresses, TEST_COIN_DECIMALS) + .wait() + .unwrap(); + + let expected: Vec<(Address, BigDecimal)> = vec![ + ("RG278CfeNPFtNztFZQir8cgdWexVhViYVy".into(), BigDecimal::from(5.77699)), + ("RYPz6Lr4muj4gcFzpMdv3ks1NCGn3mkDPN".into(), BigDecimal::from(0)), + ("RJeDDtDRtKUoL8BCKdH7TNCHqUKr7kQRsi".into(), BigDecimal::from(0.77699)), + ("RQHn9VPHBqNjYwyKfJbZCiaxVrWPKGQjeF".into(), BigDecimal::from(0.99998)), + ]; + assert_eq!(actual, expected); +} + #[test] fn test_message_hash() { let client = electrum_client_for_test(RICK_ELECTRUM_ADDRS); diff --git a/mm2src/coins/utxo/utxo_wasm_tests.rs b/mm2src/coins/utxo/utxo_wasm_tests.rs index 15d8d852be..4eaaf01d79 100644 --- a/mm2src/coins/utxo/utxo_wasm_tests.rs +++ b/mm2src/coins/utxo/utxo_wasm_tests.rs @@ -1,6 +1,7 @@ use super::rpc_clients::{ElectrumClient, ElectrumClientImpl, ElectrumProtocol}; use super::*; use crate::utxo::rpc_clients::UtxoRpcClientOps; +use crate::utxo::utxo_common_tests; use common::executor::Timer; use serialization::deserialize; use wasm_bindgen_test::*; @@ -52,3 +53,9 @@ async fn test_electrum_rpc_client() { let expected = UtxoTx::from("0400008085202f8902358549fe3cf9a66bf61fb57bca1b3b49434a148a4dc29450b5eefe583f2f9ecf000000006a4730440220112aa3737672f8aa16a58426f5e7656ad13d21a219390c7a0b2e266ee6b216a8022008e9f9e94db91f069f831b0d40b7f75938122cddceaa25197146dfb00fe82599012102031d4256c4bc9f99ac88bf3dba21773132281f65f9bf23a59928bce08961e2f3ffffffff358549fe3cf9a66bf61fb57bca1b3b49434a148a4dc29450b5eefe583f2f9ecf010000006b483045022100d054464799246254b09f96333bf52537938abe31c24bacf41c9ef600b28155950220527ec33c4a5bef79dcabf97e38aa240fecdd14c96f698560b2f10ec2abc2e992012102031d4256c4bc9f99ac88bf3dba21773132281f65f9bf23a59928bce08961e2f3ffffffff0240420f00000000001976a91405aab5342166f8594baf17a7d9bef5d56744332788ac66418f00000000001976a91405aab5342166f8594baf17a7d9bef5d56744332788ac0e2aa85f000000000000000000000000000000"); assert_eq!(actual, expected); } + +#[wasm_bindgen_test] +async fn test_electrum_display_balances() { + let rpc_client = electrum_client_for_test(&["electrum1.cipig.net:30017", "electrum2.cipig.net:30017"]).await; + utxo_common_tests::test_electrum_display_balances(&rpc_client).await; +} diff --git a/mm2src/coins/utxo/utxo_withdraw.rs b/mm2src/coins/utxo/utxo_withdraw.rs index b4e31c9643..edd06c8d7a 100644 --- a/mm2src/coins/utxo/utxo_withdraw.rs +++ b/mm2src/coins/utxo/utxo_withdraw.rs @@ -1,6 +1,6 @@ -use crate::init_withdraw::{WithdrawAwaitingStatus, WithdrawInProgressStatus, WithdrawTaskHandle}; +use crate::rpc_command::init_withdraw::{WithdrawAwaitingStatus, WithdrawInProgressStatus, WithdrawTaskHandle}; use crate::utxo::utxo_common::{big_decimal_from_sat, UtxoTxBuilder}; -use crate::utxo::{output_script, sat_from_big_decimal, ActualTxFee, Address, FeePolicy, PrivKeyPolicy, +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, WithdrawRequest, WithdrawResult}; @@ -100,7 +100,7 @@ impl From for WithdrawError { pub trait UtxoWithdraw where Self: Sized + Sync, - Coin: UtxoCommonOps, + Coin: UtxoCommonOps + GetUtxoListOps, { fn coin(&self) -> &Coin; @@ -153,7 +153,7 @@ where let script_pubkey = output_script(&to, script_type).to_bytes(); let _utxo_lock = UTXO_LOCK.lock().await; - let (unspents, _) = coin.list_unspent_ordered(&self.sender_address()).await?; + let (unspents, _) = coin.get_unspent_ordered_list(&self.sender_address()).await?; let (value, fee_policy) = if req.max { ( unspents.iter().fold(0, |sum, unspent| sum + unspent.value), @@ -246,7 +246,7 @@ pub struct InitUtxoWithdraw<'a, Coin> { #[async_trait] impl<'a, Coin> UtxoWithdraw for InitUtxoWithdraw<'a, Coin> where - Coin: UtxoCommonOps + UtxoSignerOps, + Coin: UtxoCommonOps + GetUtxoListOps + UtxoSignerOps, { fn coin(&self) -> &Coin { &self.coin } @@ -404,7 +404,7 @@ pub struct StandardUtxoWithdraw { #[async_trait] impl UtxoWithdraw for StandardUtxoWithdraw where - Coin: UtxoCommonOps, + Coin: UtxoCommonOps + GetUtxoListOps, { fn coin(&self) -> &Coin { &self.coin } diff --git a/mm2src/coins/utxo_signer/src/sign_common.rs b/mm2src/coins/utxo_signer/src/sign_common.rs index fff677f8fa..723367c7ea 100644 --- a/mm2src/coins/utxo_signer/src/sign_common.rs +++ b/mm2src/coins/utxo_signer/src/sign_common.rs @@ -36,7 +36,7 @@ pub(crate) fn p2pk_spend_with_signature( let script_sig = script_sig(signature, fork_id); TransactionInput { - previous_output: unsigned_input.previous_output.clone(), + previous_output: unsigned_input.previous_output, script_sig: Builder::default().push_bytes(&script_sig).into_bytes(), sequence: unsigned_input.sequence, script_witness: vec![], @@ -52,7 +52,7 @@ pub(crate) fn p2pkh_spend_with_signature( let script_sig = script_sig_with_pub(public_key, fork_id, signature); TransactionInput { - previous_output: unsigned_input.previous_output.clone(), + previous_output: unsigned_input.previous_output, script_sig, sequence: unsigned_input.sequence, script_witness: vec![], @@ -77,7 +77,7 @@ pub(crate) fn p2sh_spend_with_signature( resulting_script.extend_from_slice(&redeem_part); TransactionInput { - previous_output: unsigned_input.previous_output.clone(), + previous_output: unsigned_input.previous_output, script_sig: resulting_script, sequence: unsigned_input.sequence, script_witness: vec![], @@ -93,7 +93,7 @@ pub(crate) fn p2wpkh_spend_with_signature( let script_sig = script_sig(signature, fork_id); TransactionInput { - previous_output: unsigned_input.previous_output.clone(), + previous_output: unsigned_input.previous_output, script_sig: Bytes::from(Vec::new()), sequence: unsigned_input.sequence, script_witness: vec![script_sig, Bytes::from(public_key.to_vec())], diff --git a/mm2src/coins/z_coin.rs b/mm2src/coins/z_coin.rs index d51e37e753..aba2308976 100644 --- a/mm2src/coins/z_coin.rs +++ b/mm2src/coins/z_coin.rs @@ -4,9 +4,10 @@ use crate::utxo::utxo_builder::{UtxoCoinBuilderCommonOps, UtxoCoinWithIguanaPriv UtxoFieldsWithIguanaPrivKeyBuilder}; use crate::utxo::utxo_common::{big_decimal_from_sat_unsigned, payment_script}; use crate::utxo::{sat_from_big_decimal, utxo_common, ActualTxFee, AdditionalTxData, Address, BroadcastTxErr, - FeePolicy, HistoryUtxoTx, HistoryUtxoTxMap, RecentlySpentOutPoints, UtxoActivationParams, - UtxoAddressFormat, UtxoArc, UtxoCoinFields, UtxoCommonOps, UtxoFeeDetails, UtxoTxBroadcastOps, - UtxoTxGenerationOps, UtxoWeak, VerboseTransactionFrom}; + FeePolicy, GetUtxoListOps, HistoryUtxoTx, HistoryUtxoTxMap, MatureUnspentList, + RecentlySpentOutPointsGuard, UtxoActivationParams, UtxoAddressFormat, UtxoArc, UtxoCoinFields, + UtxoCommonOps, UtxoFeeDetails, UtxoTxBroadcastOps, UtxoTxGenerationOps, UtxoWeak, + VerboseTransactionFrom}; use crate::{BalanceFut, CoinBalance, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MarketCoinOps, MmCoin, NegotiateSwapContractAddrErr, NumConversError, RawTransactionFut, RawTransactionRequest, SignatureError, SignatureResult, SwapOps, TradeFee, TradePreimageFut, TradePreimageResult, TradePreimageValue, @@ -27,17 +28,18 @@ use common::{log, now_ms}; use db_common::sqlite::rusqlite::types::Type; use db_common::sqlite::rusqlite::{Connection, Error as SqliteError, Row, ToSql, NO_PARAMS}; use futures::compat::Future01CompatExt; -use futures::lock::{Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard}; +use futures::lock::Mutex as AsyncMutex; use futures::{FutureExt, TryFutureExt}; use futures01::Future; use keys::hash::H256; use keys::{KeyPair, Public}; +#[cfg(test)] use mocktopus::macros::*; use primitives::bytes::Bytes; use rpc::v1::types::{Bytes as BytesJson, ToTxHash, Transaction as RpcTransaction, H256 as H256Json}; use script::{Builder as ScriptBuilder, Opcode, Script, TransactionInputSigner}; use serde_json::Value as Json; use serialization::{deserialize, serialize_list, CoinVariant, Reader}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::convert::TryFrom; use std::path::PathBuf; use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering}; @@ -1380,6 +1382,34 @@ impl UtxoTxBroadcastOps for ZCoin { } } +/// Please note `ZCoin` is not assumed to work with transparent UTXOs. +/// Remove implementation of the `GetUtxoListOps` trait for `ZCoin` +/// when [`ZCoin::preimage_trade_fee_required_to_send_outputs`] is refactored. +#[async_trait] +#[cfg_attr(test, mockable)] +impl GetUtxoListOps for ZCoin { + async fn get_unspent_ordered_list( + &self, + address: &Address, + ) -> UtxoRpcResult<(Vec, RecentlySpentOutPointsGuard<'_>)> { + utxo_common::get_unspent_ordered_list(self, address).await + } + + async fn get_all_unspent_ordered_list( + &self, + address: &Address, + ) -> UtxoRpcResult<(Vec, RecentlySpentOutPointsGuard<'_>)> { + utxo_common::get_all_unspent_ordered_list(self, address).await + } + + async fn get_mature_unspent_ordered_list( + &self, + address: &Address, + ) -> UtxoRpcResult<(MatureUnspentList, RecentlySpentOutPointsGuard<'_>)> { + utxo_common::get_mature_unspent_ordered_list(self, address).await + } +} + #[async_trait] impl UtxoCommonOps for ZCoin { async fn get_htlc_spend_fee(&self, tx_size: u64) -> UtxoRpcResult { @@ -1450,37 +1480,15 @@ impl UtxoCommonOps for ZCoin { .await } - async fn list_all_unspent_ordered<'a>( - &'a self, - address: &Address, - ) -> UtxoRpcResult<(Vec, AsyncMutexGuard<'a, RecentlySpentOutPoints>)> { - utxo_common::list_all_unspent_ordered(self, address).await - } - - async fn list_mature_unspent_ordered<'a>( - &'a self, - address: &Address, - ) -> UtxoRpcResult<(Vec, AsyncMutexGuard<'a, RecentlySpentOutPoints>)> { - utxo_common::list_mature_unspent_ordered(self, address).await - } - - fn get_verbose_transaction_from_cache_or_rpc(&self, txid: H256Json) -> UtxoRpcFut { + fn get_verbose_transactions_from_cache_or_rpc( + &self, + tx_ids: HashSet, + ) -> UtxoRpcFut> { let selfi = self.clone(); - let fut = async move { utxo_common::get_verbose_transaction_from_cache_or_rpc(&selfi.utxo_arc, txid).await }; + let fut = async move { utxo_common::get_verbose_transactions_from_cache_or_rpc(&selfi.utxo_arc, tx_ids).await }; Box::new(fut.boxed().compat()) } - async fn cache_transaction_if_possible(&self, tx: &RpcTransaction) -> Result<(), String> { - utxo_common::cache_transaction_if_possible(&self.utxo_arc, tx).await - } - - async fn list_unspent_ordered<'a>( - &'a self, - address: &Address, - ) -> UtxoRpcResult<(Vec, AsyncMutexGuard<'a, RecentlySpentOutPoints>)> { - utxo_common::list_unspent_ordered(self, address).await - } - async fn preimage_trade_fee_required_to_send_outputs( &self, outputs: Vec, diff --git a/mm2src/common/Cargo.toml b/mm2src/common/Cargo.toml index f92d0ba453..db330a5cbc 100644 --- a/mm2src/common/Cargo.toml +++ b/mm2src/common/Cargo.toml @@ -34,7 +34,7 @@ futures-cpupool = "0.1" hex = "0.3.2" http = "0.2" http-body = "0.1" -itertools = "0.8" +itertools = "0.10" keys = { path = "../mm2_bitcoin/keys" } lazy_static = "1.4" lightning = "0.0.105" diff --git a/mm2src/common/common.rs b/mm2src/common/common.rs index db2d48c826..46f6e99525 100644 --- a/mm2src/common/common.rs +++ b/mm2src/common/common.rs @@ -78,6 +78,17 @@ macro_rules! cfg_native { }; } +/// Returns a JSON error HyRes on a failure. +#[macro_export] +macro_rules! try_h { + ($e: expr) => { + match $e { + Ok(ok) => ok, + Err(err) => return $crate::rpc_err_response(500, &ERRL!("{}", err)), + } + }; +} + #[macro_use] pub mod jsonrpc_client; #[macro_use] @@ -88,6 +99,7 @@ pub mod mm_metrics; pub mod big_int_str; pub mod crash_reports; pub mod custom_futures; +pub mod custom_iter; pub mod duplex_mutex; pub mod event_dispatcher; pub mod for_tests; @@ -807,17 +819,6 @@ pub mod lazy { } } -/// Returns a JSON error HyRes on a failure. -#[macro_export] -macro_rules! try_h { - ($e: expr) => { - match $e { - Ok(ok) => ok, - Err(err) => return $crate::rpc_err_response(500, &ERRL!("{}", err)), - } - }; -} - /// Wraps a JSON string into the `HyRes` RPC response future. pub fn rpc_response(status: u16, body: T) -> HyRes where diff --git a/mm2src/common/custom_iter.rs b/mm2src/common/custom_iter.rs new file mode 100644 index 0000000000..4a26d4b336 --- /dev/null +++ b/mm2src/common/custom_iter.rs @@ -0,0 +1,112 @@ +use std::collections::HashMap; +use std::hash::Hash; +use std::iter::FromIterator; + +pub trait CollectInto { + /// Collects `FromB` from an `IntoIterator` given the fact that `A: Into`. + /// + /// # Usage + /// + /// ```rust + /// let actual: Vec = vec!["foo", "bar"].collect_into(); + /// let expected = vec!["foo".to_owned(), "bar".to_owned()]; + /// assert_eq!(actual, expected); + /// ``` + #[inline] + fn collect_into(self) -> FromB + where + Self: IntoIterator + Sized, + A: Into, + FromB: FromIterator, + { + self.into_iter().map(A::into).collect() + } +} + +impl CollectInto for T {} + +pub trait TryIntoGroupMap { + /// An iterator method that unwraps the given `Result<(Key, Value), Err>` items yielded by the input iterator + /// and collects `(Key, Value)` tuple pairs into a `HashMap` of keys mapped to `Vec`s of values until an `Err` error is encountered. + fn try_into_group_map(self) -> Result>, Err> + where + Self: Iterator> + Sized, + K: Hash + Eq, + { + let (lower, upper) = self.size_hint(); + let capacity = upper.unwrap_or(lower); + + let mut lookup = HashMap::with_capacity(capacity); + for res in self { + let (key, val) = res?; + lookup.entry(key).or_insert_with(Vec::new).push(val); + } + Ok(lookup) + } +} + +impl TryIntoGroupMap for T {} + +pub trait TryUnzip +where + Self: Iterator> + Sized, +{ + /// An iterator method that unwraps the given `Result<(A, B), Err>` items yielded by the input iterator + /// and collects `(A, B)` tuple pairs into the pair of `(FromA, FromB)` containers until a `E` error is encountered. + fn try_unzip(self) -> Result<(FromA, FromB), E> + where + FromA: Default + Extend, + FromB: Default + Extend, + { + let (mut from_a, mut from_b) = (FromA::default(), FromB::default()); + for res in self { + let (a, b) = res?; + from_a.extend(Some(a)); + from_b.extend(Some(b)); + } + Ok((from_a, from_b)) + } +} + +impl TryUnzip for T where T: Iterator> {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_collect_into() { + let actual: Vec = vec!["foo", "bar"].collect_into(); + let expected = vec!["foo".to_owned(), "bar".to_owned()]; + assert_eq!(actual, expected); + } + + #[test] + fn test_try_into_group_map() { + let actual: Result<_, &'static str> = vec![Ok(("foo", 1)), Ok(("bar", 2)), Ok(("foo", 3))] + .into_iter() + .try_into_group_map(); + let expected: HashMap<_, _> = vec![("foo", vec![1, 3]), ("bar", vec![2])].into_iter().collect(); + assert_eq!(actual, Ok(expected)); + + let err = vec![Ok(("foo", 1)), Ok(("bar", 2)), Err("Error"), Ok(("foo", 3))] + .into_iter() + .try_into_group_map() + .unwrap_err(); + assert_eq!(err, "Error"); + } + + #[test] + fn test_try_unzip() { + let actual: Result<(Vec<_>, Vec<_>), &'static str> = vec![Ok(("foo", 1)), Ok(("bar", 2)), Ok(("foo", 3))] + .into_iter() + .try_unzip(); + assert_eq!(actual, Ok((vec!["foo", "bar", "foo"], vec![1, 2, 3]))); + + let err = vec![Ok(("foo", 1)), Ok(("bar", 2)), Err("Error"), Ok(("foo", 3))] + .into_iter() + .try_unzip::, Vec<_>>() + .unwrap_err(); + assert_eq!(err, "Error"); + } +} diff --git a/mm2src/common/fs/fs.rs b/mm2src/common/fs/fs.rs index 9bf99a904a..baf761fa2d 100644 --- a/mm2src/common/fs/fs.rs +++ b/mm2src/common/fs/fs.rs @@ -226,16 +226,26 @@ where Ok(result) } -pub async fn write_json(t: &T, path: &Path) -> FsJsonResult<()> +pub async fn write_json(t: &T, path: &Path, use_tmp_file: bool) -> FsJsonResult<()> where T: Serialize, { let content = json::to_vec(t).map_to_mm(FsJsonError::Serializing)?; + let path_tmp = if use_tmp_file { + PathBuf::from(format!("{}.tmp", path.display())) + } else { + path.to_path_buf() + }; + let fs_fut = async { - let mut file = async_fs::File::create(&path).await?; + let mut file = async_fs::File::create(&path_tmp).await?; file.write_all(&content).await?; file.flush().await?; + + if use_tmp_file { + async_fs::rename(path_tmp, path).await?; + } Ok(()) }; diff --git a/mm2src/common/jsonrpc_client.rs b/mm2src/common/jsonrpc_client.rs index b94cd6fb27..63f2ea0a55 100644 --- a/mm2src/common/jsonrpc_client.rs +++ b/mm2src/common/jsonrpc_client.rs @@ -1,6 +1,8 @@ use futures01::Future; +use itertools::Itertools; use serde::de::DeserializeOwned; use serde_json::{self as json, Value as Json}; +use std::collections::{BTreeSet, HashMap}; use std::fmt; /// Macro generating functions for RPC requests. @@ -9,41 +11,46 @@ use std::fmt; #[macro_export] macro_rules! rpc_func { ($selff:ident, $method:expr $(, $arg_name:expr)*) => {{ - let mut params = vec![]; - $( - params.push(json::value::to_value($arg_name).unwrap()); - )* - let request = JsonRpcRequest { - jsonrpc: $selff.version().into(), - id: $selff.next_id(), - method: $method.into(), - params - }; + let request = $crate::rpc_req!($selff, $method $(, $arg_name)*); $selff.send_request(request) }} } /// Macro generating functions for RPC requests. -/// Send the RPC request to specified remote endpoint using the passed address. +/// Sends the RPC request to specified remote endpoint using the passed address. /// Args must implement/derive Serialize trait. /// Generates params vector from input args, builds the request and sends it. #[macro_export] macro_rules! rpc_func_from { - ($selff:ident, $address:expr, $method:expr $(, $arg_name:ident)*) => {{ + ($selff:ident, $address:expr, $method:expr $(, $arg_name:expr)*) => {{ + let request = $crate::rpc_req!($selff, $method $(, $arg_name)*); + $selff.send_request_to($address, request) + }} +} + +/// Macro generating functions for RPC requests. +/// Args must implement/derive Serialize trait. +/// Generates params vector from input args, builds the `JsonRpcRequest` request. +#[macro_export] +macro_rules! rpc_req { + ($selff:ident, $method:expr $(, $arg_name:expr)*) => {{ let mut params = vec![]; $( params.push(json::value::to_value($arg_name).unwrap()); )* - let request = JsonRpcRequest { + JsonRpcRequest { jsonrpc: $selff.version().into(), id: $selff.next_id(), method: $method.into(), params - }; - $selff.send_request_to($address, request) + } }} } +pub type JsonRpcResponseFut = + Box + Send + 'static>; +pub type RpcRes = Box + Send + 'static>; + /// Address of server from which an Rpc response was received #[derive(Clone, Default)] pub struct JsonRpcRemoteAddr(pub String); @@ -60,8 +67,50 @@ impl From for JsonRpcRemoteAddr { fn from(addr: String) -> Self { JsonRpcRemoteAddr(addr) } } -/// Serializable RPC request -#[derive(Serialize, Deserialize, Debug, Clone)] +/// The identifier is designed to uniquely match outgoing requests and incoming responses. +/// Even if the batch response is sorted in a different order, `BTreeSet` allows it to be matched to the request. +#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] +pub enum JsonRpcId { + Single(String), + Batch(BTreeSet), +} + +/// Serializable RPC request that is either single or batch. +#[derive(Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum JsonRpcRequestEnum { + Single(JsonRpcRequest), + Batch(JsonRpcBatchRequest), +} + +impl JsonRpcRequestEnum { + /// Creates [`JsonRpcRequestEnum::Batch`] from the given `requests`. + #[inline] + pub fn new_batch(requests: Vec) -> JsonRpcRequestEnum { + JsonRpcRequestEnum::Batch(JsonRpcBatchRequest(requests)) + } + + /// Returns a `JsonRpcId` identifier of the request. + #[inline] + pub fn rpc_id(&self) -> JsonRpcId { + match self { + JsonRpcRequestEnum::Single(single) => single.rpc_id(), + JsonRpcRequestEnum::Batch(batch) => batch.rpc_id(), + } + } +} + +impl fmt::Debug for JsonRpcRequestEnum { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + JsonRpcRequestEnum::Single(single) => single.fmt(f), + JsonRpcRequestEnum::Batch(batch) => batch.fmt(f), + } + } +} + +/// Serializable RPC single request. +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct JsonRpcRequest { pub jsonrpc: String, #[serde(default)] @@ -71,10 +120,67 @@ pub struct JsonRpcRequest { } impl JsonRpcRequest { + // Returns [`JsonRpcRequest::id`]. + #[inline] pub fn get_id(&self) -> &str { &self.id } + + /// Returns a `JsonRpcId` identifier of the request. + #[inline] + pub fn rpc_id(&self) -> JsonRpcId { JsonRpcId::Single(self.id.clone()) } +} + +impl From for JsonRpcRequestEnum { + fn from(single: JsonRpcRequest) -> Self { JsonRpcRequestEnum::Single(single) } +} + +/// Serializable RPC batch request. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct JsonRpcBatchRequest(Vec); + +impl JsonRpcBatchRequest { + /// Returns a `JsonRpcId` identifier of the request. + #[inline] + pub fn rpc_id(&self) -> JsonRpcId { JsonRpcId::Batch(self.orig_sequence_ids().collect()) } + + /// Returns the number of the requests in the batch. + #[inline] + pub fn len(&self) -> usize { self.0.len() } + + /// Whether the batch is empty. + #[inline] + pub fn is_empty(&self) -> bool { self.0.is_empty() } + + /// Returns original sequence of identifiers. + /// The method is used to process batch responses in the same order in which the requests were sent. + #[inline] + fn orig_sequence_ids(&self) -> impl Iterator + '_ { self.0.iter().map(|req| req.id.clone()) } +} + +impl From for JsonRpcRequestEnum { + fn from(batch: JsonRpcBatchRequest) -> Self { JsonRpcRequestEnum::Batch(batch) } +} + +/// Deserializable RPC response that is either single or batch. +#[derive(Clone, Debug, Deserialize)] +#[serde(untagged)] +pub enum JsonRpcResponseEnum { + Single(JsonRpcResponse), + Batch(JsonRpcBatchResponse), +} + +impl JsonRpcResponseEnum { + /// Returns a `JsonRpcId` identifier of the response. + #[inline] + pub fn rpc_id(&self) -> JsonRpcId { + match self { + JsonRpcResponseEnum::Single(single) => single.rpc_id(), + JsonRpcResponseEnum::Batch(batch) => batch.rpc_id(), + } + } } -#[derive(Deserialize, Debug, Clone)] +/// Deserializable RPC single response. +#[derive(Clone, Debug, Deserialize)] pub struct JsonRpcResponse { #[serde(default)] pub jsonrpc: String, @@ -86,19 +192,55 @@ pub struct JsonRpcResponse { pub error: Json, } +impl JsonRpcResponse { + /// Returns a `JsonRpcId` identifier of the response. + #[inline] + pub fn rpc_id(&self) -> JsonRpcId { JsonRpcId::Single(self.id.clone()) } +} + +/// Deserializable RPC batch response. +#[derive(Clone, Debug, Deserialize)] +pub struct JsonRpcBatchResponse(Vec); + +impl JsonRpcBatchResponse { + /// Returns a `JsonRpcId` identifier of the response. + pub fn rpc_id(&self) -> JsonRpcId { JsonRpcId::Batch(self.0.iter().map(|res| res.id.clone()).collect()) } + + /// Returns the number of the requests in the batch. + #[inline] + pub fn len(&self) -> usize { self.0.len() } + + /// Whether the batch is empty. + #[inline] + pub fn is_empty(&self) -> bool { self.0.is_empty() } +} + +impl IntoIterator for JsonRpcBatchResponse { + type Item = JsonRpcResponse; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } +} + #[derive(Clone, Debug)] pub struct JsonRpcError { /// Additional member contains an instance info that implements the JsonRpcClient trait. /// The info is used in particular to supplement the error info. pub client_info: String, /// Source Rpc request. - pub request: JsonRpcRequest, + pub request: JsonRpcRequestEnum, /// Error type. pub error: JsonRpcErrorType, } +impl fmt::Display for JsonRpcError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{:?}", self) } +} + #[derive(Clone, Debug)] pub enum JsonRpcErrorType { + /// Invalid outgoing request error + InvalidRequest(String), /// Error from transport layer Transport(String), /// Response parse error @@ -108,40 +250,84 @@ pub enum JsonRpcErrorType { } impl JsonRpcErrorType { - pub fn is_transport(&self) -> bool { matches!(*self, JsonRpcErrorType::Transport(_)) } + /// Whether the error type is [`JsonRpcErrorType::Transport`]. + #[inline] + pub fn is_transport(&self) -> bool { matches!(self, JsonRpcErrorType::Transport(_)) } } -impl fmt::Display for JsonRpcError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{:?}", self) } -} - -pub type JsonRpcResponseFut = - Box + Send + 'static>; -pub type RpcRes = Box + Send + 'static>; - pub trait JsonRpcClient { + /// Returns a stringified version of the JSON-RPC protocol. fn version(&self) -> &'static str; + /// Returns a stringified identifier of the next request. fn next_id(&self) -> String; /// Get info that is used in particular to supplement the error info fn client_info(&self) -> String; - fn transport(&self, request: JsonRpcRequest) -> JsonRpcResponseFut; + /// Sends the given `request` to the remote. + /// Returns either an address `JsonRpcRemoteAddr` of the responder and the `JsonRpcResponseEnum` response, + /// or a stringified error. + fn transport(&self, request: JsonRpcRequestEnum) -> JsonRpcResponseFut; + /// Sends the given single `request` to the remote and tries to decode the response into `T`. fn send_request(&self, request: JsonRpcRequest) -> RpcRes { let client_info = self.client_info(); Box::new( - self.transport(request.clone()) - .then(move |result| process_transport_result(result, client_info, request)), + self.transport(JsonRpcRequestEnum::Single(request.clone())) + .then(move |result| process_transport_single_result(result, client_info, request)), + ) + } +} + +pub trait JsonRpcBatchClient: JsonRpcClient { + /// Sends the RPC batch request. + /// Responses are guaranteed to be in the same order in which they were requested. + fn batch_rpc(&self, batch_requests: I) -> RpcRes> + where + I: IntoIterator, + T: DeserializeOwned + Send + 'static, + { + let requests: Vec<_> = batch_requests.into_iter().collect(); + if requests.is_empty() { + // Return an empty vector of results. + return Box::new(futures01::future::ok(Vec::new())); + } + self.send_batch_request(JsonRpcBatchRequest(requests)) + } + + /// Sends the given batch `request` to the remote and tries to decode the batch response into `Vec`. + /// Responses are guaranteed to be in the same order in which they were requested. + fn send_batch_request(&self, request: JsonRpcBatchRequest) -> RpcRes> { + try_fu!(self.validate_batch_request(&request)); + let client_info = self.client_info(); + Box::new( + self.transport(JsonRpcRequestEnum::Batch(request.clone())) + .then(move |result| process_transport_batch_result(result, client_info, request)), ) } + + /// Validates the given batch requests if they all have unique IDs. + fn validate_batch_request(&self, request: &JsonRpcBatchRequest) -> Result<(), JsonRpcError> { + if request.orig_sequence_ids().all_unique() { + return Ok(()); + } + Err(JsonRpcError { + client_info: self.client_info(), + request: request.clone().into(), + error: JsonRpcErrorType::InvalidRequest(ERRL!("Each request in a batch must have a unique ID")), + }) + } } /// The trait is used when the rpc client instance has more than one remote endpoints. pub trait JsonRpcMultiClient: JsonRpcClient { - fn transport_exact(&self, to_addr: String, request: JsonRpcRequest) -> JsonRpcResponseFut; + /// Sends the given `request` to the specified `to_addr` remote. + /// Returns either an address `JsonRpcRemoteAddr` of the responder and the `JsonRpcResponseEnum` response, + /// or a stringified error. + fn transport_exact(&self, to_addr: String, request: JsonRpcRequestEnum) -> JsonRpcResponseFut; + /// Sends the given single `request` to the specified `to_addr` remote and tries to decode the response into `T`. fn send_request_to( &self, to_addr: &str, @@ -149,19 +335,61 @@ pub trait JsonRpcMultiClient: JsonRpcClient { ) -> RpcRes { let client_info = self.client_info(); Box::new( - self.transport_exact(to_addr.to_owned(), request.clone()) - .then(move |result| process_transport_result(result, client_info, request)), + self.transport_exact(to_addr.to_owned(), JsonRpcRequestEnum::Single(request.clone())) + .then(move |result| process_transport_single_result(result, client_info, request)), ) } } -fn process_transport_result( - result: Result<(JsonRpcRemoteAddr, JsonRpcResponse), String>, +/// Checks if the given `result` is success and contains `JsonRpcResponse`. +/// Tries to decode the batch response into `T`. +fn process_transport_single_result( + result: Result<(JsonRpcRemoteAddr, JsonRpcResponseEnum), String>, client_info: String, request: JsonRpcRequest, ) -> Result { - let (remote_addr, response) = match result { - Ok(r) => r, + let request = JsonRpcRequestEnum::Single(request); + + match result { + Ok((remote_addr, JsonRpcResponseEnum::Single(single))) => { + process_single_response(client_info, remote_addr, request, single) + }, + Ok((remote_addr, JsonRpcResponseEnum::Batch(batch))) => { + let error = ERRL!("Expeced single response, found batch response: {:?}", batch); + Err(JsonRpcError { + client_info, + request, + error: JsonRpcErrorType::Parse(remote_addr, error), + }) + }, + Err(e) => Err(JsonRpcError { + client_info, + request, + error: JsonRpcErrorType::Transport(e), + }), + } +} + +/// Checks if the given `result` is success and contains `JsonRpcBatchResponse`. +/// Tries to decode the batch response into `Vec` in the same order in which they were requested. +fn process_transport_batch_result( + result: Result<(JsonRpcRemoteAddr, JsonRpcResponseEnum), String>, + client_info: String, + request: JsonRpcBatchRequest, +) -> Result, JsonRpcError> { + let orig_ids: Vec<_> = request.orig_sequence_ids().collect(); + let request = JsonRpcRequestEnum::Batch(request); + + let (remote_addr, batch) = match result { + Ok((remote_addr, JsonRpcResponseEnum::Batch(batch))) => (remote_addr, batch), + Ok((remote_addr, JsonRpcResponseEnum::Single(single))) => { + let error = ERRL!("Expected batch response, found single response: {:?}", single); + return Err(JsonRpcError { + client_info, + request, + error: JsonRpcErrorType::Parse(remote_addr, error), + }); + }, Err(e) => { return Err(JsonRpcError { client_info, @@ -171,6 +399,54 @@ fn process_transport_result( }, }; + // Turn the vector of responses into a hashmap by their IDs to get quick access to the content of the responses. + let mut response_map: HashMap = + batch.into_iter().map(|res| (res.id.clone(), res)).collect(); + if response_map.len() != orig_ids.len() { + let error = ERRL!( + "Expected '{}' elements in batch response, found '{}'", + orig_ids.len(), + response_map.len() + ); + return Err(JsonRpcError { + client_info, + request, + error: JsonRpcErrorType::Parse(remote_addr, error), + }); + } + + let mut result = Vec::with_capacity(orig_ids.len()); + for id in orig_ids.iter() { + let single_resp = match response_map.remove(id) { + Some(res) => res, + None => { + let error = ERRL!("Batch response doesn't contain '{}' identifier", id); + return Err(JsonRpcError { + client_info, + request, + error: JsonRpcErrorType::Parse(remote_addr, error), + }); + }, + }; + + result.push(process_single_response( + client_info.clone(), + remote_addr.clone(), + request.clone(), + single_resp, + )?); + } + Ok(result) +} + +/// Tries to decode the given single `response` into `T` if it doesn't contain an error, +/// otherwise returns `JsonRpcError`. +fn process_single_response( + client_info: String, + remote_addr: JsonRpcRemoteAddr, + request: JsonRpcRequestEnum, + response: JsonRpcResponse, +) -> Result { if !response.error.is_null() { return Err(JsonRpcError { client_info, diff --git a/mm2src/docker_tests.rs b/mm2src/docker_tests.rs index f4f9b39c67..ef464be54e 100644 --- a/mm2src/docker_tests.rs +++ b/mm2src/docker_tests.rs @@ -111,7 +111,7 @@ mod docker_tests { use coins::utxo::slp::{slp_genesis_output, SlpOutput}; use coins::utxo::utxo_common::send_outputs_from_my_address; use coins::utxo::utxo_standard::{utxo_standard_coin_with_priv_key, UtxoStandardCoin}; - use coins::utxo::{dhash160, UtxoActivationParams, UtxoCommonOps}; + use coins::utxo::{dhash160, GetUtxoListOps, UtxoActivationParams, UtxoCommonOps}; use coins::{CoinProtocol, FoundSwapTxSpend, MarketCoinOps, MmCoin, SwapOps, Transaction, TransactionEnum, WithdrawRequest}; use common::for_tests::{check_my_swap_status_amounts, enable_electrum}; @@ -3233,7 +3233,7 @@ mod docker_tests { thread::sleep(Duration::from_secs(2)); let (unspents, _) = - block_on(coin.list_unspent_ordered(&coin.as_ref().derivation_method.unwrap_iguana())).unwrap(); + block_on(coin.get_unspent_ordered_list(&coin.as_ref().derivation_method.unwrap_iguana())).unwrap(); assert_eq!(unspents.len(), 1); } @@ -3291,7 +3291,7 @@ mod docker_tests { thread::sleep(Duration::from_secs(2)); let (unspents, _) = - block_on(coin.list_unspent_ordered(&coin.as_ref().derivation_method.unwrap_iguana())).unwrap(); + block_on(coin.get_unspent_ordered_list(&coin.as_ref().derivation_method.unwrap_iguana())).unwrap(); // 4 utxos are merged of 5 so the resulting unspents len must be 2 assert_eq!(unspents.len(), 2); } diff --git a/mm2src/lp_ordermatch/my_orders_storage.rs b/mm2src/lp_ordermatch/my_orders_storage.rs index f37d7658d7..523a5abddf 100644 --- a/mm2src/lp_ordermatch/my_orders_storage.rs +++ b/mm2src/lp_ordermatch/my_orders_storage.rs @@ -212,6 +212,8 @@ mod native_impl { my_taker_order_file_path, my_taker_orders_dir}; use common::fs::{read_dir_json, read_json, remove_file_async, write_json, FsJsonError}; + const USE_TMP_FILE: bool = false; + impl From for MyOrdersError { fn from(fs: FsJsonError) -> Self { match fs { @@ -255,13 +257,13 @@ mod native_impl { async fn save_new_active_maker_order(&self, order: &MakerOrder) -> MyOrdersResult<()> { let path = my_maker_order_file_path(&self.ctx, &order.uuid); - write_json(order, &path).await?; + write_json(order, &path, USE_TMP_FILE).await?; Ok(()) } async fn save_new_active_taker_order(&self, order: &TakerOrder) -> MyOrdersResult<()> { let path = my_taker_order_file_path(&self.ctx, &order.request.uuid); - write_json(order, &path).await?; + write_json(order, &path, USE_TMP_FILE).await?; Ok(()) } @@ -294,7 +296,7 @@ mod native_impl { impl MyOrdersHistory for MyOrdersStorage { async fn save_order_in_history(&self, order: &Order) -> MyOrdersResult<()> { let path = my_order_history_file_path(&self.ctx, &order.uuid()); - write_json(order, &path).await?; + write_json(order, &path, USE_TMP_FILE).await?; Ok(()) } diff --git a/mm2src/lp_swap/saved_swap.rs b/mm2src/lp_swap/saved_swap.rs index 3b8263ad2f..0f525fe2cc 100644 --- a/mm2src/lp_swap/saved_swap.rs +++ b/mm2src/lp_swap/saved_swap.rs @@ -176,6 +176,8 @@ mod native_impl { use crate::mm2::lp_swap::{my_swap_file_path, my_swaps_dir}; use common::fs::{read_dir_json, read_json, write_json, FsJsonError}; + const USE_TMP_FILE: bool = false; + impl From for SavedSwapError { fn from(fs: FsJsonError) -> Self { match fs { @@ -223,7 +225,7 @@ mod native_impl { async fn save_to_db(&self, ctx: &MmArc) -> SavedSwapResult<()> { let path = my_swap_file_path(ctx, self.uuid()); - write_json(self, &path).await?; + write_json(self, &path, USE_TMP_FILE).await?; Ok(()) } @@ -232,11 +234,11 @@ mod native_impl { match self { SavedSwap::Maker(maker) => { let path = stats_maker_swap_file_path(ctx, &maker.uuid); - write_json(self, &path).await?; + write_json(self, &path, USE_TMP_FILE).await?; }, SavedSwap::Taker(taker) => { let path = stats_taker_swap_file_path(ctx, &taker.uuid); - write_json(self, &path).await?; + write_json(self, &path, USE_TMP_FILE).await?; }, } Ok(()) diff --git a/mm2src/mm2_bitcoin/chain/src/transaction.rs b/mm2src/mm2_bitcoin/chain/src/transaction.rs index 91b90a3897..7219c9707d 100644 --- a/mm2src/mm2_bitcoin/chain/src/transaction.rs +++ b/mm2src/mm2_bitcoin/chain/src/transaction.rs @@ -21,7 +21,7 @@ const WITNESS_FLAG: u8 = 1; /// Maximum supported list size (inputs, outputs, etc.) const MAX_LIST_SIZE: usize = 8192; -#[derive(Debug, PartialEq, Eq, Hash, Clone, Default, Serializable, Deserializable)] +#[derive(Clone, Copy, Debug, Default, Deserializable, Eq, Hash, PartialEq, Serializable)] pub struct OutPoint { pub hash: H256, pub index: u32, diff --git a/mm2src/mm2_bitcoin/script/src/sign.rs b/mm2src/mm2_bitcoin/script/src/sign.rs index 3231d8cacb..8a654576ad 100644 --- a/mm2src/mm2_bitcoin/script/src/sign.rs +++ b/mm2src/mm2_bitcoin/script/src/sign.rs @@ -268,7 +268,7 @@ impl TransactionInputSigner { let unsigned_input = &self.inputs[input_index]; TransactionInput { - previous_output: unsigned_input.previous_output.clone(), + previous_output: unsigned_input.previous_output, sequence: unsigned_input.sequence, script_sig: script_sig.to_bytes(), script_witness: vec![], @@ -301,7 +301,7 @@ impl TransactionInputSigner { let inputs = if sighash.anyone_can_pay { let input = &self.inputs[input_index]; vec![TransactionInput { - previous_output: input.previous_output.clone(), + previous_output: input.previous_output, script_sig: script_pubkey.to_bytes(), sequence: input.sequence, script_witness: vec![], @@ -311,7 +311,7 @@ impl TransactionInputSigner { .iter() .enumerate() .map(|(n, input)| TransactionInput { - previous_output: input.previous_output.clone(), + previous_output: input.previous_output, script_sig: if n == input_index { script_pubkey.to_bytes() } else { diff --git a/mm2src/rpc/dispatcher/dispatcher.rs b/mm2src/rpc/dispatcher/dispatcher.rs index 6a88c69c93..c37a4b8471 100644 --- a/mm2src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/rpc/dispatcher/dispatcher.rs @@ -7,12 +7,13 @@ use crate::{mm2::lp_stats::{add_node_to_version_stat, remove_node_from_version_s stop_version_stat_collection, update_version_stat_collection}, mm2::lp_swap::{recreate_swap_data, trade_preimage_rpc}, mm2::rpc::lp_commands::{get_public_key, get_public_key_hash}}; -use coins::coin_balance::{account_balance, scan_for_new_addresses}; use coins::hd_wallet::get_new_address; -use coins::init_create_account::{init_create_new_account, init_create_new_account_status, - init_create_new_account_user_action}; -use coins::init_withdraw::{init_withdraw, withdraw_status, withdraw_user_action}; use coins::my_tx_history_v2::my_tx_history_v2_rpc; +use coins::rpc_command::account_balance::account_balance; +use coins::rpc_command::init_create_account::{init_create_new_account, init_create_new_account_status, + init_create_new_account_user_action}; +use coins::rpc_command::init_scan_for_new_addresses::{init_scan_for_new_addresses, init_scan_for_new_addresses_status}; +use coins::rpc_command::init_withdraw::{init_withdraw, withdraw_status, withdraw_user_action}; use coins::utxo::bch::BchCoin; use coins::utxo::qtum::QtumCoin; use coins::utxo::slp::SlpToken; @@ -135,6 +136,8 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, init_standalone_coin::).await, "init_qtum_status" => handle_mmrpc(ctx, request, init_standalone_coin_status::).await, "init_qtum_user_action" => handle_mmrpc(ctx, request, init_standalone_coin_user_action::).await, + "init_scan_for_new_addresses" => handle_mmrpc(ctx, request, init_scan_for_new_addresses).await, + "init_scan_for_new_addresses_status" => handle_mmrpc(ctx, request, init_scan_for_new_addresses_status).await, "init_trezor" => handle_mmrpc(ctx, request, init_trezor).await, "init_trezor_status" => handle_mmrpc(ctx, request, init_trezor_status).await, "init_trezor_user_action" => handle_mmrpc(ctx, request, init_trezor_user_action).await, @@ -149,14 +152,13 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, recreate_swap_data).await, "remove_delegation" => handle_mmrpc(ctx, request, remove_delegation).await, "remove_node_from_version_stat" => handle_mmrpc(ctx, request, remove_node_from_version_stat).await, - "scan_for_new_addresses" => handle_mmrpc(ctx, request, scan_for_new_addresses).await, "sign_message" => handle_mmrpc(ctx, request, sign_message).await, "start_simple_market_maker_bot" => handle_mmrpc(ctx, request, start_simple_market_maker_bot).await, "start_version_stat_collection" => handle_mmrpc(ctx, request, start_version_stat_collection).await, "stop_simple_market_maker_bot" => handle_mmrpc(ctx, request, stop_simple_market_maker_bot).await, "stop_version_stat_collection" => handle_mmrpc(ctx, request, stop_version_stat_collection).await, - "update_version_stat_collection" => handle_mmrpc(ctx, request, update_version_stat_collection).await, "trade_preimage" => handle_mmrpc(ctx, request, trade_preimage_rpc).await, + "update_version_stat_collection" => handle_mmrpc(ctx, request, update_version_stat_collection).await, "verify_message" => handle_mmrpc(ctx, request, verify_message).await, "withdraw" => handle_mmrpc(ctx, request, withdraw).await, "withdraw_status" => handle_mmrpc(ctx, request, withdraw_status).await,