From 36392fdf1e36d290cec97181b85889bcae7abe8f Mon Sep 17 00:00:00 2001 From: "Sergey O. Boyko" <58207208+sergeyboyko0791@users.noreply.github.com> Date: Sun, 25 Sep 2022 01:51:58 +0700 Subject: [PATCH] Implement TX history V2 for UTXO coins activated with a Hardware wallet #964 (#1467) * Refactor `BchAndSlpTxHistory` making it generic over `Coin` type * Add `UtxoTxHistoryOps`, implement it for `BchCoin` * Remove `UtxoStandardOps` implementation for `BchCoin` * TODO implement `WaitForHistoryUpdateTrigger` state * Implement `UtxoTxHistoryOps` and `CoinWithTxHistoryV2` for `UtxoStandardCoin` * Add `taget: MyTxHistoryTarget` field to `my_tx_history` request * Add `utxo_common/utxo_tx_history_common.rs` for TX history related common impl * Add `HDWalletCoinOps::derive_known_addresses` * Refactor `GetTxHistoryFilters` by requiring to set `from_addresses` * Fix `tx_history_v2_tests` * Start history background fetching on UTXO/QTUM coin initialization * Implement `UtxoTxHistoryOps` and `CoinWithTxHistoryV2` for `QtumCoin` * Implement * Implement `WaitForHistoryUpdateTrigger` state * Add `ElectrumClient::scripthash_get_history_batch` * Implement `UtxoTxHistoryOps::request_tx_history` according to `DerivationMethod` * Add `CoinBalanceReportOps` trait * Don't spawn legacy tx_history loop on `init_utxo`, `init_qtum` RPCs * Rename `EnableCoinBalance` to `CoinBalanceReport` * Add `UtxoTxHistoryV2::my_addresses` to optimize `tx_details_by_hash` * Rename `utxo_tx_history_common.rs` to `utxo_tx_history_v2_common.rs` * Implement `UtxoTxHistoryOps::tx_details_by_hash` optimized for TX history V2 * Add `UtxoTxHistoryOps::tx_from_storage_or_rpc` * Fix `CoinWithTxHistoryV2::get_tx_history_filters`, `UtxoTxHistoryOps::request_tx_history` * Add `UtxoTxHistoryError` * Test `UtxoTxHistoryOps::tx_details_by_hash` along with `UtxoStandardOps::tx_details_by_hash` * Final refactoring * Add `UtxoMyAddressesHistoryError` for `UtxoTxHistoryOps::my_addresses` * Optimize `UtxoTxHistoryOps::request_tx_history` by passing `my_addresses` argument * Fix fmt * Fix `test_bch_and_slp_testnet_history` test * Fix compile error * Refactor BCH to fix `test_bch_and_slp_testnet_history` * Use `utxo_common::request_tx_history` for `BchCoin` instead of `utxo_tx_history_v2_common`'s * Fix PR issues * Optimize TX history states by specifying `FetchingTxHashes::fetch_for_addresses` * Rename `UtxoTxHistoryOps::get_addresses_balances` to `my_addresses_balances` * Add `UtxoTxHistoryOps::address_from_str` to parse addresses within `UtxoTxHistoryOps::my_addresses_balances` result * Add `AddrFromStrError` * Add `for_addresses` argument to the `unique_tx_hashes_num_in_history` function * Add `SqlQuery::count_distinct` * Add `for_addresses` argument to the `history_contains_unconfirmed_txes` and `get_unconfirmed_txes_from_history` functions * Minor changes * Add `test_hd_utxo_tx_history` native and WASM test * Fix `SqlTxHistoryStorage` to repeat the same ordering as in `compare_transaction_details` * Fix tests * Use `utxo_common::utxo_tx_history_v2_common::request_tx_history` within for BCH coin * Move `utxo_coin_fields_for_test` to `utxo_common_tests.rs` * Add `test_bch_and_slp_testnet_history` in WASM * Add `T_BCH_ELECTRUMS` listening on WSS port * Ignore `solana_prerequisites` test * Skip serializing of `ElectrumRpcRequest::protocol` if it's None * Add timeout to `wait_till_history_has_records` helper * Fix ETH tests * Fix `test_convert_segwit_address` and `test_validateaddress_segwit` --- mm2src/coins/coin_balance.rs | 97 ++- mm2src/coins/eth/eth_tests.rs | 27 +- .../MORTY_HD_tx_history_fixtures.json | 131 ++++ mm2src/coins/hd_wallet.rs | 34 +- .../coins/hd_wallet_storage/mock_storage.rs | 4 +- mm2src/coins/hd_wallet_storage/mod.rs | 7 +- mm2src/coins/lp_coins.rs | 42 +- mm2src/coins/my_tx_history_v2.rs | 93 ++- mm2src/coins/qrc20.rs | 6 +- mm2src/coins/rpc_command/get_new_address.rs | 1 + .../hd_account_balance_rpc_error.rs | 1 + mm2src/coins/solana/solana_tests.rs | 2 + mm2src/coins/tx_history_storage/mod.rs | 32 +- .../sql_tx_history_storage_v2.rs | 195 +++-- .../tx_history_storage/tx_history_v2_tests.rs | 170 ++-- .../wasm/tx_history_storage_v2.rs | 91 +-- mm2src/coins/utxo.rs | 29 +- mm2src/coins/utxo/bch.rs | 275 +++---- mm2src/coins/utxo/bch_and_slp_tx_history.rs | 406 ---------- mm2src/coins/utxo/qtum.rs | 71 +- mm2src/coins/utxo/rpc_clients.rs | 15 +- mm2src/coins/utxo/slp.rs | 17 +- .../utxo/utxo_builder/utxo_coin_builder.rs | 15 +- mm2src/coins/utxo/utxo_common.rs | 48 +- .../utxo_common/utxo_tx_history_v2_common.rs | 417 ++++++++++ mm2src/coins/utxo/utxo_common_tests.rs | 262 ++++++- mm2src/coins/utxo/utxo_standard.rs | 71 +- mm2src/coins/utxo/utxo_tests.rs | 285 +++---- mm2src/coins/utxo/utxo_tx_history_v2.rs | 726 ++++++++++++++++++ mm2src/coins/utxo/utxo_wasm_tests.rs | 6 + mm2src/coins/utxo/utxo_withdraw.rs | 4 +- mm2src/coins/z_coin.rs | 9 +- mm2src/coins_activation/Cargo.toml | 8 +- .../src/bch_with_tokens_activation.rs | 2 +- mm2src/coins_activation/src/prelude.rs | 5 + .../standalone_coin/init_standalone_coin.rs | 32 +- .../src/utxo_activation/common_impl.rs | 38 +- .../utxo_activation/init_qtum_activation.rs | 22 +- .../init_utxo_standard_activation.rs | 23 +- .../init_utxo_standard_activation_error.rs | 9 + .../utxo_standard_activation_result.rs | 14 +- .../coins_activation/src/z_coin_activation.rs | 36 +- mm2src/db_common/src/sql_query.rs | 11 + mm2src/mm2_main/src/mm2_tests.rs | 10 +- .../src/mm2_tests/bch_and_slp_tests.rs | 83 +- mm2src/mm2_main/src/mm2_tests/structs.rs | 20 +- mm2src/mm2_test_helpers/src/for_tests.rs | 32 +- 47 files changed, 2906 insertions(+), 1028 deletions(-) create mode 100644 mm2src/coins/for_tests/MORTY_HD_tx_history_fixtures.json delete mode 100644 mm2src/coins/utxo/bch_and_slp_tx_history.rs create mode 100644 mm2src/coins/utxo/utxo_common/utxo_tx_history_v2_common.rs create mode 100644 mm2src/coins/utxo/utxo_tx_history_v2.rs diff --git a/mm2src/coins/coin_balance.rs b/mm2src/coins/coin_balance.rs index 4e361509c7..305ead0ce7 100644 --- a/mm2src/coins/coin_balance.rs +++ b/mm2src/coins/coin_balance.rs @@ -1,5 +1,6 @@ use crate::hd_pubkey::HDXPubExtractor; -use crate::hd_wallet::{HDAddressId, HDWalletCoinOps, NewAccountCreatingError, NewAddressDerivingError}; +use crate::hd_wallet::{HDAccountOps, HDAddressId, HDWalletCoinOps, HDWalletOps, NewAccountCreatingError, + NewAddressDerivingError}; use crate::{BalanceError, BalanceResult, CoinBalance, CoinWithDerivationMethod, DerivationMethod, HDAddress, MarketCoinOps}; use async_trait::async_trait; @@ -7,9 +8,11 @@ use common::log::{debug, info}; use crypto::{Bip44Chain, RpcDerivationPath}; use futures::compat::Future01CompatExt; use mm2_err_handle::prelude::*; +use mm2_number::BigDecimal; #[cfg(test)] use mocktopus::macros::*; -use std::fmt; +use std::collections::HashMap; use std::ops::Range; +use std::{fmt, iter}; pub type AddressIdRange = Range; @@ -33,11 +36,32 @@ impl From for EnableCoinBalanceError { #[derive(Clone, Debug, PartialEq, Serialize)] #[serde(tag = "wallet_type")] -pub enum EnableCoinBalance { +pub enum CoinBalanceReport { Iguana(IguanaWalletBalance), HD(HDWalletBalance), } +impl CoinBalanceReport { + /// Returns a map where the key is address, and the value is the address's total balance [`CoinBalance::total`]. + pub fn to_addresses_total_balances(&self) -> HashMap { + match self { + CoinBalanceReport::Iguana(IguanaWalletBalance { + ref address, + ref balance, + }) => iter::once((address.clone(), balance.get_total())).collect(), + CoinBalanceReport::HD(HDWalletBalance { ref accounts }) => accounts + .iter() + .flat_map(|account_balance| { + account_balance + .addresses + .iter() + .map(|addr_balance| (addr_balance.address.clone(), addr_balance.balance.get_total())) + }) + .collect(), + } + } +} + #[derive(Clone, Debug, PartialEq, Serialize)] pub struct IguanaWalletBalance { pub address: String, @@ -88,13 +112,48 @@ pub struct EnabledCoinBalanceParams { pub min_addresses_number: Option, } +#[async_trait] +pub trait CoinBalanceReportOps { + async fn coin_balance_report(&self) -> BalanceResult; +} + +#[async_trait] +impl CoinBalanceReportOps for Coin +where + Coin: CoinWithDerivationMethod::HDWallet> + + HDWalletBalanceOps + + MarketCoinOps + + Sync, + ::Address: fmt::Display + Sync, +{ + async fn coin_balance_report(&self) -> BalanceResult { + match self.derivation_method() { + DerivationMethod::Iguana(my_address) => self + .my_balance() + .compat() + .await + .map(|balance| { + CoinBalanceReport::Iguana(IguanaWalletBalance { + address: my_address.to_string(), + balance, + }) + }) + .mm_err(BalanceError::from), + DerivationMethod::HDWallet(hd_wallet) => self + .all_accounts_balances(hd_wallet) + .await + .map(|accounts| CoinBalanceReport::HD(HDWalletBalance { accounts })), + } + } +} + #[async_trait] pub trait EnableCoinBalanceOps { async fn enable_coin_balance( &self, xpub_extractor: &XPubExtractor, params: EnabledCoinBalanceParams, - ) -> MmResult + ) -> MmResult where XPubExtractor: HDXPubExtractor + Sync; } @@ -112,7 +171,7 @@ where &self, xpub_extractor: &XPubExtractor, params: EnabledCoinBalanceParams, - ) -> MmResult + ) -> MmResult where XPubExtractor: HDXPubExtractor + Sync, { @@ -122,7 +181,7 @@ where .compat() .await .map(|balance| { - EnableCoinBalance::Iguana(IguanaWalletBalance { + CoinBalanceReport::Iguana(IguanaWalletBalance { address: my_address.to_string(), balance, }) @@ -131,7 +190,7 @@ where DerivationMethod::HDWallet(hd_wallet) => self .enable_hd_wallet(hd_wallet, xpub_extractor, params) .await - .map(EnableCoinBalance::HD), + .map(CoinBalanceReport::HD), } } } @@ -164,6 +223,30 @@ pub trait HDWalletBalanceOps: HDWalletCoinOps { gap_limit: u32, ) -> BalanceResult>; + /// Requests balances of every activated HD account. + async fn all_accounts_balances(&self, hd_wallet: &Self::HDWallet) -> BalanceResult> { + let accounts = hd_wallet.get_accounts().await; + + let mut result = Vec::with_capacity(accounts.len()); + for (_account_id, hd_account) in accounts { + let addresses = self.all_known_addresses_balances(&hd_account).await?; + + let total_balance = addresses.iter().fold(CoinBalance::default(), |total, addr_balance| { + total + addr_balance.balance.clone() + }); + let account_balance = HDAccountBalance { + account_index: hd_account.account_id(), + derivation_path: RpcDerivationPath(hd_account.account_derivation_path()), + total_balance, + addresses, + }; + + result.push(account_balance); + } + + Ok(result) + } + /// Requests balances of every known addresses of the given `hd_account`. async fn all_known_addresses_balances(&self, hd_account: &Self::HDAccount) -> BalanceResult>; diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index b3ba38129c..4cd66b4457 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -3,6 +3,7 @@ use common::block_on; use mm2_core::mm_ctx::{MmArc, MmCtxBuilder}; use mocktopus::mocking::*; +const ETH_MAINNET_NODE: &str = "https://mainnet.infura.io/v3/c01c1b4cf66642528547624e1d6d9d6b"; /// The gas price for the tests const GAS_PRICE: u64 = 50_000_000_000; // `GAS_PRICE` increased by 3% @@ -976,7 +977,7 @@ fn test_get_fee_to_send_taker_fee_insufficient_balance() { platform: "ETH".to_string(), token_addr: Address::from("0xaD22f63404f7305e4713CcBd4F296f34770513f4"), }, - vec!["http://eth1.cipig.net:8555".into()], + vec![ETH_MAINNET_NODE.into()], None, ); let dex_fee_amount = u256_to_big_decimal(DEX_FEE_AMOUNT.into(), 18).expect("!u256_to_big_decimal"); @@ -992,11 +993,7 @@ fn test_get_fee_to_send_taker_fee_insufficient_balance() { #[test] fn validate_dex_fee_invalid_sender_eth() { - let (_ctx, coin) = eth_coin_for_test( - EthCoinType::Eth, - vec!["https://mainnet.infura.io/v3/c01c1b4cf66642528547624e1d6d9d6b".into()], - None, - ); + let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, vec![ETH_MAINNET_NODE.into()], None); // the real dex fee sent on mainnet // https://etherscan.io/tx/0x7e9ca16c85efd04ee5e31f2c1914b48f5606d6f9ce96ecce8c96d47d6857278f let tx = coin @@ -1031,7 +1028,7 @@ fn validate_dex_fee_invalid_sender_erc() { platform: "ETH".to_string(), token_addr: "0xa1d6df714f91debf4e0802a542e13067f31b8262".into(), }, - vec!["http://eth1.cipig.net:8555".into()], + vec![ETH_MAINNET_NODE.into()], None, ); // the real dex fee sent on mainnet @@ -1072,11 +1069,7 @@ fn sender_compressed_pub(tx: &SignedEthTx) -> [u8; 33] { #[test] fn validate_dex_fee_eth_confirmed_before_min_block() { - let (_ctx, coin) = eth_coin_for_test( - EthCoinType::Eth, - vec!["https://mainnet.infura.io/v3/c01c1b4cf66642528547624e1d6d9d6b".into()], - None, - ); + let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, vec![ETH_MAINNET_NODE.into()], None); // the real dex fee sent on mainnet // https://etherscan.io/tx/0x7e9ca16c85efd04ee5e31f2c1914b48f5606d6f9ce96ecce8c96d47d6857278f let tx = coin @@ -1113,7 +1106,7 @@ fn validate_dex_fee_erc_confirmed_before_min_block() { platform: "ETH".to_string(), token_addr: "0xa1d6df714f91debf4e0802a542e13067f31b8262".into(), }, - vec!["http://eth1.cipig.net:8555".into()], + vec![ETH_MAINNET_NODE.into()], None, ); // the real dex fee sent on mainnet @@ -1148,7 +1141,7 @@ fn validate_dex_fee_erc_confirmed_before_min_block() { #[test] fn test_negotiate_swap_contract_addr_no_fallback() { - let (_, coin) = eth_coin_for_test(EthCoinType::Eth, vec!["http://eth1.cipig.net:8555".into()], None); + let (_, coin) = eth_coin_for_test(EthCoinType::Eth, vec![ETH_MAINNET_NODE.into()], None); let input = None; let error = coin.negotiate_swap_contract_addr(input).unwrap_err().into_inner(); @@ -1177,11 +1170,7 @@ fn test_negotiate_swap_contract_addr_no_fallback() { fn test_negotiate_swap_contract_addr_has_fallback() { let fallback = "0x8500AFc0bc5214728082163326C2FF0C73f4a871".into(); - let (_, coin) = eth_coin_for_test( - EthCoinType::Eth, - vec!["http://eth1.cipig.net:8555".into()], - Some(fallback), - ); + let (_, coin) = eth_coin_for_test(EthCoinType::Eth, vec![ETH_MAINNET_NODE.into()], Some(fallback)); let input = None; let result = coin.negotiate_swap_contract_addr(input).unwrap(); diff --git a/mm2src/coins/for_tests/MORTY_HD_tx_history_fixtures.json b/mm2src/coins/for_tests/MORTY_HD_tx_history_fixtures.json new file mode 100644 index 0000000000..1d35ebcfaa --- /dev/null +++ b/mm2src/coins/for_tests/MORTY_HD_tx_history_fixtures.json @@ -0,0 +1,131 @@ +[ + { + "tx_hex": "0400008085202f890189cb3d4e9d36a7c2c6bb1b76dcfc1ee7c9d3d83b364c50a3f3a791c5efb6f392030000006b483045022100a0b910ecbf5ed1c473507c3e1a5a06ad612c982d3204d2dc066bfbbeb39c5e400220559a82d151cbcdcb097786127f499863e0864662519a9caf758c929f1659bc34012102d09f2cb1693be9c0ea73bb48d45ce61805edd1c43590681b02f877206078a5b3ffffffff0400e1f505000000001976a914ab19b1f2bd2337a58c1b5d198468951ac42d796788ac00c2eb0b000000001976a914ab19b1f2bd2337a58c1b5d198468951ac42d796788aca01f791c000000001976a914ab19b1f2bd2337a58c1b5d198468951ac42d796788ac3091cce2e80000001976a91490a0d8ba62c339ade97a14e81b6f531de03fdbb288ac00000000000000000000000000000000000000", + "tx_hash": "6ca27dd058b939c98a33625b9f68eaeebca5a3058aec062647ca6fd7634bb339", + "from": [ + "RNTv4xTLLm26p3SvsQCBy9qNK7s1RgGYSB" + ], + "to": [ + "RNTv4xTLLm26p3SvsQCBy9qNK7s1RgGYSB", + "RQstQeTUEZLh6c3YWJDkeVTTQoZUsfvNCr" + ], + "total_amount": "10010.1518", + "spent_by_me": "0", + "received_by_me": "7.777", + "my_balance_change": "7.777", + "block_height": 1629306, + "timestamp": 1663619097, + "fee_details": { + "type": "Utxo", + "coin": "MORTY", + "amount": "0.0001" + }, + "coin": "MORTY", + "internal_id": "6ca27dd058b939c98a33625b9f68eaeebca5a3058aec062647ca6fd7634bb339", + "transaction_type": "StandardTransfer", + "confirmations": 4 + }, + { + "tx_hex": "0400008085202f89011b8746195a7e80172d948e1eb7f2d6710bd2ecbe7750e653bb8d345b940da55b030000006a4730440220482c7a7762977ed3a8afcf751b7413d5ef978f604e550eb4fd35199e1f4d52400220486fb4f3831a7f512dba1271a4f793e7050ea123bcb4922e3848bb18c7cac4c4012102d09f2cb1693be9c0ea73bb48d45ce61805edd1c43590681b02f877206078a5b3ffffffff0400e1f505000000001976a914bd658bdd369c8bb98ac837071639819e5f8dd3cb88ac00c2eb0b000000001976a914bd658bdd369c8bb98ac837071639819e5f8dd3cb88aca01f791c000000001976a914bd658bdd369c8bb98ac837071639819e5f8dd3cb88acf037389ce90000001976a91490a0d8ba62c339ade97a14e81b6f531de03fdbb288ac00000000000000000000000000000000000000", + "tx_hash": "70c62f42d65f9d71a8fb7f4560057b80dc2ecd9e4990621323faf1de9a53ca97", + "from": [ + "RNTv4xTLLm26p3SvsQCBy9qNK7s1RgGYSB" + ], + "to": [ + "RNTv4xTLLm26p3SvsQCBy9qNK7s1RgGYSB", + "RSYdSLRYWuzBson2GDbWBa632q2PmFnCaH" + ], + "total_amount": "10041.2602", + "spent_by_me": "0", + "received_by_me": "7.777", + "my_balance_change": "7.777", + "block_height": 1617702, + "timestamp": 1662914954, + "fee_details": { + "type": "Utxo", + "coin": "MORTY", + "amount": "0.0001" + }, + "coin": "MORTY", + "internal_id": "70c62f42d65f9d71a8fb7f4560057b80dc2ecd9e4990621323faf1de9a53ca97", + "transaction_type": "StandardTransfer", + "confirmations": 11603 + }, + { + "tx_hex": "0400008085202f890175fb4250e95dc0c89cd89a9487324aa8dd7a2f8fd8581fc90881567ca6be02bf000000006b483045022100be3e58c5d4dbe5ea35ab831d610b42bb1ae01fc0df1786f11cbe6969d8d45c6302207b8747b4012a6c4aefecf670eaf8d787445909ebd7775b85c77d723e9d6445a8012103f7f831c6fbe62b987e4b2f455e5b7f27375cf59b57eb3ffa9e122b2c9b395f6bffffffff0200e1f505000000001976a914c23136f831b15dd4522eb1c6eb4c5cd3abbfbe3b88aca8b66428000000001976a914bd658bdd369c8bb98ac837071639819e5f8dd3cb88ac00000000000000000000000000000000000000", + "tx_hash": "bd031dc681cdc63491fd71902c5960985127b04eb02211a1049bff0d0c8ebce3", + "from": [ + "RPj9JXUVnewWwVpxZDeqGB25qVqz5qJzwP" + ], + "to": [ + "RSYdSLRYWuzBson2GDbWBa632q2PmFnCaH", + "RSyz8EJaTzhkT6uZinAtHhFu5bfsvqcLqg" + ], + "total_amount": "7.77699", + "spent_by_me": "7.77699", + "received_by_me": "6.77689", + "my_balance_change": "-1.00010", + "block_height": 1499070, + "timestamp": 1655738171, + "fee_details": { + "type": "Utxo", + "coin": "MORTY", + "amount": "0.0001" + }, + "coin": "MORTY", + "internal_id": "bd031dc681cdc63491fd71902c5960985127b04eb02211a1049bff0d0c8ebce3", + "transaction_type": "StandardTransfer", + "confirmations": 130235 + }, + { + "tx_hex": "0400008085202f89038dea273bcc9194a80b19ea1a8ca076d3d00c590b4571808f10f3eee5aa38c07d000000006b483045022100ff31b7c36145dc9fa06346fe38b75b81273b41b7d236b900322154ba136799bb02204ddd253eb93e95c9d24f8e36a8a5a42022c63d6edd81c453f24cbb5db785037d012102d6a78b71b10459bd0757a614ca1eef62f4a65514225d10e95df31ee9cb23ffd5ffffffff8dea273bcc9194a80b19ea1a8ca076d3d00c590b4571808f10f3eee5aa38c07d010000006a47304402202d3e0fd3ce7b4753725adc0175a050c919f93b564682ffd8d65974c15996b3560220211b8120acb3ad80dbdd9160f639884f0e15c0c5703c86c069b3abe85cb6c4fd012102d6a78b71b10459bd0757a614ca1eef62f4a65514225d10e95df31ee9cb23ffd5ffffffff8dea273bcc9194a80b19ea1a8ca076d3d00c590b4571808f10f3eee5aa38c07d020000006b483045022100c788b064db34e961479393bad19d8c5f7d437b02c64c3172381fbe2b7d104efd022027167b2e5a6f29a7567e43b39a3d5b8f50c20767162ec29328067a6fcbd87409012102d6a78b71b10459bd0757a614ca1eef62f4a65514225d10e95df31ee9cb23ffd5ffffffff01b8be5a2e000000001976a9149e7a424abb2f341d655ce6af1143409edef4d55588acbdb1d661000000000000000000000000000000", + "tx_hash": "bf02bea67c568108c91f58d88f2f7adda84a3287949ad89cc8c05de95042fb75", + "from": [ + "RYM6yDMn8vdqtkYKLzY5dNe7p3T6YmMWvq" + ], + "to": [ + "RPj9JXUVnewWwVpxZDeqGB25qVqz5qJzwP" + ], + "total_amount": "7.777", + "spent_by_me": "7.777", + "received_by_me": "7.77699", + "my_balance_change": "-0.00001", + "block_height": 1263905, + "timestamp": 1641460263, + "fee_details": { + "type": "Utxo", + "coin": "MORTY", + "amount": "0.00001" + }, + "coin": "MORTY", + "internal_id": "bf02bea67c568108c91f58d88f2f7adda84a3287949ad89cc8c05de95042fb75", + "transaction_type": "StandardTransfer", + "confirmations": 365400 + }, + { + "tx_hex": "0400008085202f8901fe0ccc272929c811b20ab910f5478a99229a5304480b5897cf9507149a044c63030000006b483045022100c5b24fbd1ce11736760ebc0edccad6bde2dcbbff090528db4602a485a5ec645f02201340e5f818b9d7ab75e39abaacaa5d94c03fdb8e41698182eeff389a45c5ad15012102d09f2cb1693be9c0ea73bb48d45ce61805edd1c43590681b02f877206078a5b3ffffffff0400e1f505000000001976a914fd084ad97ae0313bba5717acedfba629b8bd426988ac00c2eb0b000000001976a914fd084ad97ae0313bba5717acedfba629b8bd426988aca01f791c000000001976a914fd084ad97ae0313bba5717acedfba629b8bd426988ac40053045260100001976a91490a0d8ba62c339ade97a14e81b6f531de03fdbb288ac00000000000000000000000000000000000000", + "tx_hash": "7dc038aae5eef3108f8071450b590cd0d376a08c1aea190ba89491cc3b27ea8d", + "from": [ + "RNTv4xTLLm26p3SvsQCBy9qNK7s1RgGYSB" + ], + "to": [ + "RNTv4xTLLm26p3SvsQCBy9qNK7s1RgGYSB", + "RYM6yDMn8vdqtkYKLzY5dNe7p3T6YmMWvq" + ], + "total_amount": "12646.5887", + "spent_by_me": "0", + "received_by_me": "7.777", + "my_balance_change": "7.777", + "block_height": 1263875, + "timestamp": 1641458818, + "fee_details": { + "type": "Utxo", + "coin": "MORTY", + "amount": "0.0001" + }, + "coin": "MORTY", + "internal_id": "7dc038aae5eef3108f8071450b590cd0d376a08c1aea190ba89491cc3b27ea8d", + "transaction_type": "StandardTransfer", + "confirmations": 365430 + } +] \ No newline at end of file diff --git a/mm2src/coins/hd_wallet.rs b/mm2src/coins/hd_wallet.rs index d92b9e0649..84be4564f9 100644 --- a/mm2src/coins/hd_wallet.rs +++ b/mm2src/coins/hd_wallet.rs @@ -18,16 +18,27 @@ pub type HDAccountsMutex = AsyncMutex>; pub type HDAccountsMut<'a, HDAccount> = AsyncMutexGuard<'a, HDAccountsMap>; pub type HDAccountMut<'a, HDAccount> = AsyncMappedMutexGuard<'a, HDAccountsMap, HDAccount>; +pub type AddressDerivingResult = MmResult; + const DEFAULT_ADDRESS_LIMIT: u32 = ChildNumber::HARDENED_FLAG; const DEFAULT_ACCOUNT_LIMIT: u32 = ChildNumber::HARDENED_FLAG; const DEFAULT_RECEIVER_CHAIN: Bip44Chain = Bip44Chain::External; #[derive(Debug, Display)] pub enum AddressDerivingError { + #[display(fmt = "Coin doesn't support the given BIP44 chain: {:?}", chain)] + InvalidBip44Chain { + chain: Bip44Chain, + }, + #[display(fmt = "BIP32 address deriving error: {}", _0)] Bip32Error(Bip32Error), Internal(String), } +impl From for AddressDerivingError { + fn from(e: InvalidBip44ChainError) -> Self { AddressDerivingError::InvalidBip44Chain { chain: e.chain } } +} + impl From for AddressDerivingError { fn from(e: Bip32Error) -> Self { AddressDerivingError::Bip32Error(e) } } @@ -39,7 +50,9 @@ impl From for BalanceError { impl From for WithdrawError { fn from(e: AddressDerivingError) -> Self { match e { - AddressDerivingError::Bip32Error(e) => WithdrawError::UnexpectedFromAddress(e.to_string()), + AddressDerivingError::InvalidBip44Chain { .. } | AddressDerivingError::Bip32Error(_) => { + WithdrawError::UnexpectedFromAddress(e.to_string()) + }, AddressDerivingError::Internal(internal) => WithdrawError::InternalError(internal), } } @@ -66,6 +79,7 @@ impl From for NewAddressDerivingError { impl From for NewAddressDerivingError { fn from(e: AddressDerivingError) -> Self { match e { + AddressDerivingError::InvalidBip44Chain { chain } => NewAddressDerivingError::InvalidBip44Chain { chain }, AddressDerivingError::Bip32Error(bip32) => NewAddressDerivingError::Bip32Error(bip32), AddressDerivingError::Internal(internal) => NewAddressDerivingError::Internal(internal), } @@ -169,7 +183,7 @@ pub struct HDAddress { pub derivation_path: DerivationPath, } -#[derive(Clone, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct HDAccountAddressId { pub account_id: u32, pub chain: Bip44Chain, @@ -205,7 +219,7 @@ pub trait HDWalletCoinOps { hd_account: &Self::HDAccount, chain: Bip44Chain, address_id: u32, - ) -> MmResult, AddressDerivingError> { + ) -> AddressDerivingResult> { self.derive_addresses(hd_account, std::iter::once(HDAddressId { chain, address_id })) .await? .into_iter() @@ -220,10 +234,22 @@ pub trait HDWalletCoinOps { &self, hd_account: &Self::HDAccount, address_ids: Ids, - ) -> MmResult>, AddressDerivingError> + ) -> AddressDerivingResult>> where Ids: Iterator + Send; + async fn derive_known_addresses( + &self, + hd_account: &Self::HDAccount, + chain: Bip44Chain, + ) -> AddressDerivingResult>> { + let known_addresses_number = hd_account.known_addresses_number(chain)?; + let address_ids = (0..known_addresses_number) + .into_iter() + .map(|address_id| HDAddressId { chain, address_id }); + self.derive_addresses(hd_account, address_ids).await + } + /// Generates a new address and updates the corresponding number of used `hd_account` addresses. async fn generate_new_address( &self, diff --git a/mm2src/coins/hd_wallet_storage/mock_storage.rs b/mm2src/coins/hd_wallet_storage/mock_storage.rs index 67e694ed44..2fbbc19f4c 100644 --- a/mm2src/coins/hd_wallet_storage/mock_storage.rs +++ b/mm2src/coins/hd_wallet_storage/mock_storage.rs @@ -1,12 +1,12 @@ use crate::hd_wallet_storage::{HDAccountStorageItem, HDWalletId, HDWalletStorageInternalOps, HDWalletStorageResult}; use async_trait::async_trait; use mm2_core::mm_ctx::MmArc; -use mocktopus::macros::*; +#[cfg(test)] use mocktopus::macros::*; pub struct HDWalletMockStorage; #[async_trait] -#[mockable] +#[cfg_attr(test, mockable)] impl HDWalletStorageInternalOps for HDWalletMockStorage { async fn init(_ctx: &MmArc) -> HDWalletStorageResult where diff --git a/mm2src/coins/hd_wallet_storage/mod.rs b/mm2src/coins/hd_wallet_storage/mod.rs index 95506f52fe..7d8582d782 100644 --- a/mm2src/coins/hd_wallet_storage/mod.rs +++ b/mm2src/coins/hd_wallet_storage/mod.rs @@ -14,8 +14,9 @@ use std::ops::Deref; #[cfg(not(target_arch = "wasm32"))] mod sqlite_storage; #[cfg(target_arch = "wasm32")] mod wasm_storage; -#[cfg(test)] mod mock_storage; -#[cfg(test)] pub use mock_storage::HDWalletMockStorage; +#[cfg(any(test, target_arch = "wasm32"))] mod mock_storage; +#[cfg(any(test, target_arch = "wasm32"))] +pub use mock_storage::HDWalletMockStorage; cfg_wasm32! { use wasm_storage::HDWalletIndexedDbStorage as HDWalletStorageInstance; @@ -208,7 +209,7 @@ impl fmt::Debug for HDWalletCoinStorage { } } -#[cfg(test)] +#[cfg(any(test, target_arch = "wasm32"))] impl Default for HDWalletCoinStorage { fn default() -> Self { HDWalletCoinStorage { diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index f2beb80e76..d7c6003a58 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -2377,9 +2377,13 @@ pub async fn lp_coininit(ctx: &MmArc, ticker: &str, req: &Json) -> Result Result<(), MmError> { - let RegisterCoinParams { ticker, tx_history } = params; + let RegisterCoinParams { ticker } = params; let cctx = CoinsContext::from_ctx(ctx).map_to_mm(RegisterCoinError::Internal)?; // TODO AP: locking the coins list during the entire initialization prevents different coins from being @@ -2414,11 +2417,8 @@ pub async fn lp_register_coin( RawEntryMut::Occupied(_oe) => { return MmError::err(RegisterCoinError::CoinIsInitializedAlready { coin: ticker.clone() }) }, - RawEntryMut::Vacant(ve) => ve.insert(ticker.clone(), coin.clone()), + RawEntryMut::Vacant(ve) => ve.insert(ticker.clone(), coin), }; - if tx_history { - lp_spawn_tx_history(ctx.clone(), coin).map_to_mm(RegisterCoinError::Internal)?; - } Ok(()) } @@ -2621,7 +2621,7 @@ pub async fn send_raw_transaction(ctx: MmArc, req: Json) -> Result Ordering { +pub(crate) fn compare_transaction_details(a: &TransactionDetails, b: &TransactionDetails) -> Ordering { + let a = TxIdHeight::new(a.block_height, a.internal_id.deref()); + let b = TxIdHeight::new(b.block_height, b.internal_id.deref()); + compare_transactions(a, b) +} + +pub(crate) struct TxIdHeight { + block_height: u64, + tx_id: Id, +} + +impl TxIdHeight { + pub(crate) fn new(block_height: u64, tx_id: Id) -> TxIdHeight { TxIdHeight { block_height, tx_id } } +} + +pub(crate) fn compare_transactions(a: TxIdHeight, b: TxIdHeight) -> Ordering +where + Id: Ord, +{ // the transactions with block_height == 0 are the most recent so we need to separately handle them while sorting if a.block_height == b.block_height { - a.internal_id.cmp(&b.internal_id) + a.tx_id.cmp(&b.tx_id) } else if a.block_height == 0 { Ordering::Less } else if b.block_height == 0 { diff --git a/mm2src/coins/my_tx_history_v2.rs b/mm2src/coins/my_tx_history_v2.rs index df49663e29..7e7c97e616 100644 --- a/mm2src/coins/my_tx_history_v2.rs +++ b/mm2src/coins/my_tx_history_v2.rs @@ -1,9 +1,12 @@ -use crate::tx_history_storage::{CreateTxHistoryStorageError, GetTxHistoryFilters, TxHistoryStorageBuilder, WalletId}; -use crate::{lp_coinfind_or_err, BlockHeightAndTime, CoinFindError, HistorySyncState, MmCoin, MmCoinEnum, Transaction, - TransactionDetails, TransactionType, TxFeeDetails, UtxoRpcError}; +use crate::hd_wallet::{AddressDerivingError, InvalidBip44ChainError}; +use crate::tx_history_storage::{CreateTxHistoryStorageError, FilteringAddresses, GetTxHistoryFilters, + TxHistoryStorageBuilder, WalletId}; +use crate::{lp_coinfind_or_err, BlockHeightAndTime, CoinFindError, HDAccountAddressId, HistorySyncState, MmCoin, + MmCoinEnum, Transaction, TransactionDetails, TransactionType, TxFeeDetails, UtxoRpcError}; use async_trait::async_trait; use bitcrypto::sha256; use common::{calc_total_pages, ten, HttpStatusCode, PagingOptionsEnum, StatusCode}; +use crypto::Bip44DerivationPath; use derive_more::Display; use futures::compat::Future01CompatExt; use keys::{Address, CashAddress}; @@ -67,12 +70,17 @@ pub trait TxHistoryStorage: Send + Sync + 'static { ) -> Result, MmError>; /// Returns whether the history contains unconfirmed transactions. - async fn history_contains_unconfirmed_txes(&self, wallet_id: &WalletId) -> Result>; + async fn history_contains_unconfirmed_txes( + &self, + wallet_id: &WalletId, + for_addresses: FilteringAddresses, + ) -> Result>; /// Gets the unconfirmed transactions from the wallet's history. async fn get_unconfirmed_txes_from_history( &self, wallet_id: &WalletId, + for_addresses: FilteringAddresses, ) -> Result, MmError>; /// Updates transaction in the selected wallet's history @@ -86,7 +94,11 @@ pub trait TxHistoryStorage: Send + Sync + 'static { async fn history_has_tx_hash(&self, wallet_id: &WalletId, tx_hash: &str) -> Result>; /// Returns the number of unique transaction hashes. - async fn unique_tx_hashes_num_in_history(&self, wallet_id: &WalletId) -> Result>; + async fn unique_tx_hashes_num_in_history( + &self, + wallet_id: &WalletId, + for_addresses: FilteringAddresses, + ) -> Result>; /// Adds the given `tx_hex` transaction to the selected wallet's cache. async fn add_tx_to_cache( @@ -228,13 +240,29 @@ impl<'a, Addr: Clone + DisplayAddress + Eq + std::hash::Hash, Tx: Transaction> T } } -#[derive(Deserialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(tag = "type")] +#[serde(rename_all = "snake_case")] +pub enum MyTxHistoryTarget { + Iguana, + AccountId { account_id: u32 }, + AddressId(HDAccountAddressId), + AddressDerivationPath(Bip44DerivationPath), +} + +impl Default for MyTxHistoryTarget { + fn default() -> Self { MyTxHistoryTarget::Iguana } +} + +#[derive(Clone, Deserialize)] pub struct MyTxHistoryRequestV2 { - coin: String, + pub(crate) coin: String, #[serde(default = "ten")] pub(crate) limit: usize, #[serde(default)] pub(crate) paging_options: PagingOptionsEnum, + #[serde(default)] + pub(crate) target: MyTxHistoryTarget, } #[derive(Serialize)] @@ -247,6 +275,7 @@ pub struct MyTxHistoryDetails { #[derive(Serialize)] pub struct MyTxHistoryResponseV2 { pub(crate) coin: String, + pub(crate) target: MyTxHistoryTarget, pub(crate) current_block: u64, pub(crate) transactions: Vec, pub(crate) sync_status: HistorySyncState, @@ -261,6 +290,7 @@ pub struct MyTxHistoryResponseV2 { #[serde(tag = "error_type", content = "error_data")] pub enum MyTxHistoryErrorV2 { CoinIsNotActive(String), + InvalidTarget(String), StorageIsNotInitialized(String), StorageError(String), RpcError(String), @@ -268,6 +298,12 @@ pub enum MyTxHistoryErrorV2 { Internal(String), } +impl MyTxHistoryErrorV2 { + pub fn with_expected_target(actual: MyTxHistoryTarget, expected: &str) -> MyTxHistoryErrorV2 { + MyTxHistoryErrorV2::InvalidTarget(format!("Expected {:?} target, found: {:?}", expected, actual)) + } +} + impl HttpStatusCode for MyTxHistoryErrorV2 { fn status_code(&self) -> StatusCode { match self { @@ -276,7 +312,7 @@ impl HttpStatusCode for MyTxHistoryErrorV2 { | MyTxHistoryErrorV2::StorageError(_) | MyTxHistoryErrorV2::RpcError(_) | MyTxHistoryErrorV2::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, - MyTxHistoryErrorV2::NotSupportedFor(_) => StatusCode::BAD_REQUEST, + MyTxHistoryErrorV2::NotSupportedFor(_) | MyTxHistoryErrorV2::InvalidTarget(_) => StatusCode::BAD_REQUEST, } } } @@ -304,10 +340,28 @@ impl From for MyTxHistoryErrorV2 { fn from(err: UtxoRpcError) -> Self { MyTxHistoryErrorV2::RpcError(err.to_string()) } } +impl From for MyTxHistoryErrorV2 { + fn from(e: AddressDerivingError) -> Self { + match e { + AddressDerivingError::InvalidBip44Chain { .. } => MyTxHistoryErrorV2::InvalidTarget(e.to_string()), + AddressDerivingError::Bip32Error(_) => MyTxHistoryErrorV2::Internal(e.to_string()), + AddressDerivingError::Internal(internal) => MyTxHistoryErrorV2::Internal(internal), + } + } +} + +impl From for MyTxHistoryErrorV2 { + fn from(e: InvalidBip44ChainError) -> Self { MyTxHistoryErrorV2::InvalidTarget(e.to_string()) } +} + +#[async_trait] pub trait CoinWithTxHistoryV2 { fn history_wallet_id(&self) -> WalletId; - fn get_tx_history_filters(&self) -> GetTxHistoryFilters; + async fn get_tx_history_filters( + &self, + target: MyTxHistoryTarget, + ) -> MmResult; } /// According to the [comment](https://github.com/KomodoPlatform/atomicDEX-API/pull/1285#discussion_r888410390), @@ -319,6 +373,8 @@ pub async fn my_tx_history_v2_rpc( match lp_coinfind_or_err(&ctx, &request.coin).await? { MmCoinEnum::Bch(bch) => my_tx_history_v2_impl(ctx, &bch, request).await, MmCoinEnum::SlpToken(slp_token) => my_tx_history_v2_impl(ctx, &slp_token, request).await, + MmCoinEnum::UtxoCoin(utxo) => my_tx_history_v2_impl(ctx, &utxo, request).await, + MmCoinEnum::QtumCoin(qtum) => my_tx_history_v2_impl(ctx, &qtum, request).await, other => MmError::err(MyTxHistoryErrorV2::NotSupportedFor(other.ticker().to_owned())), } } @@ -345,7 +401,7 @@ where .await .map_to_mm(MyTxHistoryErrorV2::RpcError)?; - let filters = coin.get_tx_history_filters(); + let filters = coin.get_tx_history_filters(request.target.clone()).await?; let history = tx_history_storage .get_history(&wallet_id, filters, request.paging_options.clone(), request.limit) .await?; @@ -369,6 +425,7 @@ where Ok(MyTxHistoryResponseV2 { coin: request.coin, + target: request.target, current_block, transactions, sync_status: coin.history_sync_status(), @@ -390,3 +447,19 @@ pub async fn z_coin_tx_history_rpc( other => MmError::err(MyTxHistoryErrorV2::NotSupportedFor(other.ticker().to_owned())), } } + +#[cfg(test)] +pub(crate) mod for_tests { + use super::{CoinWithTxHistoryV2, TxHistoryStorage}; + use crate::tx_history_storage::TxHistoryStorageBuilder; + use common::block_on; + use mm2_core::mm_ctx::MmArc; + use mm2_test_helpers::for_tests::mm_ctx_with_custom_db; + + pub fn init_storage_for(coin: &Coin) -> (MmArc, impl TxHistoryStorage) { + let ctx = mm_ctx_with_custom_db(); + let storage = TxHistoryStorageBuilder::new(&ctx).build().unwrap(); + block_on(storage.init(&coin.history_wallet_id())).unwrap(); + (ctx, storage) + } +} diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index d137e75db4..6a96c4fd3f 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -9,8 +9,8 @@ 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, GetUtxoListOps, - HistoryUtxoTx, HistoryUtxoTxMap, MatureUnspentList, RecentlySpentOutPointsGuard, +use crate::utxo::{qtum, ActualTxFee, AdditionalTxData, AddrFromStrError, 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, @@ -622,7 +622,7 @@ impl UtxoCommonOps for Qrc20Coin { utxo_common::my_public_key(self.as_ref()) } - fn address_from_str(&self, address: &str) -> Result { + fn address_from_str(&self, address: &str) -> MmResult { utxo_common::checked_address_from_str(self, address) } diff --git a/mm2src/coins/rpc_command/get_new_address.rs b/mm2src/coins/rpc_command/get_new_address.rs index c3483d7429..cda996c345 100644 --- a/mm2src/coins/rpc_command/get_new_address.rs +++ b/mm2src/coins/rpc_command/get_new_address.rs @@ -89,6 +89,7 @@ impl From for GetNewAddressRpcError { impl From for GetNewAddressRpcError { fn from(e: AddressDerivingError) -> Self { match e { + AddressDerivingError::InvalidBip44Chain { chain } => GetNewAddressRpcError::InvalidBip44Chain { chain }, AddressDerivingError::Bip32Error(bip32) => GetNewAddressRpcError::ErrorDerivingAddress(bip32.to_string()), AddressDerivingError::Internal(internal) => GetNewAddressRpcError::Internal(internal), } diff --git a/mm2src/coins/rpc_command/hd_account_balance_rpc_error.rs b/mm2src/coins/rpc_command/hd_account_balance_rpc_error.rs index b4cf56275a..4f0e836ffe 100644 --- a/mm2src/coins/rpc_command/hd_account_balance_rpc_error.rs +++ b/mm2src/coins/rpc_command/hd_account_balance_rpc_error.rs @@ -85,6 +85,7 @@ impl From for HDAccountBalanceRpcError { impl From for HDAccountBalanceRpcError { fn from(e: AddressDerivingError) -> Self { match e { + AddressDerivingError::InvalidBip44Chain { chain } => HDAccountBalanceRpcError::InvalidBip44Chain { chain }, AddressDerivingError::Bip32Error(bip32) => { HDAccountBalanceRpcError::ErrorDerivingAddress(bip32.to_string()) }, diff --git a/mm2src/coins/solana/solana_tests.rs b/mm2src/coins/solana/solana_tests.rs index afe2668556..5be1f290b9 100644 --- a/mm2src/coins/solana/solana_tests.rs +++ b/mm2src/coins/solana/solana_tests.rs @@ -28,7 +28,9 @@ fn solana_keypair_from_secp() { } // Research tests +// TODO remove `ignore` attribute once the test is stable. #[test] +#[ignore] #[cfg(not(target_arch = "wasm32"))] fn solana_prerequisites() { // same test as trustwallet diff --git a/mm2src/coins/tx_history_storage/mod.rs b/mm2src/coins/tx_history_storage/mod.rs index 9fca3dcc3d..2c23290eb9 100644 --- a/mm2src/coins/tx_history_storage/mod.rs +++ b/mm2src/coins/tx_history_storage/mod.rs @@ -52,8 +52,7 @@ impl<'a> TxHistoryStorageBuilder<'a> { } /// Whether transaction is unconfirmed or confirmed. -/// Serializes to either `0u8` or `1u8` correspondingly. -#[repr(u8)] +/// Serializes to either `0` or `1` correspondingly. #[derive(Clone, Copy, Debug)] pub enum ConfirmationStatus { Unconfirmed = 0, @@ -122,33 +121,38 @@ impl WalletId { } } -#[derive(Debug, Default)] +#[derive(Clone, Debug)] pub struct GetTxHistoryFilters { token_id: Option, - for_addresses: Option, + for_addresses: FilteringAddresses, } impl GetTxHistoryFilters { #[inline] - pub fn new() -> GetTxHistoryFilters { GetTxHistoryFilters::default() } - - #[inline] - pub fn with_token_id(mut self, token_id: String) -> GetTxHistoryFilters { - self.token_id = Some(token_id); - self + pub fn for_address(address: String) -> GetTxHistoryFilters { + GetTxHistoryFilters { + token_id: None, + for_addresses: std::iter::once(address).collect(), + } } #[inline] - pub fn set_for_addresses>(&mut self, addresses: I) { - self.for_addresses = Some(addresses.into_iter().collect()); + pub fn for_addresses>(addresses: I) -> GetTxHistoryFilters { + GetTxHistoryFilters { + token_id: None, + for_addresses: addresses.into_iter().collect(), + } } #[inline] - pub fn with_for_addresses>(mut self, addresses: I) -> GetTxHistoryFilters { - self.set_for_addresses(addresses); + pub fn with_token_id(mut self, token_id: String) -> GetTxHistoryFilters { + self.set_token_id(token_id); self } + #[inline] + pub fn set_token_id(&mut self, token_id: String) { self.token_id = Some(token_id); } + /// If [`GetTxHistoryFilters::token_id`] is not specified, /// we should exclude token's transactions by applying an empty `token_id` filter. fn token_id_or_exclude(&self) -> String { self.token_id.clone().unwrap_or_default() } diff --git a/mm2src/coins/tx_history_storage/sql_tx_history_storage_v2.rs b/mm2src/coins/tx_history_storage/sql_tx_history_storage_v2.rs index e40ca72b1a..77d4ccc6af 100644 --- a/mm2src/coins/tx_history_storage/sql_tx_history_storage_v2.rs +++ b/mm2src/coins/tx_history_storage/sql_tx_history_storage_v2.rs @@ -172,88 +172,141 @@ fn update_tx_in_table_by_internal_id_sql(wallet_id: &WalletId) -> Result Result> { +fn has_transactions_with_hash_sql(wallet_id: &WalletId) -> Result> { let table_name = tx_history_table(wallet_id); validate_table_name(&table_name)?; - let sql = format!( - "SELECT COUNT(id) FROM {} WHERE confirmation_status = {};", - table_name, - ConfirmationStatus::Unconfirmed.to_sql_param() - ); + let sql = format!("SELECT COUNT(id) FROM {} WHERE tx_hash = ?1;", table_name); Ok(sql) } -fn get_unconfirmed_transactions_sql(wallet_id: &WalletId) -> Result> { - let table_name = tx_history_table(wallet_id); +fn get_tx_hex_from_cache_sql(wallet_id: &WalletId) -> Result> { + let table_name = tx_cache_table(wallet_id); validate_table_name(&table_name)?; - let sql = format!( - "SELECT details_json FROM {} WHERE confirmation_status = {};", - table_name, - ConfirmationStatus::Unconfirmed.to_sql_param() - ); + let sql = format!("SELECT tx_hex FROM {} WHERE tx_hash = ?1 LIMIT 1;", table_name); Ok(sql) } -fn has_transactions_with_hash_sql(wallet_id: &WalletId) -> Result> { - let table_name = tx_history_table(wallet_id); - validate_table_name(&table_name)?; +/// Creates `SqlQuery` builder to query transactions from `tx_history` table +/// joining `tx_addresses` table and specifying from/to `for_addresses` addresses. +fn tx_history_with_addresses_builder_preimage<'a>( + connection: &'a Connection, + wallet_id: &WalletId, + for_addresses: FilteringAddresses, +) -> Result, MmError> { + let mut sql_builder = SqlQuery::select_from_alias(connection, &tx_history_table(wallet_id), "tx_history")?; - let sql = format!("SELECT COUNT(id) FROM {} WHERE tx_hash = ?1;", table_name); + // Query transactions that were sent from/to `for_addresses` addresses. + let tx_address_table_name = tx_address_table(wallet_id); - Ok(sql) + sql_builder + .join_alias(&tx_address_table_name, "tx_address")? + .on_join_eq("tx_history.internal_id", "tx_address.internal_id")?; + + sql_builder + .and_where_in_params("tx_address.address", for_addresses)? + .group_by("tx_history.internal_id")?; + + Ok(sql_builder) } -fn unique_tx_hashes_num_sql(wallet_id: &WalletId) -> Result> { - let table_name = tx_history_table(wallet_id); - validate_table_name(&table_name)?; +fn count_unique_tx_hashes_preimage<'a>( + connection: &'a Connection, + wallet_id: &WalletId, + for_addresses: FilteringAddresses, +) -> Result, MmError> { + /// The alias is needed so that the external query can access the results of the subquery. + /// Example: + /// SUBQUERY: `SELECT h.tx_hash AS __TX_HASH_ALIAS FROM tx_history h JOIN tx_address a ON h.internal_id = a.internal_id WHERE a.address IN ('address_2', 'address_4') GROUP BY h.internal_id` + /// EXTERNAL_QUERY: `SELECT COUNT(DISTINCT __TX_HASH_ALIAS) FROM ();` + /// Here we can't use `h.tx_hash` in the external query because it doesn't know about the `tx_history h` table. + /// So we need to give the `h.tx_hash` an alias like `__TX_HASH_ALIAS`. + const TX_HASH_ALIAS: &str = "__TX_HASH_ALIAS"; - let sql = format!("SELECT COUNT(DISTINCT tx_hash) FROM {};", table_name); + let subquery = { + let mut sql_builder = tx_history_with_addresses_builder_preimage(connection, wallet_id, for_addresses)?; - Ok(sql) + // Query `tx_hash` field and give it the `__TX_HASH_ALIAS` alias. + sql_builder.field_alias("tx_history.tx_hash", TX_HASH_ALIAS)?; + + drop_mutability!(sql_builder); + sql_builder.subquery() + }; + + let mut external_query = SqlQuery::select_from_subquery(subquery)?; + external_query.count_distinct(TX_HASH_ALIAS)?; + Ok(external_query) } -fn get_tx_hex_from_cache_sql(wallet_id: &WalletId) -> Result> { - let table_name = tx_cache_table(wallet_id); - validate_table_name(&table_name)?; +fn history_contains_unconfirmed_txes_preimage<'a>( + connection: &'a Connection, + wallet_id: &WalletId, + for_addresses: FilteringAddresses, +) -> Result, MmError> { + /// The alias is needed so that the external query can access the results of the subquery. + /// Example: + /// SUBQUERY: `SELECT h.id AS __ID_ALIAS FROM tx_history h JOIN tx_address a ON h.internal_id = a.internal_id WHERE a.address IN ('address_2', 'address_4') GROUP BY h.internal_id` + /// EXTERNAL_QUERY: `SELECT COUNT(__ID_ALIAS) FROM ();` + /// Here we can't use `h.id` in the external query because it doesn't know about the `tx_history h` table. + /// So we need to give the `h.id` an alias like `__ID_ALIAS`. + const ID_ALIAS: &str = "__ID_ALIAS"; - let sql = format!("SELECT tx_hex FROM {} WHERE tx_hash = ?1 LIMIT 1;", table_name); + let subquery = { + let mut sql_builder = tx_history_with_addresses_builder_preimage(connection, wallet_id, for_addresses)?; - Ok(sql) + // Query `tx_hash` field and give it the `__ID_ALIAS` alias. + sql_builder + .field_alias("tx_history.id", ID_ALIAS)? + .and_where_eq("confirmation_status", ConfirmationStatus::Unconfirmed.to_sql_param())?; + + drop_mutability!(sql_builder); + sql_builder.subquery() + }; + + let mut external_query = SqlQuery::select_from_subquery(subquery)?; + external_query.count(ID_ALIAS)?; + Ok(external_query) } -/// Creates an `SqlQuery` instance with the required `WHERE`, `ORDER`, `GROUP_BY` constraints. -/// Please note you can refer to the [`tx_history_table(wallet_id)`] table by the `tx_history` alias. -fn get_history_builder_preimage<'a>( +fn get_unconfirmed_txes_builder_preimage<'a>( connection: &'a Connection, wallet_id: &WalletId, - token_id: String, - for_addresses: Option, + for_addresses: FilteringAddresses, ) -> Result, MmError> { - let mut sql_builder = SqlQuery::select_from_alias(connection, &tx_history_table(wallet_id), "tx_history")?; + let mut sql_builder = tx_history_with_addresses_builder_preimage(connection, wallet_id, for_addresses)?; - // Check if we need to join the [`tx_address_table(wallet_id)`] table - // to query transactions that were sent from/to `for_addresses` addresses. - if let Some(for_addresses) = for_addresses { - let tx_address_table_name = tx_address_table(wallet_id); + sql_builder + .field("details_json")? + .and_where_eq("confirmation_status", ConfirmationStatus::Unconfirmed.to_sql_param())?; - sql_builder - .join_alias(&tx_address_table_name, "tx_address")? - .on_join_eq("tx_history.internal_id", "tx_address.internal_id")?; + drop_mutability!(sql_builder); + Ok(sql_builder) +} - sql_builder - .and_where_in_params("tx_address.address", for_addresses)? - .group_by("tx_history.internal_id")?; - } +/// Creates an `SqlQuery` instance with the required `WHERE`, `ORDER`, `GROUP_BY` constraints. +/// +/// # Note +/// +/// 1) You can refer to the [`tx_history_table(wallet_id)`] table by the `tx_history` alias. +/// 2) The selected transactions will be ordered the same way as `compare_transaction_details` is implemented. +fn get_history_builder_preimage<'a>( + connection: &'a Connection, + wallet_id: &WalletId, + token_id: String, + for_addresses: FilteringAddresses, +) -> Result, MmError> { + let mut sql_builder = tx_history_with_addresses_builder_preimage(connection, wallet_id, for_addresses)?; + // Set other query conditions. sql_builder .and_where_eq_param("tx_history.token_id", token_id)? + // The following statements repeat the `compare_transaction_details` implementation: .order_asc("tx_history.confirmation_status")? .order_desc("tx_history.block_height")? - .order_asc("tx_history.id")?; + .order_asc("tx_history.internal_id")?; Ok(sql_builder) } @@ -290,7 +343,9 @@ fn tx_details_from_row(row: &Row<'_>) -> Result { impl TxHistoryStorageError for SqlError {} impl ConfirmationStatus { - fn to_sql_param(self) -> String { (self as u8).to_string() } + fn to_sql_param_str(self) -> String { (self as u8).to_string() } + + fn to_sql_param(self) -> i64 { self as i64 } } impl WalletId { @@ -393,7 +448,7 @@ impl TxHistoryStorage for SqliteTxHistoryStorage { tx_hash, internal_id.clone(), tx.block_height.to_string(), - confirmation_status.to_sql_param(), + confirmation_status.to_sql_param_str(), token_id, tx_json, ]; @@ -457,13 +512,21 @@ impl TxHistoryStorage for SqliteTxHistoryStorage { .await } - async fn history_contains_unconfirmed_txes(&self, wallet_id: &WalletId) -> Result> { - let sql = contains_unconfirmed_transactions_sql(wallet_id)?; + async fn history_contains_unconfirmed_txes( + &self, + wallet_id: &WalletId, + for_addresses: FilteringAddresses, + ) -> Result> { + let wallet_id = wallet_id.clone(); let selfi = self.clone(); async_blocking(move || { let conn = selfi.0.lock().unwrap(); - let count_unconfirmed = conn.query_row::(&sql, NO_PARAMS, |row| row.get(0))?; + let sql_query = history_contains_unconfirmed_txes_preimage(&conn, &wallet_id, for_addresses)?; + + let count_unconfirmed: u32 = sql_query + .query_single_row(|row| row.get(0))? + .or_mm_err(|| SqlError::QueryReturnedNoRows)?; Ok(count_unconfirmed > 0) }) .await @@ -472,15 +535,16 @@ impl TxHistoryStorage for SqliteTxHistoryStorage { async fn get_unconfirmed_txes_from_history( &self, wallet_id: &WalletId, + for_addresses: FilteringAddresses, ) -> Result, MmError> { - let sql = get_unconfirmed_transactions_sql(wallet_id)?; + let wallet_id = wallet_id.clone(); let selfi = self.clone(); async_blocking(move || { let conn = selfi.0.lock().unwrap(); - let mut stmt = conn.prepare(&sql)?; - let rows = stmt.query(NO_PARAMS)?; - let result = rows.mapped(tx_details_from_row).collect::>()?; + + let sql_query = get_unconfirmed_txes_builder_preimage(&conn, &wallet_id, for_addresses)?; + let result = sql_query.query(tx_details_from_row)?; Ok(result) }) .await @@ -500,7 +564,7 @@ impl TxHistoryStorage for SqliteTxHistoryStorage { let params = [ block_height, - confirmation_status.to_sql_param(), + confirmation_status.to_sql_param_str(), json_details, internal_id, ]; @@ -526,12 +590,21 @@ impl TxHistoryStorage for SqliteTxHistoryStorage { .await } - async fn unique_tx_hashes_num_in_history(&self, wallet_id: &WalletId) -> Result> { - let sql = unique_tx_hashes_num_sql(wallet_id)?; + async fn unique_tx_hashes_num_in_history( + &self, + wallet_id: &WalletId, + for_addresses: FilteringAddresses, + ) -> Result> { let selfi = self.clone(); + let wallet_id = wallet_id.clone(); + async_blocking(move || { let conn = selfi.0.lock().unwrap(); - let count: u32 = conn.query_row(&sql, NO_PARAMS, |row| row.get(0))?; + + let sql_query = count_unique_tx_hashes_preimage(&conn, &wallet_id, for_addresses)?; + let count: u32 = sql_query + .query_single_row(|row| row.get(0))? + .or_mm_err(|| SqlError::QueryReturnedNoRows)?; Ok(count as usize) }) .await @@ -583,9 +656,9 @@ impl TxHistoryStorage for SqliteTxHistoryStorage { paging: PagingOptionsEnum, limit: usize, ) -> Result> { - // Check if [`GetTxHistoryFilters::for_addresses`] is specified and empty. + // Check if [`GetTxHistoryFilters::for_addresses`] is empty. // If it is, it's much more efficient to return an empty result before we do any query. - if matches!(filters.for_addresses, Some(ref for_addresses) if for_addresses.is_empty()) { + if filters.for_addresses.is_empty() { return Ok(GetHistoryResult { transactions: Vec::new(), skipped: 0, diff --git a/mm2src/coins/tx_history_storage/tx_history_v2_tests.rs b/mm2src/coins/tx_history_storage/tx_history_v2_tests.rs index 8adefb6a74..4d9f8a997b 100644 --- a/mm2src/coins/tx_history_storage/tx_history_v2_tests.rs +++ b/mm2src/coins/tx_history_storage/tx_history_v2_tests.rs @@ -1,10 +1,14 @@ +//! Consider using very dirty [Rust script](https://pastebin.ubuntu.com/p/9r2mDmGGHT/) +//! to print all transactions from `../for_tests/tBCH_tx_history_fixtures.json` ordered. + use crate::my_tx_history_v2::{GetHistoryResult, TxHistoryStorage}; -use crate::tx_history_storage::{GetTxHistoryFilters, TxHistoryStorageBuilder, WalletId}; +use crate::tx_history_storage::{FilteringAddresses, GetTxHistoryFilters, TxHistoryStorageBuilder, WalletId}; use crate::{BytesJson, TransactionDetails}; use common::PagingOptionsEnum; use mm2_test_helpers::for_tests::mm_ctx_with_custom_db; use serde_json as json; use std::collections::HashMap; +use std::iter::FromIterator; use std::num::NonZeroUsize; const BCH_TX_HISTORY_STR: &str = include_str!("../for_tests/tBCH_tx_history_fixtures.json"); @@ -35,20 +39,6 @@ fn assert_get_history_result(actual: GetHistoryResult, expected_ids: Vec( - storage: &Storage, - wallet_id: &WalletId, -) -> Vec { - let filters = GetTxHistoryFilters::new(); - let paging_options = PagingOptionsEnum::PageNumber(NonZeroUsize::new(1).unwrap()); - let limit = u32::MAX as usize; - storage - .get_history(wallet_id, filters, paging_options, limit) - .await - .unwrap() - .transactions -} - async fn test_add_transactions_impl() { let wallet_id = wallet_id_for_test("TEST_ADD_TRANSACTIONS"); @@ -60,12 +50,20 @@ async fn test_add_transactions_impl() { let tx1 = get_bch_tx_details("6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69"); let transactions = [tx1.clone(), tx1.clone()]; + let filters = GetTxHistoryFilters::for_address("bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66".to_string()); + let paging_options = PagingOptionsEnum::PageNumber(NonZeroUsize::new(1).unwrap()); + let limit = u32::MAX as usize; + // must fail because we are adding transactions with the same internal_id storage .add_transactions_to_history(&wallet_id, transactions) .await .unwrap_err(); - let actual_txs = get_coin_history(&storage, &wallet_id).await; + let actual_txs = storage + .get_history(&wallet_id, filters.clone(), paging_options.clone(), limit) + .await + .unwrap() + .transactions; assert!(actual_txs.is_empty()); let tx2 = get_bch_tx_details("c07836722bbdfa2404d8fe0ea56700d02e2012cb9dc100ccaf1138f334a759ce"); @@ -74,7 +72,11 @@ async fn test_add_transactions_impl() { .add_transactions_to_history(&wallet_id, transactions.clone()) .await .unwrap(); - let actual_txs = get_coin_history(&storage, &wallet_id).await; + let actual_txs = storage + .get_history(&wallet_id, filters, paging_options, limit) + .await + .unwrap() + .transactions; assert_eq!(actual_txs, transactions); } @@ -187,26 +189,84 @@ async fn test_contains_and_get_unconfirmed_transaction_impl() { storage.init(&wallet_id).await.unwrap(); - let mut tx_details = get_bch_tx_details("6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69"); - tx_details.block_height = 0; + let mut tx1 = get_bch_tx_details("afa7785fdb0e49e649aa9b6467fa183c8185c398095baac2c11df50175a7f92b"); + tx1.block_height = 0; + let mut tx2 = get_bch_tx_details("06f38595a2d5d23df8a81a0d744ac3a70c3e46a01efa64a4be862b9d582167b0"); + tx2.block_height = 0; + let mut tx3 = get_bch_tx_details("0fcc9cf22ea2332c73cf6cb4cf89b764d1b936a1ef4d92a087e760378fe6b96e"); + tx3.block_height = 0; + storage - .add_transactions_to_history(&wallet_id, [tx_details.clone()]) + .add_transactions_to_history(&wallet_id, [tx1.clone(), tx2.clone(), tx3.clone()]) .await .unwrap(); - let contains_unconfirmed = storage.history_contains_unconfirmed_txes(&wallet_id).await.unwrap(); + let for_first_address = + FilteringAddresses::from_iter(["bchtest:pqtflkhvhpeqsxphk36yp7pq22stu7ed3sqfxsdt7x".to_string()]); + + let contains_unconfirmed = storage + .history_contains_unconfirmed_txes(&wallet_id, for_first_address.clone()) + .await + .unwrap(); assert!(contains_unconfirmed); - let unconfirmed_transactions = storage.get_unconfirmed_txes_from_history(&wallet_id).await.unwrap(); + let unconfirmed_transactions = storage + .get_unconfirmed_txes_from_history(&wallet_id, for_first_address.clone()) + .await + .unwrap(); + // There only 2 unconfirmed transactions for `bchtest:pqtflkhvhpeqsxphk36yp7pq22stu7ed3sqfxsdt7x` address. + assert_eq!(unconfirmed_transactions.len(), 2); + + tx1.block_height = 12345; + storage.update_tx_in_history(&wallet_id, &tx1).await.unwrap(); + + let unconfirmed_transactions = storage + .get_unconfirmed_txes_from_history(&wallet_id, for_first_address) + .await + .unwrap(); + // Now there is 1 unconfirmed transaction for `bchtest:pqtflkhvhpeqsxphk36yp7pq22stu7ed3sqfxsdt7x` address. assert_eq!(unconfirmed_transactions.len(), 1); - tx_details.block_height = 12345; - storage.update_tx_in_history(&wallet_id, &tx_details).await.unwrap(); + let for_all_addresses = FilteringAddresses::from_iter([ + "bchtest:pqtflkhvhpeqsxphk36yp7pq22stu7ed3sqfxsdt7x".to_string(), + "bchtest:qp5fphvvj3pvrrv2awhm7dyu8xjueydapg3ju9kwmm".to_string(), + ]); + let unconfirmed_transactions = storage + .get_unconfirmed_txes_from_history(&wallet_id, for_all_addresses) + .await + .unwrap(); + // 1 unconfirmed transaction for `bchtest:pqtflkhvhpeqsxphk36yp7pq22stu7ed3sqfxsdt7x` + // and 1 unconfirmed transaction for `bchtest:qp5fphvvj3pvrrv2awhm7dyu8xjueydapg3ju9kwmm`. + assert_eq!(unconfirmed_transactions.len(), 2); + + tx3.block_height = 54321; + storage.update_tx_in_history(&wallet_id, &tx3).await.unwrap(); - let contains_unconfirmed = storage.history_contains_unconfirmed_txes(&wallet_id).await.unwrap(); + let for_second_address = + FilteringAddresses::from_iter(["bchtest:qp5fphvvj3pvrrv2awhm7dyu8xjueydapg3ju9kwmm".to_string()]); + let contains_unconfirmed = storage + .history_contains_unconfirmed_txes(&wallet_id, for_second_address.clone()) + .await + .unwrap(); assert!(!contains_unconfirmed); - let unconfirmed_transactions = storage.get_unconfirmed_txes_from_history(&wallet_id).await.unwrap(); + let unconfirmed_transactions = storage + .get_unconfirmed_txes_from_history(&wallet_id, for_second_address) + .await + .unwrap(); + assert!(unconfirmed_transactions.is_empty()); + + let for_unknown_address = FilteringAddresses::from_iter(["bchtest:unknown_address".to_string()]); + let contains_unconfirmed = storage + .history_contains_unconfirmed_txes(&wallet_id, for_unknown_address.clone()) + .await + .unwrap(); + assert!(!contains_unconfirmed); + + let unconfirmed_transactions = storage + .get_unconfirmed_txes_from_history(&wallet_id, for_unknown_address) + .await + .unwrap(); assert!(unconfirmed_transactions.is_empty()); } @@ -264,8 +324,28 @@ async fn test_unique_tx_hashes_num_impl() { .await .unwrap(); - let tx_hashes_num = storage.unique_tx_hashes_num_in_history(&wallet_id).await.unwrap(); + let for_addresses = + FilteringAddresses::from_iter(["bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66".to_string()]); + let tx_hashes_num = storage + .unique_tx_hashes_num_in_history(&wallet_id, for_addresses) + .await + .unwrap(); assert_eq!(2, tx_hashes_num); + + let for_addresses = + FilteringAddresses::from_iter(["bchtest:qz2nkwgfla42y60ctk35cye2jfpygs8p3c87hd35es".to_string()]); + let tx_hashes_num = storage + .unique_tx_hashes_num_in_history(&wallet_id, for_addresses) + .await + .unwrap(); + assert_eq!(1, tx_hashes_num); + + let for_addresses = FilteringAddresses::from_iter(["bchtest:unknown_address".to_string()]); + let tx_hashes_num = storage + .unique_tx_hashes_num_in_history(&wallet_id, for_addresses) + .await + .unwrap(); + assert_eq!(0, tx_hashes_num); } async fn test_add_and_get_tx_from_cache_impl() { @@ -351,7 +431,7 @@ async fn test_get_history_page_number_impl() { .await .unwrap(); - let filters = GetTxHistoryFilters::new(); + let filters = GetTxHistoryFilters::for_address("bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66".to_string()); let paging = PagingOptionsEnum::PageNumber(NonZeroUsize::new(1).unwrap()); let limit = 4; @@ -365,21 +445,19 @@ async fn test_get_history_page_number_impl() { ]; assert_get_history_result(result, expected_internal_ids, 0, 123); - let filters = GetTxHistoryFilters::new() + let filters = GetTxHistoryFilters::for_address("slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8".to_string()) .with_token_id("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7".to_owned()); let paging = PagingOptionsEnum::PageNumber(NonZeroUsize::new(2).unwrap()); - let limit = 5; + let limit = 3; let result = storage.get_history(&wallet_id, filters, paging, limit).await.unwrap(); let expected_internal_ids: Vec = vec![ - "433b641bc89e1b59c22717918583c60ec98421805c8e85b064691705d9aeb970".into(), - "cd6ec10b0cd9747ddc66ac5c97c2d7b493e8cea191bc2d847b3498719d4bd989".into(), + "babe9bd0dc1495dff0920da14a76311b744daadc9d01314f8bd4e2438c6b183b".into(), "1c1e68357cf5a6dacb53881f13aa5d2048fe0d0fab24b76c9ec48f53884bed97".into(), - "c4304b5ef4f1b88ed4939534a8ca9eca79f592939233174ae08002e8454e3f06".into(), - "b0035434a1e7be5af2ed991ee2a21a90b271c5852a684a0b7d315c5a770d1b1c".into(), + "cd6ec10b0cd9747ddc66ac5c97c2d7b493e8cea191bc2d847b3498719d4bd989".into(), ]; - assert_get_history_result(result, expected_internal_ids, 5, 121); + assert_get_history_result(result, expected_internal_ids, 3, 119); } async fn test_get_history_from_id_impl() { @@ -395,7 +473,7 @@ async fn test_get_history_from_id_impl() { .await .unwrap(); - let filters = GetTxHistoryFilters::new(); + let filters = GetTxHistoryFilters::for_address("bchtest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsnnczzt66".to_string()); let paging = PagingOptionsEnum::FromId("6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69".into()); let limit = 3; @@ -408,7 +486,7 @@ async fn test_get_history_from_id_impl() { ]; assert_get_history_result(result, expected_internal_ids, 1, 123); - let filters = GetTxHistoryFilters::new() + let filters = GetTxHistoryFilters::for_address("slptest:qzx0llpyp8gxxsmad25twksqnwd62xm3lsg8lecug8".to_string()) .with_token_id("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7".to_owned()); let paging = PagingOptionsEnum::FromId("433b641bc89e1b59c22717918583c60ec98421805c8e85b064691705d9aeb970".into()); let limit = 4; @@ -416,12 +494,12 @@ async fn test_get_history_from_id_impl() { let result = storage.get_history(&wallet_id, filters, paging, limit).await.unwrap(); let expected_internal_ids: Vec = vec![ - "cd6ec10b0cd9747ddc66ac5c97c2d7b493e8cea191bc2d847b3498719d4bd989".into(), + "babe9bd0dc1495dff0920da14a76311b744daadc9d01314f8bd4e2438c6b183b".into(), "1c1e68357cf5a6dacb53881f13aa5d2048fe0d0fab24b76c9ec48f53884bed97".into(), - "c4304b5ef4f1b88ed4939534a8ca9eca79f592939233174ae08002e8454e3f06".into(), + "cd6ec10b0cd9747ddc66ac5c97c2d7b493e8cea191bc2d847b3498719d4bd989".into(), "b0035434a1e7be5af2ed991ee2a21a90b271c5852a684a0b7d315c5a770d1b1c".into(), ]; - assert_get_history_result(result, expected_internal_ids, 6, 121); + assert_get_history_result(result, expected_internal_ids, 3, 119); } async fn test_get_history_for_addresses_impl() { @@ -441,8 +519,7 @@ async fn test_get_history_for_addresses_impl() { "slptest:ppfdp6t2qs7rc79wxjppwv0hwvr776x5vu2enth4zh".to_owned(), "slptest:pqgk69yyj6dzag4mdyur9lykye89ucz9vskelzwhck".to_owned(), ]; - let filters = GetTxHistoryFilters::new() - .with_for_addresses(for_addresses) + let filters = GetTxHistoryFilters::for_addresses(for_addresses) .with_token_id("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7".to_owned()); let paging = PagingOptionsEnum::PageNumber(NonZeroUsize::new(1).unwrap()); let limit = 5; @@ -462,8 +539,7 @@ async fn test_get_history_for_addresses_impl() { "slptest:ppfdp6t2qs7rc79wxjppwv0hwvr776x5vu2enth4zh".to_owned(), "slptest:pqgk69yyj6dzag4mdyur9lykye89ucz9vskelzwhck".to_owned(), ]; - let filters = GetTxHistoryFilters::new() - .with_for_addresses(for_addresses) + let filters = GetTxHistoryFilters::for_addresses(for_addresses) .with_token_id("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7".to_owned()); let paging = PagingOptionsEnum::FromId("e46fa0836be0534f7799b2ef5b538551ea25b6f430b7e015a95731efb7a0cd4f".into()); let limit = 4; @@ -482,8 +558,7 @@ async fn test_get_history_for_addresses_impl() { "slptest:ppfdp6t2qs7rc79wxjppwv0hwvr776x5vu2enth4zh".to_owned(), "slptest:pqgk69yyj6dzag4mdyur9lykye89ucz9vskelzwhck".to_owned(), ]; - let filters = GetTxHistoryFilters::new() - .with_for_addresses(for_addresses) + let filters = GetTxHistoryFilters::for_addresses(for_addresses) .with_token_id("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7".to_owned()); let paging = PagingOptionsEnum::FromId("6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69".into()); let limit = 2; @@ -498,8 +573,7 @@ async fn test_get_history_for_addresses_impl() { "slptest:ppfdp6t2qs7rc79wxjppwv0hwvr776x5vu2enth4zh".to_owned(), "slptest:pqgk69yyj6dzag4mdyur9lykye89ucz9vskelzwhck".to_owned(), ]; - let filters = GetTxHistoryFilters::new() - .with_for_addresses(for_addresses) + let filters = GetTxHistoryFilters::for_addresses(for_addresses) .with_token_id("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7".to_owned()); let paging = PagingOptionsEnum::PageNumber(NonZeroUsize::new(2).unwrap()); let limit = 4; diff --git a/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v2.rs b/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v2.rs index a07bdaf6fc..996c7e3b7f 100644 --- a/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v2.rs +++ b/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v2.rs @@ -3,7 +3,7 @@ use crate::tx_history_storage::wasm::tx_history_db::{TxHistoryDb, TxHistoryDbLoc use crate::tx_history_storage::wasm::{WasmTxHistoryError, WasmTxHistoryResult}; use crate::tx_history_storage::{token_id_from_tx_type, ConfirmationStatus, CreateTxHistoryStorageError, FilteringAddresses, GetTxHistoryFilters, WalletId}; -use crate::{CoinsContext, TransactionDetails}; +use crate::{compare_transaction_details, CoinsContext, TransactionDetails}; use async_trait::async_trait; use common::PagingOptionsEnum; use itertools::Itertools; @@ -118,24 +118,23 @@ impl TxHistoryStorage for IndexedDbTxHistoryStorage { json::from_value(details_json).map_to_mm(|e| WasmTxHistoryError::ErrorDeserializing(e.to_string())) } - async fn history_contains_unconfirmed_txes(&self, wallet_id: &WalletId) -> Result> { - let locked_db = self.lock_db().await?; - let db_transaction = locked_db.get_inner().transaction().await?; - let table = db_transaction.table::().await?; - - let index_keys = MultiIndex::new(TxHistoryTableV2::WALLET_ID_CONFIRMATION_STATUS_INDEX) - .with_value(&wallet_id.ticker)? - .with_value(wallet_id.hd_wallet_rmd160_or_exclude())? - .with_value(ConfirmationStatus::Unconfirmed)?; - - let count_unconfirmed = table.count_by_multi_index(index_keys).await?; - Ok(count_unconfirmed > 0) + /// Since we need to filter the transactions by the given `for_addresses`, + /// we can't use [`DbTable::count_by_multi_index`]. + /// TODO consider one of the solutions described at [`IndexedDbTxHistoryStorage::get_history`]. + async fn history_contains_unconfirmed_txes( + &self, + wallet_id: &WalletId, + for_addresses: FilteringAddresses, + ) -> Result> { + let txs = self.get_unconfirmed_txes_from_history(wallet_id, for_addresses).await?; + Ok(!txs.is_empty()) } /// Gets the unconfirmed transactions from the history async fn get_unconfirmed_txes_from_history( &self, wallet_id: &WalletId, + for_addresses: FilteringAddresses, ) -> MmResult, Self::Error> { let locked_db = self.lock_db().await?; let db_transaction = locked_db.get_inner().transaction().await?; @@ -146,13 +145,13 @@ impl TxHistoryStorage for IndexedDbTxHistoryStorage { .with_value(wallet_id.hd_wallet_rmd160_or_exclude())? .with_value(ConfirmationStatus::Unconfirmed)?; - table + let transactions = table .get_items_by_multi_index(index_keys) .await? .into_iter() - .map(|(_item_id, item)| tx_details_from_item(item)) - // Collect `WasmTxHistoryResult>`. - .collect() + .map(|(_item_id, item)| item); + + Self::take_according_to_filtering_addresses(transactions, &for_addresses) } /// Updates transaction in the selected coin's history @@ -183,9 +182,12 @@ impl TxHistoryStorage for IndexedDbTxHistoryStorage { Ok(count_txs > 0) } - /// TODO consider refactoring this method to return unique internal_id's instead of tx_hash, - /// since the method requests the whole TX history of the specified wallet. - async fn unique_tx_hashes_num_in_history(&self, wallet_id: &WalletId) -> Result> { + /// TODO consider refactoring this method to avoid fetching all transactions. + async fn unique_tx_hashes_num_in_history( + &self, + wallet_id: &WalletId, + for_addresses: FilteringAddresses, + ) -> Result> { let locked_db = self.lock_db().await?; let db_transaction = locked_db.get_inner().transaction().await?; let table = db_transaction.table::().await?; @@ -196,12 +198,15 @@ impl TxHistoryStorage for IndexedDbTxHistoryStorage { // `IndexedDb` doesn't provide an elegant way to count records applying custom filters to index properties like `tx_hash`, // so currently fetch all records with `coin,hd_wallet_rmd160=wallet_id` and apply the `unique_by(|tx| tx.tx_hash)` to them. - Ok(table + let transactions = table .get_items_by_multi_index(index_keys) .await? .into_iter() - .unique_by(|(_item_id, tx)| tx.tx_hash.clone()) - .count()) + .map(|(_item_id, tx)| tx) + .unique_by(|tx| tx.tx_hash.clone()); + + let filtered_transactions = Self::take_according_to_filtering_addresses(transactions, &for_addresses)?; + Ok(filtered_transactions.len()) } async fn add_tx_to_cache( @@ -260,9 +265,9 @@ impl TxHistoryStorage for IndexedDbTxHistoryStorage { paging: PagingOptionsEnum, limit: usize, ) -> MmResult { - // Check if [`GetTxHistoryFilters::for_addresses`] is specified and empty. + // Check if [`GetTxHistoryFilters::for_addresses`] is empty. // If it is, it's much more efficient to return an empty result before we do any query. - if matches!(filters.for_addresses, Some(ref for_addresses) if for_addresses.is_empty()) { + if filters.for_addresses.is_empty() { return Ok(GetHistoryResult { transactions: Vec::new(), skipped: 0, @@ -285,7 +290,7 @@ impl TxHistoryStorage for IndexedDbTxHistoryStorage { .into_iter() .map(|(_item_id, tx)| tx); - let transactions = Self::take_according_to_filtering_addresses(transactions, &filters.for_addresses); + let transactions = Self::take_according_to_filtering_addresses(transactions, &filters.for_addresses)?; Self::take_according_to_paging_opts(transactions, paging, limit) } } @@ -293,28 +298,31 @@ impl TxHistoryStorage for IndexedDbTxHistoryStorage { impl IndexedDbTxHistoryStorage { fn take_according_to_filtering_addresses( txs: I, - for_addresses: &Option, - ) -> Vec + for_addresses: &FilteringAddresses, + ) -> WasmTxHistoryResult> where I: Iterator, { - match for_addresses { - Some(for_addresses) => txs - .filter(|tx| { - tx.from_addresses.has_intersection(for_addresses) || tx.to_addresses.has_intersection(for_addresses) - }) - .collect(), - None => txs.collect(), - } + txs.filter(|tx| { + tx.from_addresses.has_intersection(for_addresses) || tx.to_addresses.has_intersection(for_addresses) + }) + .map(tx_details_from_item) + .collect() } pub(super) fn take_according_to_paging_opts( - txs: Vec, + mut txs: Vec, paging: PagingOptionsEnum, limit: usize, ) -> WasmTxHistoryResult { let total_count = txs.len(); + // This is super inefficient to fetch the whole transaction history, sort it on the client side. + // It's required to implement `DESC` order for `IdbCursor` in order to sort the transactions + // the same way as `compare_transaction_details` does. + // But it's difficult to implement, and I think it can be postponed for a while. + txs.sort_by(compare_transaction_details); + let skip = match paging { // `page_number` is ignored if from_uuid is set PagingOptionsEnum::FromId(from_internal_id) => { @@ -336,15 +344,8 @@ impl IndexedDbTxHistoryStorage { PagingOptionsEnum::PageNumber(page_number) => (page_number.get() - 1) * limit, }; - let transactions = txs - .into_iter() - .skip(skip) - .take(limit) - .map(tx_details_from_item) - // Collect `WasmTxHistoryResult` items into `WasmTxHistoryResult>` - .collect::>>()?; Ok(GetHistoryResult { - transactions, + transactions: txs.into_iter().skip(skip).take(limit).collect(), skipped: skip, total: total_count, }) diff --git a/mm2src/coins/utxo.rs b/mm2src/coins/utxo.rs index 35b4be4e6d..59bdf81960 100644 --- a/mm2src/coins/utxo.rs +++ b/mm2src/coins/utxo.rs @@ -22,7 +22,6 @@ // pub mod bch; -pub mod bch_and_slp_tx_history; mod bchd_grpc; #[allow(clippy::all)] #[rustfmt::skip] @@ -36,6 +35,7 @@ pub mod utxo_block_header_storage; pub mod utxo_builder; pub mod utxo_common; pub mod utxo_standard; +pub mod utxo_tx_history_v2; pub mod utxo_withdraw; use async_trait::async_trait; @@ -68,7 +68,7 @@ use mm2_metrics::MetricsArc; use mm2_number::BigDecimal; #[cfg(test)] use mocktopus::macros::*; use num_traits::ToPrimitive; -use primitives::hash::{H256, H264}; +use primitives::hash::{H160, H256, H264}; use rpc::v1::types::{Bytes as BytesJson, Transaction as RpcTransaction, H256 as H256Json}; use script::{Builder, Script, SignatureVersion, TransactionInputSigner}; use serde_json::{self as json, Value as Json}; @@ -106,7 +106,7 @@ use crate::hd_wallet::{HDAccountOps, HDAccountsMutex, HDAddress, HDAddressId, HD InvalidBip44ChainError}; use crate::hd_wallet_storage::{HDAccountStorageItem, HDWalletCoinStorage, HDWalletStorageError, HDWalletStorageResult}; use crate::utxo::tx_cache::UtxoVerboseCacheShared; -use crate::TransactionErr; +use crate::{TransactionErr, VerificationError}; pub mod tx_cache; #[cfg(target_arch = "wasm32")] @@ -739,6 +739,26 @@ impl From for GetConfirmedTxError { fn from(err: serialization::Error) -> Self { GetConfirmedTxError::SerializationError(err) } } +#[derive(Debug, Display)] +pub enum AddrFromStrError { + #[display(fmt = "{}", _0)] + Unsupported(UnsupportedAddr), + #[display(fmt = "Cannot determine format: {:?}", _0)] + CannotDetermineFormat(Vec), +} + +impl From for AddrFromStrError { + fn from(e: UnsupportedAddr) -> Self { AddrFromStrError::Unsupported(e) } +} + +impl From for VerificationError { + fn from(e: AddrFromStrError) -> Self { VerificationError::AddressDecodingError(e.to_string()) } +} + +impl From for WithdrawError { + fn from(e: AddrFromStrError) -> Self { WithdrawError::InvalidAddress(e.to_string()) } +} + impl UtxoCoinFields { pub fn transaction_preimage(&self) -> TransactionInputSigner { let lock_time = if self.conf.ticker == "KMD" { @@ -929,7 +949,7 @@ pub trait UtxoCommonOps: /// Try to parse address from string using specified on asset enable format, /// and if it failed inform user that he used a wrong format. - fn address_from_str(&self, address: &str) -> Result; + fn address_from_str(&self, address: &str) -> MmResult; async fn get_current_mtp(&self) -> UtxoRpcResult; @@ -1433,6 +1453,7 @@ impl Default for ElectrumBuilderArgs { #[derive(Debug)] pub struct UtxoHDWallet { + pub hd_wallet_rmd160: H160, pub hd_wallet_storage: HDWalletCoinStorage, pub address_format: UtxoAddressFormat, /// Derivation path of the coin. diff --git a/mm2src/coins/utxo/bch.rs b/mm2src/coins/utxo/bch.rs index 5b7de796c3..3f58fd53b5 100644 --- a/mm2src/coins/utxo/bch.rs +++ b/mm2src/coins/utxo/bch.rs @@ -1,15 +1,18 @@ use super::*; -use crate::my_tx_history_v2::{CoinWithTxHistoryV2, TxDetailsBuilder, TxHistoryStorage, TxHistoryStorageError}; +use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHistoryTarget, TxDetailsBuilder, + TxHistoryStorage}; use crate::tx_history_storage::{GetTxHistoryFilters, WalletId}; use crate::utxo::rpc_clients::UtxoRpcFut; -use crate::utxo::slp::{parse_slp_script, ParseSlpScriptError, SlpGenesisParams, SlpTokenInfo, SlpTransaction, - SlpUnspent}; +use crate::utxo::slp::{parse_slp_script, SlpGenesisParams, SlpTokenInfo, SlpTransaction, SlpUnspent}; use crate::utxo::utxo_builder::{UtxoArcBuilder, UtxoCoinBuilder}; use crate::utxo::utxo_common::big_decimal_from_sat_unsigned; -use crate::{BlockHeightAndTime, CanRefundHtlc, CoinBalance, CoinProtocol, NegotiateSwapContractAddrErr, - PrivKeyBuildPolicy, RawTransactionFut, RawTransactionRequest, SearchForSwapTxSpendInput, SignatureResult, - SwapOps, TradePreimageValue, TransactionFut, TransactionType, TxFeeDetails, TxMarshalingErr, - UnexpectedDerivationMethod, ValidateAddressResult, ValidatePaymentInput, VerificationResult, WithdrawFut}; +use crate::utxo::utxo_tx_history_v2::{UtxoMyAddressesHistoryError, UtxoTxDetailsError, UtxoTxDetailsParams, + UtxoTxHistoryOps}; +use crate::{BlockHeightAndTime, CanRefundHtlc, CoinBalance, CoinProtocol, CoinWithDerivationMethod, + NegotiateSwapContractAddrErr, PrivKeyBuildPolicy, RawTransactionFut, RawTransactionRequest, + SearchForSwapTxSpendInput, SignatureResult, SwapOps, TradePreimageValue, TransactionFut, TransactionType, + TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidatePaymentInput, + VerificationResult, WithdrawFut}; use common::log::warn; use derive_more::Display; use futures::{FutureExt, TryFutureExt}; @@ -140,40 +143,6 @@ impl From for IsSlpUtxoError { fn from(err: serialization::Error) -> IsSlpUtxoError { IsSlpUtxoError::TxDeserialization(err) } } -#[derive(Debug)] -#[allow(clippy::large_enum_variant)] -pub enum GetTxDetailsError { - StorageError(E), - AddressesFromScriptError(String), - SlpTokenIdIsNotGenesisTx(H256), - TxDeserializationError(serialization::Error), - RpcError(UtxoRpcError), - ParseSlpScriptError(ParseSlpScriptError), - ToSlpAddressError(String), - InvalidSlpTransaction(H256), - AddressDerivationError(UnexpectedDerivationMethod), -} - -impl From for GetTxDetailsError { - fn from(err: UtxoRpcError) -> Self { GetTxDetailsError::RpcError(err) } -} - -impl From for GetTxDetailsError { - fn from(err: E) -> Self { GetTxDetailsError::StorageError(err) } -} - -impl From for GetTxDetailsError { - fn from(err: serialization::Error) -> Self { GetTxDetailsError::TxDeserializationError(err) } -} - -impl From for GetTxDetailsError { - fn from(err: ParseSlpScriptError) -> Self { GetTxDetailsError::ParseSlpScriptError(err) } -} - -impl From for GetTxDetailsError { - fn from(err: UnexpectedDerivationMethod) -> Self { GetTxDetailsError::AddressDerivationError(err) } -} - impl BchCoin { pub fn slp_prefix(&self) -> &CashAddrPrefix { &self.slp_addr_prefix } @@ -378,36 +347,21 @@ impl BchCoin { Ok(slp_address) } - async fn tx_from_storage_or_rpc( - &self, - tx_hash: &H256Json, - storage: &T, - ) -> Result>> { - let tx_hash_str = format!("{:02x}", tx_hash); - let wallet_id = self.history_wallet_id(); - let tx_bytes = match storage.tx_bytes_from_cache(&wallet_id, &tx_hash_str).await? { - Some(tx_bytes) => tx_bytes, - None => { - let tx_bytes = self.as_ref().rpc_client.get_transaction_bytes(tx_hash).compat().await?; - storage.add_tx_to_cache(&wallet_id, &tx_hash_str, &tx_bytes).await?; - tx_bytes - }, - }; - let tx = deserialize(tx_bytes.0.as_slice())?; - Ok(tx) - } - /// Returns multiple details by tx hash if token transfers also occurred in the transaction pub async fn transaction_details_with_token_transfers( &self, - tx_hash: &H256Json, - block_height_and_time: Option, - storage: &T, - ) -> Result, MmError>> { - let tx = self.tx_from_storage_or_rpc(tx_hash, storage).await?; + params: UtxoTxDetailsParams<'_, T>, + ) -> MmResult, UtxoTxDetailsError> { + let tx = self.tx_from_storage_or_rpc(params.hash, params.storage).await?; let bch_tx_details = self - .bch_tx_details(tx_hash, &tx, block_height_and_time, storage) + .bch_tx_details( + params.hash, + &tx, + params.block_height_and_time, + params.storage, + params.my_addresses, + ) .await?; let maybe_op_return: Script = tx.outputs[0].script_pubkey.clone().into(); if !(maybe_op_return.is_pay_to_public_key_hash() @@ -419,9 +373,10 @@ impl BchCoin { .slp_tx_details( &tx, slp_details.transaction, - block_height_and_time, + params.block_height_and_time, bch_tx_details.fee_details.clone(), - storage, + params.storage, + params.my_addresses, ) .await?; return Ok(vec![bch_tx_details, slp_tx_details]); @@ -437,10 +392,9 @@ impl BchCoin { tx: &UtxoTx, height_and_time: Option, storage: &T, - ) -> Result>> { - let my_address = self.as_ref().derivation_method.iguana_or_err()?; - let my_addresses = [my_address.clone()]; - let mut tx_builder = TxDetailsBuilder::new(self.ticker().to_owned(), tx, height_and_time, my_addresses); + my_addresses: &HashSet
, + ) -> MmResult { + let mut tx_builder = TxDetailsBuilder::new(self.ticker().to_owned(), tx, height_and_time, my_addresses.clone()); for output in &tx.outputs { let addresses = match self.addresses_from_script(&output.script_pubkey.clone().into()) { Ok(a) => a, @@ -457,7 +411,7 @@ impl BchCoin { self.ticker(), tx_hash, ); - return MmError::err(GetTxDetailsError::AddressesFromScriptError(msg)); + return MmError::err(UtxoTxDetailsError::TxAddressDeserializationError(msg)); } let amount = big_decimal_from_sat_unsigned(output.value, self.decimals()); @@ -475,14 +429,14 @@ impl BchCoin { let prev_script = prev_tx.outputs[index as usize].script_pubkey.clone().into(); let addresses = self .addresses_from_script(&prev_script) - .map_to_mm(GetTxDetailsError::AddressesFromScriptError)?; + .map_to_mm(UtxoTxDetailsError::TxAddressDeserializationError)?; if addresses.len() != 1 { let msg = format!( "{} tx {:02x} output script resulted into unexpected number of addresses", self.ticker(), tx_hash, ); - return MmError::err(GetTxDetailsError::AddressesFromScriptError(msg)); + return MmError::err(UtxoTxDetailsError::TxAddressDeserializationError(msg)); } let prev_value = prev_tx.outputs[index as usize].value; @@ -506,13 +460,16 @@ impl BchCoin { &self, token_id: H256, storage: &T, - ) -> Result>> { + ) -> MmResult { let token_genesis_tx = self.tx_from_storage_or_rpc(&token_id.into(), storage).await?; let maybe_genesis_script: Script = token_genesis_tx.outputs[0].script_pubkey.clone().into(); let slp_details = parse_slp_script(&maybe_genesis_script)?; match slp_details.transaction { SlpTransaction::Genesis(params) => Ok(params), - _ => MmError::err(GetTxDetailsError::SlpTokenIdIsNotGenesisTx(token_id)), + _ => { + let error = format!("SLP token ID '{}' is not a genesis TX", token_id); + MmError::err(UtxoTxDetailsError::InvalidTransaction(error)) + }, } } @@ -521,7 +478,7 @@ impl BchCoin { utxo_tx: &UtxoTx, slp_tx: SlpTransaction, storage: &T, - ) -> Result, MmError>> { + ) -> MmResult, UtxoTxDetailsError> { let slp_amounts = match slp_tx { SlpTransaction::Send { token_id, amounts } => { let genesis_params = self.get_slp_genesis_params(token_id, storage).await?; @@ -554,22 +511,29 @@ impl BchCoin { Some(output) => { let addresses = self .addresses_from_script(&output.script_pubkey.clone().into()) - .map_to_mm(GetTxDetailsError::AddressesFromScriptError)?; + .map_to_mm(UtxoTxDetailsError::TxAddressDeserializationError)?; if addresses.len() != 1 { let msg = format!( "{} tx {:?} output script resulted into unexpected number of addresses", self.ticker(), utxo_tx.hash().reversed(), ); - return MmError::err(GetTxDetailsError::AddressesFromScriptError(msg)); + return MmError::err(UtxoTxDetailsError::TxAddressDeserializationError(msg)); } let slp_address = self .slp_address(&addresses[0]) - .map_to_mm(GetTxDetailsError::ToSlpAddressError)?; + .map_to_mm(UtxoTxDetailsError::InvalidTransaction)?; result.insert(output_index, (slp_address, amount)); }, - None => return MmError::err(GetTxDetailsError::InvalidSlpTransaction(utxo_tx.hash().reversed())), + None => { + let error = format!( + "Unexpected '{}' output index at {} TX", + output_index, + utxo_tx.hash().reversed() + ); + return MmError::err(UtxoTxDetailsError::InvalidTransaction(error)); + }, } } Ok(result) @@ -582,20 +546,21 @@ impl BchCoin { height_and_time: Option, tx_fee: Option, storage: &Storage, - ) -> Result>> { + my_addresses: &HashSet
, + ) -> MmResult { let token_id = match slp_tx.token_id() { Some(id) => id, None => tx.hash().reversed(), }; - let my_address = self.as_ref().derivation_method.iguana_or_err()?; - let slp_address = self - .slp_address(my_address) - .map_to_mm(GetTxDetailsError::ToSlpAddressError)?; - let addresses = [slp_address]; + let slp_addresses: Vec<_> = my_addresses + .iter() + .map(|addr| self.slp_address(addr)) + .collect::>() + .map_to_mm(UtxoTxDetailsError::Internal)?; let mut slp_tx_details_builder = - TxDetailsBuilder::new(self.ticker().to_owned(), tx, height_and_time, addresses); + TxDetailsBuilder::new(self.ticker().to_owned(), tx, height_and_time, slp_addresses); let slp_transferred_amounts = self.slp_transferred_amounts(tx, slp_tx, storage).await?; for (_, (address, amount)) in slp_transferred_amounts { slp_tx_details_builder.transferred_to(address, &amount); @@ -760,7 +725,7 @@ impl UtxoCommonOps for BchCoin { utxo_common::my_public_key(self.as_ref()) } - fn address_from_str(&self, address: &str) -> Result { + fn address_from_str(&self, address: &str) -> MmResult { utxo_common::checked_address_from_str(self, address) } @@ -1145,29 +1110,6 @@ impl MarketCoinOps for BchCoin { fn min_trading_vol(&self) -> MmNumber { utxo_common::min_trading_vol(self.as_ref()) } } -#[async_trait] -impl UtxoStandardOps for BchCoin { - async fn tx_details_by_hash( - &self, - hash: &[u8], - input_transactions: &mut HistoryUtxoTxMap, - ) -> Result { - utxo_common::tx_details_by_hash(self, hash, input_transactions).await - } - - async fn request_tx_history(&self, metrics: MetricsArc) -> RequestTxHistoryResult { - utxo_common::request_tx_history(self, metrics).await - } - - async fn update_kmd_rewards( - &self, - tx_details: &mut TransactionDetails, - input_transactions: &mut HistoryUtxoTxMap, - ) -> UtxoRpcResult<()> { - utxo_common::update_kmd_rewards(self, tx_details, input_transactions).await - } -} - #[async_trait] impl MmCoin for BchCoin { fn is_asset_chain(&self) -> bool { utxo_common::is_asset_chain(&self.utxo_arc) } @@ -1188,13 +1130,9 @@ impl MmCoin for BchCoin { fn validate_address(&self, address: &str) -> ValidateAddressResult { utxo_common::validate_address(self, address) } - fn process_history_loop(&self, ctx: MmArc) -> Box + Send> { - Box::new( - utxo_common::process_history_loop(self.clone(), ctx) - .map(|_| Ok(())) - .boxed() - .compat(), - ) + fn process_history_loop(&self, _ctx: MmArc) -> Box + Send> { + warn!("'process_history_loop' is not implemented for BchCoin! Consider using 'my_tx_history_v2'"); + Box::new(futures01::future::err(())) } fn history_sync_status(&self) -> HistorySyncState { utxo_common::history_sync_status(&self.utxo_arc) } @@ -1246,11 +1184,87 @@ impl MmCoin for BchCoin { } } +impl CoinWithDerivationMethod for BchCoin { + type Address = Address; + type HDWallet = UtxoHDWallet; + + fn derivation_method(&self) -> &DerivationMethod { + utxo_common::derivation_method(self.as_ref()) + } +} + +#[async_trait] impl CoinWithTxHistoryV2 for BchCoin { fn history_wallet_id(&self) -> WalletId { WalletId::new(self.ticker().to_owned()) } - /// There are not specific filters for `BchCoin`. - fn get_tx_history_filters(&self) -> GetTxHistoryFilters { GetTxHistoryFilters::new() } + /// TODO consider using `utxo_common::utxo_tx_history_common::get_tx_history_filters` + /// when `BchCoin` implements `CoinWithDerivationMethod`. + async fn get_tx_history_filters( + &self, + target: MyTxHistoryTarget, + ) -> MmResult { + match target { + MyTxHistoryTarget::Iguana => (), + target => { + let error = format!("Expected 'Iguana' target, found {target:?}"); + return MmError::err(MyTxHistoryErrorV2::InvalidTarget(error)); + }, + } + let my_address = self.my_address().map_to_mm(MyTxHistoryErrorV2::Internal)?; + Ok(GetTxHistoryFilters::for_address(my_address)) + } +} + +#[async_trait] +impl UtxoTxHistoryOps for BchCoin { + async fn my_addresses(&self) -> MmResult, UtxoMyAddressesHistoryError> { + let my_address = self.as_ref().derivation_method.iguana_or_err()?; + Ok(std::iter::once(my_address.clone()).collect()) + } + + async fn tx_details_by_hash( + &self, + params: UtxoTxDetailsParams<'_, Storage>, + ) -> MmResult, UtxoTxDetailsError> + where + Storage: TxHistoryStorage, + { + Ok(self.transaction_details_with_token_transfers(params).await?) + } + + async fn tx_from_storage_or_rpc( + &self, + tx_hash: &H256Json, + storage: &Storage, + ) -> MmResult { + utxo_common::utxo_tx_history_v2_common::tx_from_storage_or_rpc(self, tx_hash, storage).await + } + + async fn request_tx_history( + &self, + metrics: MetricsArc, + for_addresses: &HashSet
, + ) -> RequestTxHistoryResult { + utxo_common::utxo_tx_history_v2_common::request_tx_history(self, metrics, for_addresses).await + } + + async fn get_block_timestamp(&self, height: u64) -> MmResult { + self.get_block_timestamp(height).await + } + + async fn my_addresses_balances(&self) -> BalanceResult> { + let my_address = self.my_address().map_to_mm(BalanceError::Internal)?; + let my_balance = self.my_balance().compat().await?; + Ok(std::iter::once((my_address, my_balance.into_total())).collect()) + } + + fn address_from_str(&self, address: &str) -> MmResult { + utxo_common::checked_address_from_str(self, address) + } + + fn set_history_sync_state(&self, new_state: HistorySyncState) { + *self.as_ref().history_sync_state.lock().unwrap() = new_state; + } } // testnet @@ -1320,17 +1334,9 @@ pub fn bch_coin_for_test() -> BchCoin { #[cfg(test)] mod bch_tests { use super::*; - use crate::tx_history_storage::TxHistoryStorageBuilder; + use crate::my_tx_history_v2::for_tests::init_storage_for; use crate::{TransactionType, TxFeeDetails}; use common::block_on; - use mm2_test_helpers::for_tests::mm_ctx_with_custom_db; - - fn init_storage_for(coin: &Coin) -> (MmArc, impl TxHistoryStorage) { - let ctx = mm_ctx_with_custom_db(); - let storage = TxHistoryStorageBuilder::new(&ctx).build().unwrap(); - block_on(storage.init(&coin.history_wallet_id())).unwrap(); - (ctx, storage) - } #[test] fn test_get_slp_genesis_params() { @@ -1351,7 +1357,8 @@ mod bch_tests { let hash = "a8dcc3c6776e93e7bd21fb81551e853447c55e2d8ac141b418583bc8095ce390".into(); let tx = block_on(coin.tx_from_storage_or_rpc(&hash, &storage)).unwrap(); - let details = block_on(coin.bch_tx_details(&hash, &tx, None, &storage)).unwrap(); + let my_addresses = block_on(coin.my_addresses()).unwrap(); + let details = block_on(coin.bch_tx_details(&hash, &tx, None, &storage, &my_addresses)).unwrap(); let expected_total: BigDecimal = "0.11407782".parse().unwrap(); assert_eq!(expected_total, details.total_amount); @@ -1395,7 +1402,9 @@ mod bch_tests { let slp_details = parse_slp_script(&tx.outputs[0].script_pubkey).unwrap(); - let slp_tx_details = block_on(coin.slp_tx_details(&tx, slp_details.transaction, None, None, &storage)).unwrap(); + let my_addresses = block_on(coin.my_addresses()).unwrap(); + let slp_tx_details = + block_on(coin.slp_tx_details(&tx, slp_details.transaction, None, None, &storage, &my_addresses)).unwrap(); let expected_total: BigDecimal = "6.2974".parse().unwrap(); assert_eq!(expected_total, slp_tx_details.total_amount); diff --git a/mm2src/coins/utxo/bch_and_slp_tx_history.rs b/mm2src/coins/utxo/bch_and_slp_tx_history.rs deleted file mode 100644 index 65b14e1a69..0000000000 --- a/mm2src/coins/utxo/bch_and_slp_tx_history.rs +++ /dev/null @@ -1,406 +0,0 @@ -/// This module is named bch_and_slp_tx_history temporary. We will most likely use the same approach for every -/// supported UTXO coin. -use super::RequestTxHistoryResult; -use crate::my_tx_history_v2::{CoinWithTxHistoryV2, TxHistoryStorage}; -use crate::utxo::bch::BchCoin; -use crate::utxo::utxo_common; -use crate::utxo::UtxoStandardOps; -use crate::{BlockHeightAndTime, HistorySyncState, MarketCoinOps}; -use async_trait::async_trait; -use common::executor::Timer; -use common::log::{error, info}; -use common::state_machine::prelude::*; -use futures::compat::Future01CompatExt; -use mm2_metrics::MetricsArc; -use mm2_number::BigDecimal; -use rpc::v1::types::H256 as H256Json; -use std::collections::HashMap; -use std::str::FromStr; - -struct BchAndSlpHistoryCtx { - coin: BchCoin, - storage: Storage, - metrics: MetricsArc, - current_balance: BigDecimal, -} - -// States have to be generic over storage type because BchAndSlpHistoryCtx is generic over it -struct Init { - phantom: std::marker::PhantomData, -} - -impl Init { - fn new() -> Self { - Init { - phantom: Default::default(), - } - } -} - -impl TransitionFrom> for Stopped {} - -#[async_trait] -impl State for Init { - type Ctx = BchAndSlpHistoryCtx; - type Result = (); - - async fn on_changed(self: Box, ctx: &mut BchAndSlpHistoryCtx) -> StateResult, ()> { - *ctx.coin.as_ref().history_sync_state.lock().unwrap() = HistorySyncState::NotStarted; - - if let Err(e) = ctx.storage.init(&ctx.coin.history_wallet_id()).await { - return Self::change_state(Stopped::storage_error(e)); - } - - Self::change_state(FetchingTxHashes::new()) - } -} - -// States have to be generic over storage type because BchAndSlpHistoryCtx is generic over it -struct FetchingTxHashes { - phantom: std::marker::PhantomData, -} - -impl FetchingTxHashes { - fn new() -> Self { - FetchingTxHashes { - phantom: Default::default(), - } - } -} - -impl TransitionFrom> for FetchingTxHashes {} -impl TransitionFrom> for FetchingTxHashes {} -impl TransitionFrom> for FetchingTxHashes {} - -#[async_trait] -impl State for FetchingTxHashes { - type Ctx = BchAndSlpHistoryCtx; - type Result = (); - - async fn on_changed(self: Box, ctx: &mut BchAndSlpHistoryCtx) -> StateResult, ()> { - let wallet_id = ctx.coin.history_wallet_id(); - if let Err(e) = ctx.storage.init(&wallet_id).await { - return Self::change_state(Stopped::storage_error(e)); - } - - let maybe_tx_ids = ctx.coin.request_tx_history(ctx.metrics.clone()).await; - match maybe_tx_ids { - RequestTxHistoryResult::Ok(all_tx_ids_with_height) => { - let in_storage = match ctx.storage.unique_tx_hashes_num_in_history(&wallet_id).await { - Ok(num) => num, - Err(e) => return Self::change_state(Stopped::storage_error(e)), - }; - if all_tx_ids_with_height.len() > in_storage { - let txes_left = all_tx_ids_with_height.len() - in_storage; - *ctx.coin.as_ref().history_sync_state.lock().unwrap() = - HistorySyncState::InProgress(json!({ "transactions_left": txes_left })); - } - - Self::change_state(UpdatingUnconfirmedTxes::new(all_tx_ids_with_height)) - }, - RequestTxHistoryResult::HistoryTooLarge => Self::change_state(Stopped::::history_too_large()), - RequestTxHistoryResult::Retry { error } => { - error!("Error {} on requesting tx history for {}", error, ctx.coin.ticker()); - Self::change_state(OnIoErrorCooldown::new()) - }, - RequestTxHistoryResult::CriticalError(e) => { - error!( - "Critical error {} on requesting tx history for {}", - e, - ctx.coin.ticker() - ); - Self::change_state(Stopped::::unknown(e)) - }, - } - } -} - -// States have to be generic over storage type because BchAndSlpHistoryCtx is generic over it -struct OnIoErrorCooldown { - phantom: std::marker::PhantomData, -} - -impl OnIoErrorCooldown { - fn new() -> Self { - OnIoErrorCooldown { - phantom: Default::default(), - } - } -} - -impl TransitionFrom> for OnIoErrorCooldown {} -impl TransitionFrom> for OnIoErrorCooldown {} -impl TransitionFrom> for OnIoErrorCooldown {} - -#[async_trait] -impl State for OnIoErrorCooldown { - type Ctx = BchAndSlpHistoryCtx; - type Result = (); - - async fn on_changed(self: Box, _ctx: &mut BchAndSlpHistoryCtx) -> StateResult, ()> { - Timer::sleep(30.).await; - Self::change_state(FetchingTxHashes::new()) - } -} - -// States have to be generic over storage type because BchAndSlpHistoryCtx is generic over it -struct WaitForHistoryUpdateTrigger { - phantom: std::marker::PhantomData, -} - -impl WaitForHistoryUpdateTrigger { - fn new() -> Self { - WaitForHistoryUpdateTrigger { - phantom: Default::default(), - } - } -} - -impl TransitionFrom> for WaitForHistoryUpdateTrigger {} - -#[async_trait] -impl State for WaitForHistoryUpdateTrigger { - type Ctx = BchAndSlpHistoryCtx; - type Result = (); - - async fn on_changed(self: Box, ctx: &mut BchAndSlpHistoryCtx) -> StateResult { - let wallet_id = ctx.coin.history_wallet_id(); - loop { - Timer::sleep(30.).await; - match ctx.storage.history_contains_unconfirmed_txes(&wallet_id).await { - Ok(contains) => { - if contains { - return Self::change_state(FetchingTxHashes::new()); - } - }, - Err(e) => return Self::change_state(Stopped::storage_error(e)), - } - - match ctx.coin.my_balance().compat().await { - Ok(balance) => { - let total_balance = balance.into_total(); - if ctx.current_balance != total_balance { - ctx.current_balance = total_balance; - return Self::change_state(FetchingTxHashes::new()); - } - }, - Err(e) => { - error!("Error {} on balance fetching for the coin {}", e, ctx.coin.ticker()); - }, - } - } - } -} - -// States have to be generic over storage type because BchAndSlpHistoryCtx is generic over it -struct UpdatingUnconfirmedTxes { - phantom: std::marker::PhantomData, - all_tx_ids_with_height: Vec<(H256Json, u64)>, -} - -impl UpdatingUnconfirmedTxes { - fn new(all_tx_ids_with_height: Vec<(H256Json, u64)>) -> Self { - UpdatingUnconfirmedTxes { - phantom: Default::default(), - all_tx_ids_with_height, - } - } -} - -impl TransitionFrom> for UpdatingUnconfirmedTxes {} - -#[async_trait] -impl State for UpdatingUnconfirmedTxes { - type Ctx = BchAndSlpHistoryCtx; - type Result = (); - - async fn on_changed(self: Box, ctx: &mut BchAndSlpHistoryCtx) -> StateResult, ()> { - let wallet_id = ctx.coin.history_wallet_id(); - match ctx.storage.get_unconfirmed_txes_from_history(&wallet_id).await { - Ok(unconfirmed) => { - let txs_with_height: HashMap = self.all_tx_ids_with_height.clone().into_iter().collect(); - for mut tx in unconfirmed { - let found = match H256Json::from_str(&tx.tx_hash) { - Ok(unconfirmed_tx_hash) => txs_with_height.get(&unconfirmed_tx_hash), - Err(_) => None, - }; - - match found { - Some(height) => { - if *height > 0 { - match ctx.coin.get_block_timestamp(*height).await { - Ok(time) => tx.timestamp = time, - Err(_) => return Self::change_state(OnIoErrorCooldown::new()), - }; - tx.block_height = *height; - if let Err(e) = ctx.storage.update_tx_in_history(&wallet_id, &tx).await { - return Self::change_state(Stopped::storage_error(e)); - } - } - }, - None => { - // This can potentially happen when unconfirmed tx is removed from mempool for some reason. - // Or if the hash is undecodable. We should remove it from storage too. - if let Err(e) = ctx.storage.remove_tx_from_history(&wallet_id, &tx.internal_id).await { - return Self::change_state(Stopped::storage_error(e)); - } - }, - } - } - Self::change_state(FetchingTransactionsData::new(self.all_tx_ids_with_height)) - }, - Err(e) => Self::change_state(Stopped::storage_error(e)), - } - } -} - -// States have to be generic over storage type because BchAndSlpHistoryCtx is generic over it -struct FetchingTransactionsData { - phantom: std::marker::PhantomData, - all_tx_ids_with_height: Vec<(H256Json, u64)>, -} - -impl TransitionFrom> for FetchingTransactionsData {} - -impl FetchingTransactionsData { - fn new(all_tx_ids_with_height: Vec<(H256Json, u64)>) -> Self { - FetchingTransactionsData { - phantom: Default::default(), - all_tx_ids_with_height, - } - } -} - -#[async_trait] -impl State for FetchingTransactionsData { - type Ctx = BchAndSlpHistoryCtx; - type Result = (); - - async fn on_changed(self: Box, ctx: &mut BchAndSlpHistoryCtx) -> StateResult, ()> { - let wallet_id = ctx.coin.history_wallet_id(); - for (tx_hash, height) in self.all_tx_ids_with_height { - let tx_hash_string = format!("{:02x}", tx_hash); - match ctx.storage.history_has_tx_hash(&wallet_id, &tx_hash_string).await { - Ok(true) => continue, - Ok(false) => (), - Err(e) => return Self::change_state(Stopped::storage_error(e)), - } - - let block_height_and_time = if height > 0 { - let timestamp = match ctx.coin.get_block_timestamp(height).await { - Ok(time) => time, - Err(_) => return Self::change_state(OnIoErrorCooldown::new()), - }; - Some(BlockHeightAndTime { height, timestamp }) - } else { - None - }; - let tx_details = match ctx - .coin - .transaction_details_with_token_transfers(&tx_hash, block_height_and_time, &ctx.storage) - .await - { - Ok(tx) => tx, - Err(e) => { - error!( - "Error {:?} on getting {} tx details for hash {:02x}", - e, - ctx.coin.ticker(), - tx_hash - ); - return Self::change_state(OnIoErrorCooldown::new()); - }, - }; - - if let Err(e) = ctx.storage.add_transactions_to_history(&wallet_id, tx_details).await { - return Self::change_state(Stopped::storage_error(e)); - } - - // wait for for one second to reduce the number of requests to electrum servers - Timer::sleep(1.).await; - } - info!("Tx history fetching finished for {}", ctx.coin.ticker()); - *ctx.coin.as_ref().history_sync_state.lock().unwrap() = HistorySyncState::Finished; - Self::change_state(WaitForHistoryUpdateTrigger::new()) - } -} - -#[derive(Debug)] -enum StopReason { - HistoryTooLarge, - StorageError(E), - UnknownError(String), -} - -struct Stopped { - phantom: std::marker::PhantomData, - stop_reason: StopReason, -} - -impl Stopped { - fn history_too_large() -> Self { - Stopped { - phantom: Default::default(), - stop_reason: StopReason::HistoryTooLarge, - } - } - - fn storage_error(e: E) -> Self { - Stopped { - phantom: Default::default(), - stop_reason: StopReason::StorageError(e), - } - } - - fn unknown(e: String) -> Self { - Stopped { - phantom: Default::default(), - stop_reason: StopReason::UnknownError(e), - } - } -} - -impl TransitionFrom> for Stopped {} -impl TransitionFrom> for Stopped {} -impl TransitionFrom> for Stopped {} -impl TransitionFrom> for Stopped {} - -#[async_trait] -impl LastState for Stopped { - type Ctx = BchAndSlpHistoryCtx; - type Result = (); - - async fn on_changed(self: Box, ctx: &mut Self::Ctx) -> Self::Result { - info!( - "Stopping tx history fetching for {}. Reason: {:?}", - ctx.coin.ticker(), - self.stop_reason - ); - let new_state_json = match self.stop_reason { - StopReason::HistoryTooLarge => json!({ - "code": utxo_common::HISTORY_TOO_LARGE_ERR_CODE, - "message": "Got `history too large` error from Electrum server. History is not available", - }), - reason => json!({ - "message": format!("{:?}", reason), - }), - }; - *ctx.coin.as_ref().history_sync_state.lock().unwrap() = HistorySyncState::Error(new_state_json); - } -} - -pub async fn bch_and_slp_history_loop( - coin: BchCoin, - storage: impl TxHistoryStorage, - metrics: MetricsArc, - current_balance: BigDecimal, -) { - let ctx = BchAndSlpHistoryCtx { - coin, - storage, - metrics, - current_balance, - }; - let state_machine: StateMachine<_, ()> = StateMachine::from_ctx(ctx); - state_machine.run(Init::new()).await; -} diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index bd1aa1efd3..2d61bd5fe9 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -2,8 +2,9 @@ use super::*; use crate::coin_balance::{self, EnableCoinBalanceError, EnabledCoinBalanceParams, HDAccountBalance, HDAddressBalance, HDWalletBalance, HDWalletBalanceOps}; use crate::hd_pubkey::{ExtractExtendedPubkey, HDExtractPubkeyError, HDXPubExtractor}; -use crate::hd_wallet::{AccountUpdatingError, AddressDerivingError, HDAccountMut, NewAccountCreatingError}; +use crate::hd_wallet::{AccountUpdatingError, AddressDerivingResult, HDAccountMut, NewAccountCreatingError}; use crate::hd_wallet_storage::HDWalletCoinWithStorageOps; +use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHistoryTarget, TxHistoryStorage}; use crate::rpc_command::account_balance::{self, AccountBalanceParams, AccountBalanceRpcOps, HDAccountBalanceResponse}; use crate::rpc_command::get_new_address::{self, GetNewAddressParams, GetNewAddressResponse, GetNewAddressRpcError, GetNewAddressRpcOps}; @@ -14,9 +15,12 @@ use crate::rpc_command::init_create_account::{self, CreateAccountRpcError, Creat use crate::rpc_command::init_scan_for_new_addresses::{self, InitScanAddressesRpcOps, ScanAddressesParams, ScanAddressesResponse}; use crate::rpc_command::init_withdraw::{InitWithdrawCoin, WithdrawTaskHandle}; +use crate::tx_history_storage::{GetTxHistoryFilters, WalletId}; use crate::utxo::utxo_builder::{BlockHeaderUtxoArcOps, MergeUtxoArcOps, UtxoCoinBuildError, UtxoCoinBuilder, UtxoCoinBuilderCommonOps, UtxoFieldsWithHardwareWalletBuilder, UtxoFieldsWithIguanaPrivKeyBuilder}; +use crate::utxo::utxo_tx_history_v2::{UtxoMyAddressesHistoryError, UtxoTxDetailsError, UtxoTxDetailsParams, + UtxoTxHistoryOps}; use crate::{eth, CanRefundHtlc, CoinBalance, CoinWithDerivationMethod, DelegationError, DelegationFut, GetWithdrawSenderAddress, NegotiateSwapContractAddrErr, PrivKeyBuildPolicy, SearchForSwapTxSpendInput, SignatureResult, StakingInfosFut, SwapOps, TradePreimageValue, TransactionFut, TxMarshalingErr, @@ -393,7 +397,7 @@ impl UtxoCommonOps for QtumCoin { utxo_common::my_public_key(self.as_ref()) } - fn address_from_str(&self, address: &str) -> Result { + fn address_from_str(&self, address: &str) -> MmResult { utxo_common::checked_address_from_str(self, address) } @@ -952,7 +956,7 @@ impl HDWalletCoinOps for QtumCoin { &self, hd_account: &Self::HDAccount, address_ids: Ids, - ) -> MmResult>, AddressDerivingError> + ) -> AddressDerivingResult>> where Ids: Iterator + Send, { @@ -1092,6 +1096,67 @@ impl InitCreateAccountRpcOps for QtumCoin { } } +#[async_trait] +impl CoinWithTxHistoryV2 for QtumCoin { + fn history_wallet_id(&self) -> WalletId { utxo_common::utxo_tx_history_v2_common::history_wallet_id(self.as_ref()) } + + async fn get_tx_history_filters( + &self, + target: MyTxHistoryTarget, + ) -> MmResult { + utxo_common::utxo_tx_history_v2_common::get_tx_history_filters(self, target).await + } +} + +#[async_trait] +impl UtxoTxHistoryOps for QtumCoin { + async fn my_addresses(&self) -> MmResult, UtxoMyAddressesHistoryError> { + utxo_common::utxo_tx_history_v2_common::my_addresses(self).await + } + + async fn tx_details_by_hash( + &self, + params: UtxoTxDetailsParams<'_, Storage>, + ) -> MmResult, UtxoTxDetailsError> + where + Storage: TxHistoryStorage, + { + utxo_common::utxo_tx_history_v2_common::tx_details_by_hash(self, params).await + } + + async fn tx_from_storage_or_rpc( + &self, + tx_hash: &H256Json, + storage: &Storage, + ) -> MmResult { + utxo_common::utxo_tx_history_v2_common::tx_from_storage_or_rpc(self, tx_hash, storage).await + } + + async fn request_tx_history( + &self, + metrics: MetricsArc, + for_addresses: &HashSet
, + ) -> RequestTxHistoryResult { + utxo_common::utxo_tx_history_v2_common::request_tx_history(self, metrics, for_addresses).await + } + + async fn get_block_timestamp(&self, height: u64) -> MmResult { + self.as_ref().rpc_client.get_block_timestamp(height).await + } + + async fn my_addresses_balances(&self) -> BalanceResult> { + utxo_common::utxo_tx_history_v2_common::my_addresses_balances(self).await + } + + fn address_from_str(&self, address: &str) -> MmResult { + utxo_common::checked_address_from_str(self, address) + } + + fn set_history_sync_state(&self, new_state: HistorySyncState) { + *self.as_ref().history_sync_state.lock().unwrap() = new_state; + } +} + /// Parse contract address (H160) from string. /// Qtum Contract addresses have another checksum verification algorithm, because of this do not use [`eth::valid_addr_from_str`]. pub fn contract_addr_from_str(addr: &str) -> Result { eth::addr_from_str(addr) } diff --git a/mm2src/coins/utxo/rpc_clients.rs b/mm2src/coins/utxo/rpc_clients.rs index d3419fa6d6..f7aaf8f62f 100644 --- a/mm2src/coins/utxo/rpc_clients.rs +++ b/mm2src/coins/utxo/rpc_clients.rs @@ -75,6 +75,7 @@ pub type JsonRpcPendingRequestsShared = Arc>; pub type JsonRpcPendingRequests = HashMap>; pub type UnspentMap = HashMap>; +type ElectrumTxHistory = Vec; type ElectrumScriptHash = String; type ScriptHashUnspents = Vec; @@ -1835,10 +1836,22 @@ impl ElectrumClient { } /// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-scripthash-get-history - pub fn scripthash_get_history(&self, hash: &str) -> RpcRes> { + pub fn scripthash_get_history(&self, hash: &str) -> RpcRes { rpc_func!(self, "blockchain.scripthash.get_history", hash) } + /// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-scripthash-get-history + /// Requests history of the `hashes` in a batch and returns them in the same order they were requested. + pub fn scripthash_get_history_batch(&self, hashes: I) -> RpcRes> + where + I: IntoIterator, + { + let requests = hashes + .into_iter() + .map(|hash| rpc_req!(self, "blockchain.scripthash.get_history", hash)); + self.batch_rpc(requests) + } + /// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-scripthash-gethistory pub fn scripthash_get_balance(&self, hash: &str) -> RpcRes { let arc = self.clone(); diff --git a/mm2src/coins/utxo/slp.rs b/mm2src/coins/utxo/slp.rs index 0112508cd8..77517e32c3 100644 --- a/mm2src/coins/utxo/slp.rs +++ b/mm2src/coins/utxo/slp.rs @@ -3,7 +3,7 @@ //! Tracking issue: https://github.com/KomodoPlatform/atomicDEX-API/issues/701 //! More info about the protocol and implementation guides can be found at https://slp.dev/ -use crate::my_tx_history_v2::CoinWithTxHistoryV2; +use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHistoryTarget}; use crate::tx_history_storage::{GetTxHistoryFilters, WalletId}; use crate::utxo::bch::BchCoin; use crate::utxo::bchd_grpc::{check_slp_transaction, validate_slp_utxos, ValidateSlpUtxosErr}; @@ -1749,11 +1749,22 @@ impl MmCoin for SlpToken { fn is_coin_protocol_supported(&self, _info: &Option>) -> bool { true } } +#[async_trait] impl CoinWithTxHistoryV2 for SlpToken { fn history_wallet_id(&self) -> WalletId { WalletId::new(self.platform_ticker().to_owned()) } - fn get_tx_history_filters(&self) -> GetTxHistoryFilters { - GetTxHistoryFilters::new().with_token_id(self.token_id().to_string()) + /// TODO consider using `utxo_common::utxo_tx_history_common::get_tx_history_filters` + /// when `SlpToken` implements `CoinWithDerivationMethod`. + async fn get_tx_history_filters( + &self, + target: MyTxHistoryTarget, + ) -> MmResult { + match target { + MyTxHistoryTarget::Iguana => (), + target => return MmError::err(MyTxHistoryErrorV2::with_expected_target(target, "Iguana")), + } + let my_address = self.my_address().map_to_mm(MyTxHistoryErrorV2::Internal)?; + Ok(GetTxHistoryFilters::for_address(my_address).with_token_id(self.token_id().to_string())) } } diff --git a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs index 689007961b..5cb9bfe11d 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs @@ -26,7 +26,7 @@ pub use keys::{Address, AddressFormat as UtxoAddressFormat, AddressHashEnum, Key Type as ScriptType}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; -use primitives::hash::H256; +use primitives::hash::{H160, H256}; use rand::seq::SliceRandom; use serde_json::{self as json, Value as Json}; use spv_validation::storage::{BlockHeaderStorageError, BlockHeaderStorageOps}; @@ -190,7 +190,7 @@ pub trait UtxoFieldsWithHardwareWalletBuilder: UtxoCoinBuilderCommonOps { if !self.supports_trezor(&conf) { return MmError::err(UtxoCoinBuildError::CoinDoesntSupportTrezor); } - self.check_if_trezor_is_initialized()?; + let hd_wallet_rmd160 = self.trezor_wallet_rmd160()?; // For now, use a default script pubkey. // TODO change the type of `recently_spent_outpoints` to `AsyncMutex>` @@ -207,6 +207,7 @@ pub trait UtxoFieldsWithHardwareWalletBuilder: UtxoCoinBuilderCommonOps { .await?; let gap_limit = self.gap_limit(); let hd_wallet = UtxoHDWallet { + hd_wallet_rmd160, hd_wallet_storage, address_format, derivation_path, @@ -266,6 +267,16 @@ pub trait UtxoFieldsWithHardwareWalletBuilder: UtxoCoinBuilderCommonOps { fn supports_trezor(&self, conf: &UtxoCoinConf) -> bool { conf.trezor_coin.is_some() } + fn trezor_wallet_rmd160(&self) -> UtxoCoinBuildResult { + let crypto_ctx = CryptoCtx::from_ctx(self.ctx())?; + let hw_ctx = crypto_ctx + .hw_ctx() + .or_mm_err(|| UtxoCoinBuildError::HwContextNotInitialized)?; + match hw_ctx.hw_wallet_type() { + HwWalletType::Trezor => Ok(hw_ctx.rmd160()), + } + } + fn check_if_trezor_is_initialized(&self) -> UtxoCoinBuildResult<()> { let crypto_ctx = CryptoCtx::from_ctx(self.ctx())?; let hw_ctx = crypto_ctx diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index 0fe89f7c2b..d2b5bdb44c 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -1,7 +1,7 @@ use super::*; use crate::coin_balance::{AddressBalanceStatus, HDAddressBalance, HDWalletBalanceOps}; use crate::hd_pubkey::{ExtractExtendedPubkey, HDExtractPubkeyError, HDXPubExtractor}; -use crate::hd_wallet::{AccountUpdatingError, AddressDerivingError, HDAccountMut, HDAccountsMap, +use crate::hd_wallet::{AccountUpdatingError, AddressDerivingResult, HDAccountMut, HDAccountsMap, NewAccountCreatingError}; use crate::hd_wallet_storage::{HDWalletCoinWithStorageOps, HDWalletStorageResult}; use crate::rpc_command::init_withdraw::WithdrawTaskHandle; @@ -50,8 +50,7 @@ use utxo_signer::UtxoSignerOps; pub use chain::Transaction as UtxoTx; -#[cfg(not(target_arch = "wasm32"))] -use crate::TxFeeDetails::Utxo; +pub mod utxo_tx_history_v2_common; pub const DEFAULT_FEE_VOUT: usize = 0; pub const DEFAULT_SWAP_TX_SPEND_SIZE: u64 = 305; @@ -104,7 +103,7 @@ fn derive_address_with_cache( hd_account: &UtxoHDAccount, hd_addresses_cache: &mut HashMap, hd_address_id: HDAddressId, -) -> MmResult +) -> AddressDerivingResult where T: UtxoCommonOps, { @@ -148,7 +147,7 @@ pub async fn derive_addresses( coin: &T, hd_account: &UtxoHDAccount, address_ids: Ids, -) -> MmResult, AddressDerivingError> +) -> AddressDerivingResult> where T: UtxoCommonOps, Ids: Iterator, @@ -177,7 +176,7 @@ pub async fn derive_addresses( coin: &T, hd_account: &UtxoHDAccount, address_ids: Ids, -) -> MmResult, AddressDerivingError> +) -> AddressDerivingResult> where T: UtxoCommonOps, Ids: Iterator, @@ -611,31 +610,36 @@ where coin.my_spendable_balance() } -pub fn address_from_str_unchecked(coin: &UtxoCoinFields, address: &str) -> Result { - if let Ok(legacy) = Address::from_str(address) { - return Ok(legacy); - } +pub fn address_from_str_unchecked(coin: &UtxoCoinFields, address: &str) -> MmResult { + let mut errors = Vec::with_capacity(3); + + match Address::from_str(address) { + Ok(legacy) => return Ok(legacy), + Err(e) => errors.push(e.to_string()), + }; - if let Ok(segwit) = Address::from_segwitaddress( + match Address::from_segwitaddress( address, coin.conf.checksum_type, coin.conf.pub_addr_prefix, coin.conf.pub_t_addr_prefix, ) { - return Ok(segwit); + Ok(segwit) => return Ok(segwit), + Err(e) => errors.push(e), } - if let Ok(cashaddress) = Address::from_cashaddress( + match Address::from_cashaddress( address, coin.conf.checksum_type, coin.conf.pub_addr_prefix, coin.conf.p2sh_addr_prefix, coin.conf.pub_t_addr_prefix, ) { - return Ok(cashaddress); + Ok(cashaddress) => return Ok(cashaddress), + Err(e) => errors.push(e), } - return ERR!("Invalid address: {}", address); + MmError::err(AddrFromStrError::CannotDetermineFormat(errors)) } pub fn my_public_key(coin: &UtxoCoinFields) -> Result<&Public, MmError> { @@ -646,9 +650,9 @@ pub fn my_public_key(coin: &UtxoCoinFields) -> Result<&Public, MmError(coin: &T, address: &str) -> Result { - let addr = try_s!(address_from_str_unchecked(coin.as_ref(), address)); - try_s!(check_withdraw_address_supported(coin, &addr)); +pub fn checked_address_from_str(coin: &T, address: &str) -> MmResult { + let addr = address_from_str_unchecked(coin.as_ref(), address)?; + check_withdraw_address_supported(coin, &addr)?; Ok(addr) } @@ -1791,7 +1795,7 @@ pub fn verify_message( let message_hash = sign_message_hash(coin.as_ref(), message).ok_or(VerificationError::PrefixNotFound)?; let signature = CompactSignature::from(base64::decode(signature_base64)?); let recovered_pubkey = Public::recover_compact(&H256::from(message_hash), &signature)?; - let received_address = checked_address_from_str(coin, address).map_err(VerificationError::AddressDecodingError)?; + let received_address = checked_address_from_str(coin, address)?; Ok(AddressHashEnum::from(recovered_pubkey.address_hash()) == received_address.hash) } @@ -2092,7 +2096,7 @@ pub fn validate_address(coin: &T, address: &str) -> ValidateAd Err(e) => { return ValidateAddressResult { is_valid: false, - reason: Some(e), + reason: Some(e.to_string()), } }, }; @@ -2141,7 +2145,7 @@ where let to_write: Vec = history .into_iter() .filter_map(|mut tx| match tx.fee_details { - Some(Utxo(ref mut fee_details)) => { + Some(TxFeeDetails::Utxo(ref mut fee_details)) => { if fee_details.coin.is_none() { fee_details.coin = Some(String::from(&tx.coin)); updated = true; @@ -3642,7 +3646,7 @@ pub fn addr_format_for_standard_scripts(coin: &dyn AsRef) -> Utx } } -fn check_withdraw_address_supported(coin: &T, addr: &Address) -> Result<(), MmError> +fn check_withdraw_address_supported(coin: &T, addr: &Address) -> MmResult<(), UnsupportedAddr> where T: UtxoCommonOps, { diff --git a/mm2src/coins/utxo/utxo_common/utxo_tx_history_v2_common.rs b/mm2src/coins/utxo/utxo_common/utxo_tx_history_v2_common.rs new file mode 100644 index 0000000000..1f5c16da5d --- /dev/null +++ b/mm2src/coins/utxo/utxo_common/utxo_tx_history_v2_common.rs @@ -0,0 +1,417 @@ +use crate::coin_balance::CoinBalanceReportOps; +use crate::hd_wallet::{HDAccountOps, HDWalletCoinOps, HDWalletOps}; +use crate::my_tx_history_v2::{CoinWithTxHistoryV2, DisplayAddress, MyTxHistoryErrorV2, MyTxHistoryTarget, + TxDetailsBuilder, TxHistoryStorage}; +use crate::tx_history_storage::{GetTxHistoryFilters, WalletId}; +use crate::utxo::rpc_clients::{electrum_script_hash, ElectrumClient, NativeClient, UtxoRpcClientEnum}; +use crate::utxo::utxo_common::{big_decimal_from_sat, HISTORY_TOO_LARGE_ERROR}; +use crate::utxo::utxo_tx_history_v2::{UtxoMyAddressesHistoryError, UtxoTxDetailsError, UtxoTxDetailsParams, + UtxoTxHistoryOps}; +use crate::utxo::{output_script, RequestTxHistoryResult, UtxoCoinFields, UtxoCommonOps, UtxoHDAccount}; +use crate::{big_decimal_from_sat_unsigned, compare_transactions, BalanceResult, CoinWithDerivationMethod, + DerivationMethod, HDAccountAddressId, MarketCoinOps, TransactionDetails, TxFeeDetails, TxIdHeight, + UtxoFeeDetails, UtxoTx}; +use common::jsonrpc_client::JsonRpcErrorType; +use crypto::Bip44Chain; +use futures::compat::Future01CompatExt; +use itertools::Itertools; +use keys::{Address, Type as ScriptType}; +use mm2_err_handle::prelude::*; +use mm2_metrics::MetricsArc; +use mm2_number::BigDecimal; +use rpc::v1::types::{TransactionInputEnum, H256 as H256Json}; +use serialization::deserialize; +use std::collections::{HashMap, HashSet}; +use std::convert::TryFrom; +use std::iter; + +/// [`CoinWithTxHistoryV2::history_wallet_id`] implementation. +pub fn history_wallet_id(coin: &UtxoCoinFields) -> WalletId { WalletId::new(coin.conf.ticker.clone()) } + +/// [`CoinWithTxHistoryV2::get_tx_history_filters`] implementation. +/// Returns `GetTxHistoryFilters` according to the derivation method. +pub async fn get_tx_history_filters( + coin: &Coin, + target: MyTxHistoryTarget, +) -> MmResult +where + Coin: CoinWithDerivationMethod::HDWallet> + + HDWalletCoinOps + + MarketCoinOps + + Sync, + ::Address: DisplayAddress, +{ + match (coin.derivation_method(), target) { + (DerivationMethod::Iguana(_), MyTxHistoryTarget::Iguana) => { + let my_address = coin.my_address().map_to_mm(MyTxHistoryErrorV2::Internal)?; + Ok(GetTxHistoryFilters::for_address(my_address)) + }, + (DerivationMethod::Iguana(_), target) => { + MmError::err(MyTxHistoryErrorV2::with_expected_target(target, "Iguana")) + }, + (DerivationMethod::HDWallet(hd_wallet), MyTxHistoryTarget::AccountId { account_id }) => { + get_tx_history_filters_for_hd_account(coin, hd_wallet, account_id).await + }, + (DerivationMethod::HDWallet(hd_wallet), MyTxHistoryTarget::AddressId(hd_address_id)) => { + get_tx_history_filters_for_hd_address(coin, hd_wallet, hd_address_id).await + }, + (DerivationMethod::HDWallet(hd_wallet), MyTxHistoryTarget::AddressDerivationPath(derivation_path)) => { + let hd_address_id = HDAccountAddressId::from(derivation_path); + get_tx_history_filters_for_hd_address(coin, hd_wallet, hd_address_id).await + }, + (DerivationMethod::HDWallet(_), target) => MmError::err(MyTxHistoryErrorV2::with_expected_target( + target, + "an HD account/address", + )), + } +} + +/// `get_tx_history_filters` function's helper. +async fn get_tx_history_filters_for_hd_account( + coin: &Coin, + hd_wallet: &Coin::HDWallet, + account_id: u32, +) -> MmResult +where + Coin: HDWalletCoinOps + Sync, + Coin::Address: DisplayAddress, +{ + let hd_account = hd_wallet + .get_account(account_id) + .await + .or_mm_err(|| MyTxHistoryErrorV2::InvalidTarget(format!("No such account_id={account_id}")))?; + + let external_addresses = coin.derive_known_addresses(&hd_account, Bip44Chain::External).await?; + let internal_addresses = coin.derive_known_addresses(&hd_account, Bip44Chain::Internal).await?; + + let addresses_iter = external_addresses + .into_iter() + .chain(internal_addresses) + .map(|hd_address| DisplayAddress::display_address(&hd_address.address)); + Ok(GetTxHistoryFilters::for_addresses(addresses_iter)) +} + +/// `get_tx_history_filters` function's helper. +async fn get_tx_history_filters_for_hd_address( + coin: &Coin, + hd_wallet: &Coin::HDWallet, + hd_address_id: HDAccountAddressId, +) -> MmResult +where + Coin: HDWalletCoinOps + Sync, + Coin::Address: DisplayAddress, +{ + let hd_account = hd_wallet + .get_account(hd_address_id.account_id) + .await + .or_mm_err(|| MyTxHistoryErrorV2::InvalidTarget(format!("No such account_id={}", hd_address_id.account_id)))?; + + let is_address_activated = hd_account.is_address_activated(hd_address_id.chain, hd_address_id.address_id)?; + if !is_address_activated { + let error = format!( + "'{:?}:{}' address is not activated", + hd_address_id.chain, hd_address_id.address_id + ); + return MmError::err(MyTxHistoryErrorV2::InvalidTarget(error)); + } + + let hd_address = coin + .derive_address(&hd_account, hd_address_id.chain, hd_address_id.address_id) + .await?; + Ok(GetTxHistoryFilters::for_address(hd_address.address.display_address())) +} + +/// [`UtxoTxHistoryOps::my_addresses`] implementation. +pub async fn my_addresses(coin: &Coin) -> MmResult, UtxoMyAddressesHistoryError> +where + Coin: HDWalletCoinOps
+ UtxoCommonOps, +{ + const ADDRESSES_CAPACITY: usize = 60; + + match coin.as_ref().derivation_method { + DerivationMethod::Iguana(ref my_address) => Ok(iter::once(my_address.clone()).collect()), + DerivationMethod::HDWallet(ref hd_wallet) => { + let hd_accounts = hd_wallet.get_accounts().await; + + let mut all_addresses = HashSet::with_capacity(ADDRESSES_CAPACITY); + for (_, hd_account) in hd_accounts { + let external_addresses = coin.derive_known_addresses(&hd_account, Bip44Chain::External).await?; + let internal_addresses = coin.derive_known_addresses(&hd_account, Bip44Chain::Internal).await?; + + let addresses_it = external_addresses + .into_iter() + .chain(internal_addresses) + .map(|hd_address| hd_address.address); + all_addresses.extend(addresses_it); + } + + Ok(all_addresses) + }, + } +} + +/// [`UtxoTxHistoryOps::tx_details_by_hash`] implementation. +pub async fn tx_details_by_hash( + coin: &Coin, + params: UtxoTxDetailsParams<'_, Storage>, +) -> MmResult, UtxoTxDetailsError> +where + Coin: UtxoTxHistoryOps + UtxoCommonOps + MarketCoinOps, + Storage: TxHistoryStorage, +{ + let ticker = coin.ticker(); + let decimals = coin.as_ref().decimals; + + let verbose_tx = coin + .as_ref() + .rpc_client + .get_verbose_transaction(params.hash) + .compat() + .await?; + let tx: UtxoTx = deserialize(verbose_tx.hex.as_slice())?; + + let mut tx_builder = TxDetailsBuilder::new( + ticker.to_string(), + &tx, + params.block_height_and_time, + params.my_addresses.clone(), + ); + + let mut input_amount = 0; + let mut output_amount = 0; + + for input in tx.inputs.iter() { + // input transaction is zero if the tx is the coinbase transaction + if input.previous_output.hash.is_zero() { + continue; + } + + let prev_tx_hash: H256Json = input.previous_output.hash.reversed().into(); + + let prev_tx = coin.tx_from_storage_or_rpc(&prev_tx_hash, params.storage).await?; + + let prev_output_index = input.previous_output.index as usize; + let prev_tx_value = prev_tx.outputs[prev_output_index].value; + let prev_script = prev_tx.outputs[prev_output_index].script_pubkey.clone().into(); + + input_amount += prev_tx_value; + let amount = big_decimal_from_sat_unsigned(prev_tx_value, decimals); + + let from: Vec
= coin + .addresses_from_script(&prev_script) + .map_to_mm(UtxoTxDetailsError::TxAddressDeserializationError)?; + for address in from { + tx_builder.transferred_from(address, &amount); + } + } + + for output in tx.outputs.iter() { + let output_script = output.script_pubkey.clone().into(); + let to = coin + .addresses_from_script(&output_script) + .map_to_mm(UtxoTxDetailsError::TxAddressDeserializationError)?; + if to.is_empty() { + continue; + } + + output_amount += output.value; + let amount = big_decimal_from_sat_unsigned(output.value, decimals); + for address in to { + tx_builder.transferred_to(address, &amount); + } + } + + let fee = if input_amount == 0 { + let fee = verbose_tx.vin.iter().fold(0., |cur, input| { + let fee = match input { + TransactionInputEnum::Lelantus(lelantus) => lelantus.n_fees, + _ => 0., + }; + cur + fee + }); + BigDecimal::try_from(fee)? + } else { + let fee = input_amount as i64 - output_amount as i64; + big_decimal_from_sat(fee, decimals) + }; + + let fee_details = UtxoFeeDetails { + coin: Some(ticker.to_string()), + amount: fee, + }; + + tx_builder.set_tx_fee(Some(TxFeeDetails::from(fee_details))); + Ok(vec![tx_builder.build()]) +} + +/// [`UtxoTxHistoryOps::tx_from_storage_or_rpc`] implementation. +pub async fn tx_from_storage_or_rpc( + coin: &Coin, + tx_hash: &H256Json, + storage: &Storage, +) -> MmResult +where + Coin: CoinWithTxHistoryV2 + UtxoCommonOps, + Storage: TxHistoryStorage, +{ + let tx_hash_str = format!("{:02x}", tx_hash); + let wallet_id = coin.history_wallet_id(); + let tx_bytes = match storage.tx_bytes_from_cache(&wallet_id, &tx_hash_str).await? { + Some(tx_bytes) => tx_bytes, + None => { + let tx_bytes = coin.as_ref().rpc_client.get_transaction_bytes(tx_hash).compat().await?; + storage.add_tx_to_cache(&wallet_id, &tx_hash_str, &tx_bytes).await?; + tx_bytes + }, + }; + let tx = deserialize(tx_bytes.0.as_slice())?; + Ok(tx) +} + +/// [`UtxoTxHistoryOps::my_addresses_balances`] implementation. +/// Requests balances of all activated addresses. +pub async fn my_addresses_balances(coin: &Coin) -> BalanceResult> +where + Coin: CoinBalanceReportOps, +{ + let coin_balance = coin.coin_balance_report().await?; + Ok(coin_balance.to_addresses_total_balances()) +} + +/// [`UtxoTxHistoryOps::request_tx_history`] implementation. +/// Requests transaction history according to `UtxoRpcClientEnum`. +pub async fn request_tx_history( + coin: &Coin, + metrics: MetricsArc, + for_addresses: &HashSet
, +) -> RequestTxHistoryResult +where + Coin: UtxoCommonOps + MarketCoinOps, +{ + let ticker = coin.ticker(); + match coin.as_ref().rpc_client { + UtxoRpcClientEnum::Native(ref native) => { + request_tx_history_with_native(ticker, native, metrics, for_addresses).await + }, + UtxoRpcClientEnum::Electrum(ref electrum) => { + request_tx_history_with_electrum(ticker, electrum, metrics, for_addresses).await + }, + } +} + +/// `request_tx_history_with_der_method` function's helper. +async fn request_tx_history_with_native( + ticker: &str, + native: &NativeClient, + metrics: MetricsArc, + for_addresses: &HashSet
, +) -> RequestTxHistoryResult { + let my_addresses: HashSet = for_addresses.iter().map(DisplayAddress::display_address).collect(); + + let mut from = 0; + let mut all_transactions = vec![]; + loop { + mm_counter!(metrics, "tx.history.request.count", 1, + "coin" => ticker, "client" => "native", "method" => "listtransactions"); + + let transactions = match native.list_transactions(100, from).compat().await { + Ok(value) => value, + Err(e) => { + return RequestTxHistoryResult::Retry { + error: ERRL!("Error {} on list transactions", e), + }; + }, + }; + + mm_counter!(metrics, "tx.history.response.count", 1, + "coin" => ticker, "client" => "native", "method" => "listtransactions"); + + if transactions.is_empty() { + break; + } + from += 100; + all_transactions.extend(transactions); + } + + mm_counter!(metrics, "tx.history.response.total_length", all_transactions.len() as u64, + "coin" => ticker, "client" => "native", "method" => "listtransactions"); + + let all_transactions = all_transactions + .into_iter() + .filter_map(|item| { + if my_addresses.contains(&item.address) { + Some((item.txid, item.blockindex)) + } else { + None + } + }) + .collect(); + + RequestTxHistoryResult::Ok(all_transactions) +} + +/// `request_tx_history_with_der_method` function's helper. +async fn request_tx_history_with_electrum( + ticker: &str, + electrum: &ElectrumClient, + metrics: MetricsArc, + for_addresses: &HashSet
, +) -> RequestTxHistoryResult { + fn addr_to_script_hash(addr: &Address) -> String { + let script = output_script(addr, ScriptType::P2PKH); + let script_hash = electrum_script_hash(&script); + hex::encode(script_hash) + } + + let script_hashes_count = for_addresses.len() as u64; + let script_hashes = for_addresses.iter().map(addr_to_script_hash); + + mm_counter!(metrics, "tx.history.request.count", script_hashes_count, + "coin" => ticker, "client" => "electrum", "method" => "blockchain.scripthash.get_history"); + + let hashes_history = match electrum.scripthash_get_history_batch(script_hashes).compat().await { + Ok(hashes_history) => hashes_history, + Err(e) => match &e.error { + JsonRpcErrorType::InvalidRequest(e) + | JsonRpcErrorType::Transport(e) + | JsonRpcErrorType::Parse(_, e) + | JsonRpcErrorType::Internal(e) => { + return RequestTxHistoryResult::Retry { + error: ERRL!("Error {} on scripthash_get_history", e), + }; + }, + JsonRpcErrorType::Response(_addr, err) => { + if HISTORY_TOO_LARGE_ERROR.eq(err) { + return RequestTxHistoryResult::HistoryTooLarge; + } else { + return RequestTxHistoryResult::Retry { + error: ERRL!("Error {:?} on scripthash_get_history", e), + }; + } + }, + }, + }; + + let ordered_history: Vec<_> = hashes_history + .into_iter() + .flatten() + .map(|item| { + let height = if item.height < 0 { 0 } else { item.height as u64 }; + (item.tx_hash, height) + }) + // We need to order transactions by their height and TX hash. + .sorted_by(|(tx_hash_left, height_left), (tx_hash_right, height_right)| { + let left = TxIdHeight::new(*height_left, tx_hash_left); + let right = TxIdHeight::new(*height_right, tx_hash_right); + compare_transactions(left, right) + }) + .collect(); + + mm_counter!(metrics, "tx.history.response.count", script_hashes_count, + "coin" => ticker, "client" => "electrum", "method" => "blockchain.scripthash.get_history"); + + mm_counter!(metrics, "tx.history.response.total_length", ordered_history.len() as u64, + "coin" => ticker, "client" => "electrum", "method" => "blockchain.scripthash.get_history"); + + RequestTxHistoryResult::Ok(ordered_history) +} diff --git a/mm2src/coins/utxo/utxo_common_tests.rs b/mm2src/coins/utxo/utxo_common_tests.rs index 94613eb5c8..e4ae575bb2 100644 --- a/mm2src/coins/utxo/utxo_common_tests.rs +++ b/mm2src/coins/utxo/utxo_common_tests.rs @@ -1,9 +1,193 @@ use super::*; +use crate::hd_wallet::HDAccountsMap; +use crate::my_tx_history_v2::{my_tx_history_v2_impl, CoinWithTxHistoryV2, MyTxHistoryDetails, MyTxHistoryRequestV2, + MyTxHistoryResponseV2, MyTxHistoryTarget}; +use crate::tx_history_storage::TxHistoryStorageBuilder; use crate::utxo::rpc_clients::{ElectrumClient, UtxoRpcClientOps}; +use crate::utxo::tx_cache::dummy_tx_cache::DummyVerboseCache; +use crate::utxo::tx_cache::UtxoVerboseCacheOps; +use crate::utxo::utxo_tx_history_v2::{utxo_history_loop, UtxoTxHistoryOps}; +use crate::{compare_transaction_details, UtxoStandardCoin}; +use common::executor::{spawn, Timer}; use common::jsonrpc_client::JsonRpcErrorType; +use common::PagingOptionsEnum; +use crypto::privkey::key_pair_from_seed; +use itertools::Itertools; +use mm2_test_helpers::for_tests::mm_ctx_with_custom_db; use std::convert::TryFrom; +use std::num::NonZeroUsize; -pub async fn test_electrum_display_balances(rpc_client: &ElectrumClient) { +pub(super) const TEST_COIN_NAME: &'static str = "RICK"; +// Made-up hrp for rick to test p2wpkh script +pub(super) const TEST_COIN_HRP: &'static str = "rck"; +pub(super) const TEST_COIN_DECIMALS: u8 = 8; + +const MORTY_HD_TX_HISTORY_STR: &str = include_str!("../for_tests/MORTY_HD_tx_history_fixtures.json"); + +lazy_static! { + static ref MORTY_HD_TX_HISTORY: Vec = parse_tx_history(MORTY_HD_TX_HISTORY_STR); + static ref MORTY_HD_TX_HISTORY_MAP: HashMap = + parse_tx_history_map(MORTY_HD_TX_HISTORY_STR); +} + +fn parse_tx_history(history_str: &'static str) -> Vec { json::from_str(history_str).unwrap() } + +fn parse_tx_history_map(history_str: &'static str) -> HashMap { + parse_tx_history(history_str) + .into_iter() + .map(|tx| (format!("{:02x}", tx.internal_id), tx)) + .collect() +} + +pub(super) fn utxo_coin_fields_for_test( + rpc_client: UtxoRpcClientEnum, + force_seed: Option<&str>, + is_segwit_coin: bool, +) -> UtxoCoinFields { + let checksum_type = ChecksumType::DSHA256; + let default_seed = "spice describe gravity federal blast come thank unfair canal monkey style afraid"; + let seed = match force_seed { + Some(s) => s.into(), + None => match std::env::var("BOB_PASSPHRASE") { + Ok(p) => { + if p.is_empty() { + default_seed.into() + } else { + p + } + }, + Err(_) => default_seed.into(), + }, + }; + let key_pair = key_pair_from_seed(&seed).unwrap(); + let my_address = Address { + prefix: 60, + hash: key_pair.public().address_hash().into(), + t_addr_prefix: 0, + checksum_type, + hrp: if is_segwit_coin { + Some(TEST_COIN_HRP.to_string()) + } else { + None + }, + addr_format: if is_segwit_coin { + UtxoAddressFormat::Segwit + } else { + UtxoAddressFormat::Standard + }, + }; + let my_script_pubkey = Builder::build_p2pkh(&my_address.hash).to_bytes(); + + let priv_key_policy = PrivKeyPolicy::KeyPair(key_pair); + let derivation_method = DerivationMethod::Iguana(my_address); + + let bech32_hrp = if is_segwit_coin { + Some(TEST_COIN_HRP.to_string()) + } else { + None + }; + + UtxoCoinFields { + conf: UtxoCoinConf { + is_pos: false, + requires_notarization: false.into(), + overwintered: true, + segwit: true, + tx_version: 4, + default_address_format: UtxoAddressFormat::Standard, + asset_chain: true, + p2sh_addr_prefix: 85, + p2sh_t_addr_prefix: 0, + pub_addr_prefix: 60, + pub_t_addr_prefix: 0, + sign_message_prefix: Some(String::from("Komodo Signed Message:\n")), + bech32_hrp, + ticker: TEST_COIN_NAME.into(), + wif_prefix: 0, + tx_fee_volatility_percent: DEFAULT_DYNAMIC_FEE_VOLATILITY_PERCENT, + version_group_id: 0x892f2085, + consensus_branch_id: 0x76b809bb, + zcash: true, + checksum_type, + fork_id: 0, + signature_version: SignatureVersion::Base, + required_confirmations: 1.into(), + force_min_relay_fee: false, + mtp_block_count: NonZeroU64::new(11).unwrap(), + estimate_fee_mode: None, + mature_confirmations: MATURE_CONFIRMATIONS_DEFAULT, + estimate_fee_blocks: 1, + trezor_coin: None, + enable_spv_proof: false, + block_headers_verification_params: None, + }, + decimals: TEST_COIN_DECIMALS, + dust_amount: UTXO_DUST_AMOUNT, + tx_fee: TxFee::FixedPerKb(1000), + rpc_client, + priv_key_policy, + derivation_method, + history_sync_state: Mutex::new(HistorySyncState::NotEnabled), + tx_cache: DummyVerboseCache::default().into_shared(), + recently_spent_outpoints: AsyncMutex::new(RecentlySpentOutPoints::new(my_script_pubkey)), + tx_hash_algo: TxHashAlgo::DSHA256, + check_utxo_maturity: false, + block_headers_status_notifier: None, + block_headers_status_watcher: None, + } +} + +pub(super) fn utxo_coin_from_fields(coin: UtxoCoinFields) -> UtxoStandardCoin { + let arc: UtxoArc = coin.into(); + arc.into() +} + +pub(super) async fn wait_for_tx_history_finished( + ctx: &MmArc, + coin: &Coin, + target: MyTxHistoryTarget, + expected_txs: usize, + timeout_s: u64, +) -> MyTxHistoryResponseV2 +where + Coin: CoinWithTxHistoryV2 + MmCoin, +{ + let started_at = now_ms() / 1000; + let wait_until = started_at + timeout_s; + + let req = MyTxHistoryRequestV2 { + coin: coin.ticker().to_owned(), + limit: u32::MAX as usize, + paging_options: PagingOptionsEnum::PageNumber(NonZeroUsize::new(1).unwrap()), + target, + }; + + while now_ms() / 1000 < wait_until { + Timer::sleep(3.).await; + + let response = my_tx_history_v2_impl(ctx.clone(), coin, req.clone()).await.unwrap(); + if response.transactions.len() >= expected_txs { + return response; + } + } + + panic!("Waited too long until {} for TX history finishes", wait_until) +} + +pub(super) fn get_morty_hd_transactions_ordered(tx_hashes: &[&str]) -> Vec { + tx_hashes + .iter() + .map(|tx_hash| { + MORTY_HD_TX_HISTORY_MAP + .get(*tx_hash) + .expect(&format!("No such {tx_hash:?} TX in the file")) + .clone() + }) + .sorted_by(compare_transaction_details) + .collect() +} + +pub(super) async fn test_electrum_display_balances(rpc_client: &ElectrumClient) { let addresses = vec![ "RG278CfeNPFtNztFZQir8cgdWexVhViYVy".into(), "RYPz6Lr4muj4gcFzpMdv3ks1NCGn3mkDPN".into(), @@ -48,3 +232,79 @@ pub async fn test_electrum_display_balances(rpc_client: &ElectrumClient) { ekind => panic!("Unexpected `JsonRpcErrorType`: {:?}", ekind), } } + +/// TODO move this test to `mm2_tests.rs` +/// when [Trezor Daemon Emulator](https://github.com/trezor/trezord-go#emulator-support) is integrated. +pub(super) async fn test_hd_utxo_tx_history_impl(rpc_client: ElectrumClient) { + let ctx = mm_ctx_with_custom_db(); + + let hd_account_for_test = UtxoHDAccount { + account_id: 0, + extended_pubkey: Secp256k1ExtendedPublicKey::from_str("xpub6DEHSksajpRPM59RPw7Eg6PKdU7E2ehxJWtYdrfQ6JFmMGBsrR6jA78ANCLgzKYm4s5UqQ4ydLEYPbh3TRVvn5oAZVtWfi4qJLMntpZ8uGJ").unwrap(), + account_derivation_path: Bip44PathToAccount::from_str("m/44'/141'/0'").unwrap(), + external_addresses_number: 11, + internal_addresses_number: 3, + derived_addresses: HDAddressesCache::default(), + }; + let mut hd_accounts = HDAccountsMap::new(); + hd_accounts.insert(0, hd_account_for_test); + + let mut fields = utxo_coin_fields_for_test(rpc_client.into(), None, false); + fields.conf.ticker = "MORTY".to_string(); + fields.derivation_method = DerivationMethod::HDWallet(UtxoHDWallet { + hd_wallet_rmd160: "6d9d2b554d768232320587df75c4338ecc8bf37d".into(), + hd_wallet_storage: HDWalletCoinStorage::default(), + address_format: UtxoAddressFormat::Standard, + derivation_path: Bip44PathToCoin::from_str("m/44'/141'").unwrap(), + accounts: HDAccountsMutex::new(hd_accounts), + gap_limit: 20, + }); + + let coin = utxo_coin_from_fields(fields); + + let current_balances = coin.my_addresses_balances().await.unwrap(); + + let storage = TxHistoryStorageBuilder::new(&ctx).build().unwrap(); + spawn(utxo_history_loop( + coin.clone(), + storage, + ctx.metrics.clone(), + current_balances, + )); + + let target = MyTxHistoryTarget::AccountId { account_id: 0 }; + let tx_history = wait_for_tx_history_finished(&ctx, &coin, target, 4, 30).await; + + let actual: Vec<_> = tx_history.transactions.into_iter().map(|tx| tx.details).collect(); + let expected = get_morty_hd_transactions_ordered(&[ + "70c62f42d65f9d71a8fb7f4560057b80dc2ecd9e4990621323faf1de9a53ca97", + "bd031dc681cdc63491fd71902c5960985127b04eb02211a1049bff0d0c8ebce3", + "bf02bea67c568108c91f58d88f2f7adda84a3287949ad89cc8c05de95042fb75", + "7dc038aae5eef3108f8071450b590cd0d376a08c1aea190ba89491cc3b27ea8d", + ]); + assert_eq!(actual, expected); + + // Activate new `RQstQeTUEZLh6c3YWJDkeVTTQoZUsfvNCr` address. + match coin.as_ref().derivation_method { + DerivationMethod::HDWallet(ref hd_wallet) => { + let mut accounts = hd_wallet.accounts.lock().await; + accounts.get_mut(&0).unwrap().internal_addresses_number += 1 + }, + _ => unimplemented!(), + } + + // Wait for the TX history loop to fetch Transactions of the activated address. + let target = MyTxHistoryTarget::AccountId { account_id: 0 }; + let tx_history = wait_for_tx_history_finished(&ctx, &coin, target, 5, 60).await; + + let actual: Vec<_> = tx_history.transactions.into_iter().map(|tx| tx.details).collect(); + let expected = get_morty_hd_transactions_ordered(&[ + // New transaction: + "6ca27dd058b939c98a33625b9f68eaeebca5a3058aec062647ca6fd7634bb339", + "70c62f42d65f9d71a8fb7f4560057b80dc2ecd9e4990621323faf1de9a53ca97", + "bd031dc681cdc63491fd71902c5960985127b04eb02211a1049bff0d0c8ebce3", + "bf02bea67c568108c91f58d88f2f7adda84a3287949ad89cc8c05de95042fb75", + "7dc038aae5eef3108f8071450b590cd0d376a08c1aea190ba89491cc3b27ea8d", + ]); + assert_eq!(actual, expected); +} diff --git a/mm2src/coins/utxo/utxo_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index 752529eb76..685f823fe1 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -2,8 +2,9 @@ use super::*; use crate::coin_balance::{self, EnableCoinBalanceError, EnabledCoinBalanceParams, HDAccountBalance, HDAddressBalance, HDWalletBalance, HDWalletBalanceOps}; use crate::hd_pubkey::{ExtractExtendedPubkey, HDExtractPubkeyError, HDXPubExtractor}; -use crate::hd_wallet::{AccountUpdatingError, AddressDerivingError, HDAccountMut, NewAccountCreatingError}; +use crate::hd_wallet::{AccountUpdatingError, AddressDerivingResult, HDAccountMut, NewAccountCreatingError}; use crate::hd_wallet_storage::HDWalletCoinWithStorageOps; +use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHistoryTarget, TxHistoryStorage}; use crate::rpc_command::account_balance::{self, AccountBalanceParams, AccountBalanceRpcOps, HDAccountBalanceResponse}; use crate::rpc_command::get_new_address::{self, GetNewAddressParams, GetNewAddressResponse, GetNewAddressRpcError, GetNewAddressRpcOps}; @@ -14,7 +15,10 @@ use crate::rpc_command::init_create_account::{self, CreateAccountRpcError, Creat use crate::rpc_command::init_scan_for_new_addresses::{self, InitScanAddressesRpcOps, ScanAddressesParams, ScanAddressesResponse}; use crate::rpc_command::init_withdraw::{InitWithdrawCoin, WithdrawTaskHandle}; +use crate::tx_history_storage::{GetTxHistoryFilters, WalletId}; use crate::utxo::utxo_builder::{UtxoArcBuilder, UtxoCoinBuilder}; +use crate::utxo::utxo_tx_history_v2::{UtxoMyAddressesHistoryError, UtxoTxDetailsError, UtxoTxDetailsParams, + UtxoTxHistoryOps}; use crate::{CanRefundHtlc, CoinBalance, CoinWithDerivationMethod, GetWithdrawSenderAddress, NegotiateSwapContractAddrErr, PrivKeyBuildPolicy, SearchForSwapTxSpendInput, SignatureResult, SwapOps, TradePreimageValue, TransactionFut, TxMarshalingErr, ValidateAddressResult, ValidatePaymentInput, @@ -158,7 +162,7 @@ impl UtxoCommonOps for UtxoStandardCoin { utxo_common::my_public_key(self.as_ref()) } - fn address_from_str(&self, address: &str) -> Result { + fn address_from_str(&self, address: &str) -> MmResult { utxo_common::checked_address_from_str(self, address) } @@ -712,7 +716,7 @@ impl HDWalletCoinOps for UtxoStandardCoin { &self, hd_account: &Self::HDAccount, address_ids: Ids, - ) -> MmResult>, AddressDerivingError> + ) -> AddressDerivingResult>> where Ids: Iterator + Send, { @@ -851,3 +855,64 @@ impl InitCreateAccountRpcOps for UtxoStandardCoin { init_create_account::common_impl::revert_creating_account(self, account_id).await } } + +#[async_trait] +impl CoinWithTxHistoryV2 for UtxoStandardCoin { + fn history_wallet_id(&self) -> WalletId { utxo_common::utxo_tx_history_v2_common::history_wallet_id(self.as_ref()) } + + async fn get_tx_history_filters( + &self, + target: MyTxHistoryTarget, + ) -> MmResult { + utxo_common::utxo_tx_history_v2_common::get_tx_history_filters(self, target).await + } +} + +#[async_trait] +impl UtxoTxHistoryOps for UtxoStandardCoin { + async fn my_addresses(&self) -> MmResult, UtxoMyAddressesHistoryError> { + utxo_common::utxo_tx_history_v2_common::my_addresses(self).await + } + + async fn tx_details_by_hash( + &self, + params: UtxoTxDetailsParams<'_, Storage>, + ) -> MmResult, UtxoTxDetailsError> + where + Storage: TxHistoryStorage, + { + utxo_common::utxo_tx_history_v2_common::tx_details_by_hash(self, params).await + } + + async fn tx_from_storage_or_rpc( + &self, + tx_hash: &H256Json, + storage: &Storage, + ) -> MmResult { + utxo_common::utxo_tx_history_v2_common::tx_from_storage_or_rpc(self, tx_hash, storage).await + } + + async fn request_tx_history( + &self, + metrics: MetricsArc, + for_addresses: &HashSet
, + ) -> RequestTxHistoryResult { + utxo_common::utxo_tx_history_v2_common::request_tx_history(self, metrics, for_addresses).await + } + + async fn get_block_timestamp(&self, height: u64) -> MmResult { + self.as_ref().rpc_client.get_block_timestamp(height).await + } + + async fn my_addresses_balances(&self) -> BalanceResult> { + utxo_common::utxo_tx_history_v2_common::my_addresses_balances(self).await + } + + fn address_from_str(&self, address: &str) -> MmResult { + utxo_common::checked_address_from_str(self, address) + } + + fn set_history_sync_state(&self, new_state: HistorySyncState) { + *self.as_ref().history_sync_state.lock().unwrap() = new_state; + } +} diff --git a/mm2src/coins/utxo/utxo_tests.rs b/mm2src/coins/utxo/utxo_tests.rs index efb1a2c3ee..e64c7f6753 100644 --- a/mm2src/coins/utxo/utxo_tests.rs +++ b/mm2src/coins/utxo/utxo_tests.rs @@ -2,6 +2,8 @@ use super::*; use crate::coin_balance::HDAddressBalance; use crate::hd_wallet::HDAccountsMap; use crate::hd_wallet_storage::{HDWalletMockStorage, HDWalletStorageInternalOps}; +use crate::my_tx_history_v2::for_tests::init_storage_for; +use crate::my_tx_history_v2::CoinWithTxHistoryV2; use crate::rpc_command::account_balance::{AccountBalanceParams, AccountBalanceRpcOps, HDAccountBalanceResponse}; use crate::rpc_command::get_new_address::{GetNewAddressParams, GetNewAddressRpcError, GetNewAddressRpcOps}; use crate::rpc_command::init_scan_for_new_addresses::{InitScanAddressesRpcOps, ScanAddressesParams, @@ -11,17 +13,17 @@ use crate::utxo::rpc_clients::{BlockHashOrHeight, ElectrumBalance, ElectrumClien GetAddressInfoRes, ListSinceBlockRes, NativeClient, NativeClientImpl, NativeUnspent, NetworkInfo, UtxoRpcClientOps, ValidateAddressRes, VerboseBlock}; use crate::utxo::spv::SimplePaymentVerification; -use crate::utxo::tx_cache::dummy_tx_cache::DummyVerboseCache; -use crate::utxo::tx_cache::UtxoVerboseCacheOps; use crate::utxo::utxo_block_header_storage::BlockHeaderStorage; use crate::utxo::utxo_builder::{UtxoArcBuilder, UtxoCoinBuilderCommonOps}; use crate::utxo::utxo_common::UtxoTxBuilder; -use crate::utxo::utxo_common_tests; +use crate::utxo::utxo_common_tests::{self, utxo_coin_fields_for_test, utxo_coin_from_fields, TEST_COIN_DECIMALS, + TEST_COIN_NAME}; use crate::utxo::utxo_sql_block_header_storage::SqliteBlockHeadersStorage; use crate::utxo::utxo_standard::{utxo_standard_coin_with_priv_key, UtxoStandardCoin}; +use crate::utxo::utxo_tx_history_v2::{UtxoTxDetailsParams, UtxoTxHistoryOps}; #[cfg(not(target_arch = "wasm32"))] use crate::WithdrawFee; -use crate::{CoinBalance, PrivKeyBuildPolicy, SearchForSwapTxSpendInput, StakingInfosDetails, SwapOps, - TradePreimageValue, TxFeeDetails, TxMarshalingErr}; +use crate::{BlockHeightAndTime, CoinBalance, PrivKeyBuildPolicy, SearchForSwapTxSpendInput, StakingInfosDetails, + SwapOps, TradePreimageValue, TxFeeDetails, TxMarshalingErr}; use chain::{BlockHeader, OutPoint}; use common::executor::Timer; use common::{block_on, now_ms, OrdRange, PagingOptionsEnum, DEX_FEE_ADDR_RAW_PUBKEY}; @@ -31,7 +33,7 @@ use futures::future::join_all; use futures::TryFutureExt; use mm2_core::mm_ctx::MmCtxBuilder; use mm2_number::bigdecimal::{BigDecimal, Signed}; -use mm2_test_helpers::for_tests::RICK_ELECTRUM_ADDRS; +use mm2_test_helpers::for_tests::{MORTY_ELECTRUM_ADDRS, RICK_ELECTRUM_ADDRS}; use mocktopus::mocking::*; use rpc::v1::types::H256 as H256Json; use serialization::{deserialize, CoinVariant}; @@ -41,11 +43,6 @@ use std::iter; use std::mem::discriminant; use std::num::NonZeroUsize; -const TEST_COIN_NAME: &'static str = "RICK"; -// Made-up hrp for rick to test p2wpkh script -const TEST_COIN_HRP: &'static str = "rck"; -const TEST_COIN_DECIMALS: u8 = 8; - pub fn electrum_client_for_test(servers: &[&str]) -> ElectrumClient { let ctx = MmCtxBuilder::default().into_mm_arc(); let servers: Vec<_> = servers.iter().map(|server| json!({ "url": server })).collect(); @@ -77,115 +74,57 @@ pub fn electrum_client_for_test(servers: &[&str]) -> ElectrumClient { #[cfg(not(target_arch = "wasm32"))] fn native_client_for_test() -> NativeClient { NativeClient(Arc::new(NativeClientImpl::default())) } -fn utxo_coin_fields_for_test( +fn utxo_coin_for_test( rpc_client: UtxoRpcClientEnum, force_seed: Option<&str>, is_segwit_coin: bool, -) -> UtxoCoinFields { - let checksum_type = ChecksumType::DSHA256; - let default_seed = "spice describe gravity federal blast come thank unfair canal monkey style afraid"; - let seed = match force_seed { - Some(s) => s.into(), - None => match std::env::var("BOB_PASSPHRASE") { - Ok(p) => { - if p.is_empty() { - default_seed.into() - } else { - p - } - }, - Err(_) => default_seed.into(), - }, - }; - let key_pair = key_pair_from_seed(&seed).unwrap(); - let my_address = Address { - prefix: 60, - hash: key_pair.public().address_hash().into(), - t_addr_prefix: 0, - checksum_type, - hrp: if is_segwit_coin { - Some(TEST_COIN_HRP.to_string()) - } else { - None - }, - addr_format: if is_segwit_coin { - UtxoAddressFormat::Segwit - } else { - UtxoAddressFormat::Standard - }, - }; - let my_script_pubkey = Builder::build_p2pkh(&my_address.hash).to_bytes(); - - let priv_key_policy = PrivKeyPolicy::KeyPair(key_pair); - let derivation_method = DerivationMethod::Iguana(my_address); +) -> UtxoStandardCoin { + utxo_coin_from_fields(utxo_coin_fields_for_test(rpc_client, force_seed, is_segwit_coin)) +} - let bech32_hrp = if is_segwit_coin { - Some(TEST_COIN_HRP.to_string()) - } else { - None - }; +/// Returns `TransactionDetails` of the given `tx_hash` via [`UtxoStandardOps::tx_details_by_hash`]. +#[track_caller] +fn get_tx_details_by_hash(coin: &Coin, tx_hash: &str) -> TransactionDetails { + let hash = hex::decode(tx_hash).unwrap(); + let mut input_transactions = HistoryUtxoTxMap::new(); - UtxoCoinFields { - conf: UtxoCoinConf { - is_pos: false, - requires_notarization: false.into(), - overwintered: true, - segwit: true, - tx_version: 4, - default_address_format: UtxoAddressFormat::Standard, - asset_chain: true, - p2sh_addr_prefix: 85, - p2sh_t_addr_prefix: 0, - pub_addr_prefix: 60, - pub_t_addr_prefix: 0, - sign_message_prefix: Some(String::from("Komodo Signed Message:\n")), - bech32_hrp, - ticker: TEST_COIN_NAME.into(), - wif_prefix: 0, - tx_fee_volatility_percent: DEFAULT_DYNAMIC_FEE_VOLATILITY_PERCENT, - version_group_id: 0x892f2085, - consensus_branch_id: 0x76b809bb, - zcash: true, - checksum_type, - fork_id: 0, - signature_version: SignatureVersion::Base, - required_confirmations: 1.into(), - force_min_relay_fee: false, - mtp_block_count: NonZeroU64::new(11).unwrap(), - estimate_fee_mode: None, - mature_confirmations: MATURE_CONFIRMATIONS_DEFAULT, - estimate_fee_blocks: 1, - trezor_coin: None, - enable_spv_proof: false, - block_headers_verification_params: None, - }, - decimals: TEST_COIN_DECIMALS, - dust_amount: UTXO_DUST_AMOUNT, - tx_fee: TxFee::FixedPerKb(1000), - rpc_client, - priv_key_policy, - derivation_method, - history_sync_state: Mutex::new(HistorySyncState::NotEnabled), - tx_cache: DummyVerboseCache::default().into_shared(), - recently_spent_outpoints: AsyncMutex::new(RecentlySpentOutPoints::new(my_script_pubkey)), - tx_hash_algo: TxHashAlgo::DSHA256, - check_utxo_maturity: false, - block_headers_status_notifier: None, - block_headers_status_watcher: None, - } + block_on(UtxoStandardOps::tx_details_by_hash( + coin, + &hash, + &mut input_transactions, + )) + .unwrap() } -fn utxo_coin_from_fields(coin: UtxoCoinFields) -> UtxoStandardCoin { - let arc: UtxoArc = coin.into(); - arc.into() +/// Returns `TransactionDetails` of the given `tx_hash` via [`UtxoTxHistoryOps::tx_details_by_hash`]. +fn get_tx_details_by_hash_v2(coin: &Coin, tx_hash: &str, height: u64, timestamp: u64) -> Vec +where + Coin: CoinWithTxHistoryV2 + UtxoTxHistoryOps, +{ + let my_addresses = block_on(coin.my_addresses()).unwrap(); + let (_ctx, storage) = init_storage_for(coin); + let params = UtxoTxDetailsParams { + hash: &hex::decode(tx_hash).unwrap().as_slice().into(), + block_height_and_time: Some(BlockHeightAndTime { height, timestamp }), + storage: &storage, + my_addresses: &my_addresses, + }; + + block_on(UtxoTxHistoryOps::tx_details_by_hash(coin, params)).unwrap() } -fn utxo_coin_for_test( - rpc_client: UtxoRpcClientEnum, - force_seed: Option<&str>, - is_segwit_coin: bool, -) -> UtxoStandardCoin { - utxo_coin_from_fields(utxo_coin_fields_for_test(rpc_client, force_seed, is_segwit_coin)) +/// Returns `TransactionDetails` of the given `tx_hash` and checks that +/// [`UtxoTxHistoryOps::tx_details_by_hash`] and [`UtxoStandardOps::tx_details_by_hash`] return the same TX details. +#[track_caller] +fn get_tx_details_eq_for_both_versions(coin: &Coin, tx_hash: &str) -> TransactionDetails +where + Coin: CoinWithTxHistoryV2 + UtxoTxHistoryOps + UtxoStandardOps, +{ + let tx_details_v1 = get_tx_details_by_hash(coin, tx_hash); + let tx_details_v2 = get_tx_details_by_hash_v2(coin, tx_hash, tx_details_v1.block_height, tx_details_v1.timestamp); + + assert_eq!(vec![tx_details_v1.clone()], tx_details_v2); + tx_details_v1 } #[test] @@ -996,6 +935,10 @@ fn list_since_block_btc_serde() { #[test] // https://github.com/KomodoPlatform/atomicDEX-API/issues/587 fn get_tx_details_coinbase_transaction() { + /// Hash of coinbase transaction + /// https://morty.explorer.dexstats.info/tx/b59b093ed97c1798f2a88ee3375a0c11d0822b6e4468478777f899891abd34a5 + const TX_HASH: &str = "b59b093ed97c1798f2a88ee3375a0c11d0822b6e4468478777f899891abd34a5"; + let client = electrum_client_for_test(&[ "electrum1.cipig.net:10018", "electrum2.cipig.net:10018", @@ -1007,16 +950,8 @@ fn get_tx_details_coinbase_transaction() { false, ); - let fut = async move { - // hash of coinbase transaction https://morty.explorer.dexstats.info/tx/b59b093ed97c1798f2a88ee3375a0c11d0822b6e4468478777f899891abd34a5 - let hash = hex::decode("b59b093ed97c1798f2a88ee3375a0c11d0822b6e4468478777f899891abd34a5").unwrap(); - - let mut input_transactions = HistoryUtxoTxMap::new(); - let tx_details = coin.tx_details_by_hash(&hash, &mut input_transactions).await.unwrap(); - assert!(tx_details.from.is_empty()); - }; - - block_on(fut); + let tx_details = get_tx_details_eq_for_both_versions(&coin, TX_HASH); + assert!(tx_details.from.is_empty()); } #[test] @@ -1358,6 +1293,8 @@ fn test_get_median_time_past_from_native_does_not_have_median_in_get_block() { #[test] fn test_cashaddresses_in_tx_details_by_hash() { + const TX_HASH: &str = "0f2f6e0c8f440c641895023782783426c3aca1acc78d7c0db7751995e8aa5751"; + let conf = json!({ "coin": "BCH", "pubtype": 0, @@ -1384,23 +1321,17 @@ fn test_cashaddresses_in_tx_details_by_hash() { )) .unwrap(); - let hash = hex::decode("0f2f6e0c8f440c641895023782783426c3aca1acc78d7c0db7751995e8aa5751").unwrap(); - let fut = async { - let mut input_transactions = HistoryUtxoTxMap::new(); - let tx_details = coin.tx_details_by_hash(&hash, &mut input_transactions).await.unwrap(); - log!("{:?}", tx_details); - - assert!(tx_details - .from - .iter() - .any(|addr| addr == "bchtest:qze8g4gx3z428jjcxzpycpxl7ke7d947gca2a7n2la")); - assert!(tx_details - .to - .iter() - .any(|addr| addr == "bchtest:qr39na5d25wdeecgw3euh9fkd4ygvd4pnsury96597")); - }; + let tx_details = get_tx_details_eq_for_both_versions(&coin, TX_HASH); + log!("{:?}", tx_details); - block_on(fut); + assert!(tx_details + .from + .iter() + .any(|addr| addr == "bchtest:qze8g4gx3z428jjcxzpycpxl7ke7d947gca2a7n2la")); + assert!(tx_details + .to + .iter() + .any(|addr| addr == "bchtest:qr39na5d25wdeecgw3euh9fkd4ygvd4pnsury96597")); } #[test] @@ -1432,11 +1363,16 @@ fn test_address_from_str_with_cashaddress_activated() { .unwrap(); // other error on parse - let error = coin - .address_from_str("bitcoincash:000000000000000000000000000000000000000000") + let error = UtxoCommonOps::address_from_str(&coin, "bitcoincash:000000000000000000000000000000000000000000") .err() .unwrap(); - assert!(error.contains("Invalid address: bitcoincash:000000000000000000000000000000000000000000")); + match error.into_inner() { + AddrFromStrError::CannotDetermineFormat(_) => (), + other => panic!( + "Expected 'AddrFromStrError::CannotDetermineFormat' error, found: {}", + other + ), + } } #[test] @@ -1466,18 +1402,33 @@ fn test_address_from_str_with_legacy_address_activated() { )) .unwrap(); - let error = coin - .address_from_str("bitcoincash:qzxqqt9lh4feptf0mplnk58gnajfepzwcq9f2rxk55") + let error = UtxoCommonOps::address_from_str(&coin, "bitcoincash:qzxqqt9lh4feptf0mplnk58gnajfepzwcq9f2rxk55") .err() .unwrap(); - assert!(error.contains("Legacy address format activated for BCH, but CashAddress format used instead")); + match error.into_inner() { + AddrFromStrError::Unsupported(UnsupportedAddr::FormatMismatch { + ticker, + activated_format, + used_format, + }) => { + assert_eq!(ticker, "BCH"); + assert_eq!(activated_format, "Legacy"); + assert_eq!(used_format, "CashAddress"); + }, + other => panic!("Expected 'UnsupportedAddr::FormatMismatch' error, found: {}", other), + } // other error on parse - let error = coin - .address_from_str("0000000000000000000000000000000000") + let error = UtxoCommonOps::address_from_str(&coin, "0000000000000000000000000000000000") .err() .unwrap(); - assert!(error.contains("Invalid address: 0000000000000000000000000000000000")); + match error.into_inner() { + AddrFromStrError::CannotDetermineFormat(_) => (), + other => panic!( + "Expected 'AddrFromStrError::CannotDetermineFormat' error, found: {}", + other + ), + } } #[test] @@ -2660,10 +2611,11 @@ fn firo_lelantus_tx_details() { "electrumx03.firo.org:50001", ]); let coin = utxo_coin_for_test(electrum.into(), None, false); - let mut map = HashMap::new(); - let tx_hash = hex::decode("ad812911f5cba3eab7c193b6cd7020ea02fb5c25634ae64959c3171a6bd5a74d").unwrap(); - let tx_details = block_on(coin.tx_details_by_hash(&tx_hash, &mut map)).unwrap(); + let tx_details = get_tx_details_eq_for_both_versions( + &coin, + "ad812911f5cba3eab7c193b6cd7020ea02fb5c25634ae64959c3171a6bd5a74d", + ); let expected_fee = TxFeeDetails::Utxo(UtxoFeeDetails { coin: Some(TEST_COIN_NAME.into()), @@ -2671,8 +2623,10 @@ fn firo_lelantus_tx_details() { }); assert_eq!(Some(expected_fee), tx_details.fee_details); - let tx_hash = hex::decode("06ed4b75010edcf404a315be70903473f44050c978bc37fbcee90e0b49114ba8").unwrap(); - let tx_details = block_on(coin.tx_details_by_hash(&tx_hash, &mut map)).unwrap(); + let tx_details = get_tx_details_eq_for_both_versions( + &coin, + "06ed4b75010edcf404a315be70903473f44050c978bc37fbcee90e0b49114ba8", + ); let expected_fee = TxFeeDetails::Utxo(UtxoFeeDetails { coin: Some(TEST_COIN_NAME.into()), @@ -2894,9 +2848,10 @@ fn test_tx_details_kmd_rewards() { fields.derivation_method = DerivationMethod::Iguana(Address::from("RMGJ9tRST45RnwEKHPGgBLuY3moSYP7Mhk")); let coin = utxo_coin_from_fields(fields); - let mut input_transactions = HistoryUtxoTxMap::new(); - let hash = hex::decode("535ffa3387d3fca14f4a4d373daf7edf00e463982755afce89bc8c48d8168024").unwrap(); - let tx_details = block_on(coin.tx_details_by_hash(&hash, &mut input_transactions)).expect("!tx_details_by_hash"); + let tx_details = get_tx_details_eq_for_both_versions( + &coin, + "535ffa3387d3fca14f4a4d373daf7edf00e463982755afce89bc8c48d8168024", + ); let expected_fee = TxFeeDetails::Utxo(UtxoFeeDetails { coin: Some("KMD".into()), @@ -2918,6 +2873,8 @@ fn test_tx_details_kmd_rewards() { #[ignore] #[cfg(not(target_arch = "wasm32"))] fn test_tx_details_kmd_rewards_claimed_by_other() { + const TX_HASH: &str = "f09e8894959e74c1e727ffa5a753a30bf2dc6d5d677cc1f24b7ee5bb64e32c7d"; + let electrum = electrum_client_for_test(&[ "electrum1.cipig.net:10001", "electrum2.cipig.net:10001", @@ -2928,9 +2885,7 @@ fn test_tx_details_kmd_rewards_claimed_by_other() { fields.derivation_method = DerivationMethod::Iguana(Address::from("RMGJ9tRST45RnwEKHPGgBLuY3moSYP7Mhk")); let coin = utxo_coin_from_fields(fields); - let mut input_transactions = HistoryUtxoTxMap::new(); - let hash = hex::decode("f09e8894959e74c1e727ffa5a753a30bf2dc6d5d677cc1f24b7ee5bb64e32c7d").unwrap(); - let tx_details = block_on(coin.tx_details_by_hash(&hash, &mut input_transactions)).expect("!tx_details_by_hash"); + let tx_details = get_tx_details_eq_for_both_versions(&coin, TX_HASH); let expected_fee = TxFeeDetails::Utxo(UtxoFeeDetails { coin: Some("KMD".into()), @@ -2947,6 +2902,8 @@ fn test_tx_details_kmd_rewards_claimed_by_other() { #[test] fn test_tx_details_bch_no_rewards() { + const TX_HASH: &str = "eb13d926f15cbb896e0bcc7a1a77a4ec63504e57a1524c13a7a9b80f43ecb05c"; + let electrum = electrum_client_for_test(&[ "electroncash.de:50003", "tbch.loping.net:60001", @@ -2956,10 +2913,7 @@ fn test_tx_details_bch_no_rewards() { ]); let coin = utxo_coin_for_test(electrum.into(), None, false); - let mut input_transactions = HistoryUtxoTxMap::new(); - let hash = hex::decode("eb13d926f15cbb896e0bcc7a1a77a4ec63504e57a1524c13a7a9b80f43ecb05c").unwrap(); - let tx_details = block_on(coin.tx_details_by_hash(&hash, &mut input_transactions)).expect("!tx_details_by_hash"); - + let tx_details = get_tx_details_eq_for_both_versions(&coin, TX_HASH); let expected_fee = TxFeeDetails::Utxo(UtxoFeeDetails { coin: Some(TEST_COIN_NAME.into()), amount: BigDecimal::from_str("0.00000452").unwrap(), @@ -3498,6 +3452,7 @@ fn test_account_balance_rpc() { derived_addresses: HDAddressesCache::default(), }); fields.derivation_method = DerivationMethod::HDWallet(UtxoHDWallet { + hd_wallet_rmd160: "21605444b36ec72780bdf52a5ffbc18288893664".into(), hd_wallet_storage: HDWalletCoinStorage::default(), address_format: UtxoAddressFormat::Standard, derivation_path: Bip44PathToCoin::from_str("m/44'/141'").unwrap(), @@ -3824,6 +3779,7 @@ fn test_scan_for_new_addresses() { derived_addresses: HDAddressesCache::default(), }); fields.derivation_method = DerivationMethod::HDWallet(UtxoHDWallet { + hd_wallet_rmd160: "21605444b36ec72780bdf52a5ffbc18288893664".into(), hd_wallet_storage: HDWalletCoinStorage::default(), address_format: UtxoAddressFormat::Standard, derivation_path: Bip44PathToCoin::from_str("m/44'/141'").unwrap(), @@ -3957,6 +3913,7 @@ fn test_get_new_address() { hd_accounts.insert(2, hd_account_for_test); fields.derivation_method = DerivationMethod::HDWallet(UtxoHDWallet { + hd_wallet_rmd160: "21605444b36ec72780bdf52a5ffbc18288893664".into(), hd_wallet_storage: HDWalletCoinStorage::default(), address_format: UtxoAddressFormat::Standard, derivation_path: Bip44PathToCoin::from_str("m/44'/141'").unwrap(), @@ -4250,3 +4207,9 @@ fn test_tx_enum_from_bytes() { discriminant(&TxMarshalingErr::CrossCheckFailed(String::new())) ); } + +#[test] +fn test_hd_utxo_tx_history() { + let client = electrum_client_for_test(MORTY_ELECTRUM_ADDRS); + block_on(utxo_common_tests::test_hd_utxo_tx_history_impl(client)); +} diff --git a/mm2src/coins/utxo/utxo_tx_history_v2.rs b/mm2src/coins/utxo/utxo_tx_history_v2.rs new file mode 100644 index 0000000000..f822a285b5 --- /dev/null +++ b/mm2src/coins/utxo/utxo_tx_history_v2.rs @@ -0,0 +1,726 @@ +use super::RequestTxHistoryResult; +use crate::hd_wallet::AddressDerivingError; +use crate::my_tx_history_v2::{CoinWithTxHistoryV2, DisplayAddress, TxHistoryStorage, TxHistoryStorageError}; +use crate::tx_history_storage::FilteringAddresses; +use crate::utxo::bch::BchCoin; +use crate::utxo::slp::ParseSlpScriptError; +use crate::utxo::{utxo_common, AddrFromStrError, GetBlockHeaderError}; +use crate::{BalanceError, BalanceResult, BlockHeightAndTime, HistorySyncState, MarketCoinOps, NumConversError, + ParseBigDecimalError, TransactionDetails, UnexpectedDerivationMethod, UtxoRpcError, UtxoTx}; +use async_trait::async_trait; +use common::executor::Timer; +use common::log::{error, info}; +use common::state_machine::prelude::*; +use derive_more::Display; +use keys::Address; +use mm2_err_handle::prelude::*; +use mm2_metrics::MetricsArc; +use mm2_number::BigDecimal; +use rpc::v1::types::H256 as H256Json; +use std::collections::{hash_map::Entry, HashMap, HashSet}; +use std::iter::FromIterator; +use std::str::FromStr; + +macro_rules! try_or_stop_unknown { + ($exp:expr, $fmt:literal) => { + match $exp { + Ok(t) => t, + Err(e) => return Self::change_state(Stopped::unknown(format!("{}: {}", $fmt, e))), + } + }; +} + +#[derive(Debug, Display)] +pub enum UtxoMyAddressesHistoryError { + AddressDerivingError(AddressDerivingError), + UnexpectedDerivationMethod(UnexpectedDerivationMethod), +} + +impl From for UtxoMyAddressesHistoryError { + fn from(e: AddressDerivingError) -> Self { UtxoMyAddressesHistoryError::AddressDerivingError(e) } +} + +impl From for UtxoMyAddressesHistoryError { + fn from(e: UnexpectedDerivationMethod) -> Self { UtxoMyAddressesHistoryError::UnexpectedDerivationMethod(e) } +} + +#[allow(clippy::large_enum_variant)] +#[derive(Debug, Display)] +pub enum UtxoTxDetailsError { + #[display(fmt = "Storage error: {}", _0)] + StorageError(String), + #[display(fmt = "Transaction deserialization error: {}", _0)] + TxDeserializationError(serialization::Error), + #[display(fmt = "Invalid transaction: {}", _0)] + InvalidTransaction(String), + #[display(fmt = "TX Address deserialization error: {}", _0)] + TxAddressDeserializationError(String), + #[display(fmt = "{}", _0)] + NumConversionErr(NumConversError), + #[display(fmt = "RPC error: {}", _0)] + RpcError(UtxoRpcError), + #[display(fmt = "Internal error: {}", _0)] + Internal(String), +} + +impl From for UtxoTxDetailsError { + fn from(e: serialization::Error) -> Self { UtxoTxDetailsError::TxDeserializationError(e) } +} + +impl From for UtxoTxDetailsError { + fn from(e: UtxoRpcError) -> Self { UtxoTxDetailsError::RpcError(e) } +} + +impl From for UtxoTxDetailsError { + fn from(e: NumConversError) -> Self { UtxoTxDetailsError::NumConversionErr(e) } +} + +impl From for UtxoTxDetailsError { + fn from(e: ParseBigDecimalError) -> Self { UtxoTxDetailsError::from(NumConversError::from(e)) } +} + +impl From for UtxoTxDetailsError { + fn from(err: ParseSlpScriptError) -> Self { + UtxoTxDetailsError::InvalidTransaction(format!("Error parsing SLP script: {err}")) + } +} + +impl From for UtxoTxDetailsError +where + StorageErr: TxHistoryStorageError, +{ + fn from(e: StorageErr) -> Self { UtxoTxDetailsError::StorageError(format!("{:?}", e)) } +} + +pub struct UtxoTxDetailsParams<'a, Storage> { + pub hash: &'a H256Json, + pub block_height_and_time: Option, + pub storage: &'a Storage, + pub my_addresses: &'a HashSet
, +} + +#[async_trait] +pub trait UtxoTxHistoryOps: CoinWithTxHistoryV2 + MarketCoinOps + Send + Sync + 'static { + /// Returns addresses for those we need to request Transaction history. + async fn my_addresses(&self) -> MmResult, UtxoMyAddressesHistoryError>; + + /// Returns Transaction details by hash using the coin RPC if required. + async fn tx_details_by_hash( + &self, + params: UtxoTxDetailsParams<'_, T>, + ) -> MmResult, UtxoTxDetailsError> + where + T: TxHistoryStorage; + + /// Loads transaction from `storage` or requests it using coin RPC. + async fn tx_from_storage_or_rpc( + &self, + tx_hash: &H256Json, + storage: &Storage, + ) -> MmResult; + + /// Requests transaction history. + async fn request_tx_history(&self, metrics: MetricsArc, for_addresses: &HashSet
) + -> RequestTxHistoryResult; + + /// Requests timestamp of the given block. + + async fn get_block_timestamp(&self, height: u64) -> MmResult; + + /// Requests balances of all activated coin's addresses. + async fn my_addresses_balances(&self) -> BalanceResult>; + + fn address_from_str(&self, address: &str) -> MmResult; + + /// Sets the history sync state. + fn set_history_sync_state(&self, new_state: HistorySyncState); +} + +struct UtxoTxHistoryCtx { + coin: Coin, + storage: Storage, + metrics: MetricsArc, + /// Last requested balances of the activated coin's addresses. + /// TODO add a `CoinBalanceState` structure and replace [`HashMap`] everywhere. + balances: HashMap, +} + +impl UtxoTxHistoryCtx +where + Coin: UtxoTxHistoryOps, + Storage: TxHistoryStorage, +{ + /// Requests balances for every activated address, updates the balances in [`UtxoTxHistoryCtx::balances`] + /// and returns the addresses whose balance has changed. + /// + /// # Note + /// + /// [`UtxoTxHistoryCtx::balances`] is changed if we successfully handled all balances **only**. + async fn updated_addresses(&mut self) -> BalanceResult> { + let current_balances = self.coin.my_addresses_balances().await?; + + // Create a copy of the CTX balances state. + // We must not to save any change of `ctx.balances` if an error occurs while processing `current_balances` collection. + let mut ctx_balances = self.balances.clone(); + + let mut updated_addresses = HashSet::with_capacity(ctx_balances.len()); + for (address, current_balance) in current_balances { + let updated_address = match ctx_balances.entry(address.clone()) { + // Do nothing if the balance hasn't been changed. + Entry::Occupied(entry) if *entry.get() == current_balance => continue, + Entry::Occupied(mut entry) => { + entry.insert(current_balance); + address + }, + Entry::Vacant(entry) => { + entry.insert(current_balance); + address + }, + }; + + // Currently, it's easier to convert `Address` from stringified address + // than to refactor `CoinBalanceReport` by replacing stringified addresses with a type parameter. + // Such refactoring will lead to huge code changes, complex and nested trait bounds. + // I personally think that it's overhead since, a least for now, + // we need to parse `CoinBalanceReport` within the transaction history only. + match self.coin.address_from_str(&updated_address) { + Ok(addr) => updated_addresses.insert(addr), + Err(e) => { + let (kind, trace) = e.split(); + let error = + format!("Error on converting address from 'UtxoTxHistoryOps::my_addresses_balances': {kind}"); + return MmError::err_with_trace(BalanceError::Internal(error), trace); + }, + }; + } + + // Save the changes in the context. + self.balances = ctx_balances; + + Ok(updated_addresses) + } +} + +// States have to be generic over storage type because BchAndSlpHistoryCtx is generic over it +struct Init { + phantom: std::marker::PhantomData<(Coin, Storage)>, +} + +impl Init { + fn new() -> Self { + Init { + phantom: Default::default(), + } + } +} + +impl TransitionFrom> for Stopped {} + +#[async_trait] +impl State for Init +where + Coin: UtxoTxHistoryOps, + Storage: TxHistoryStorage, +{ + type Ctx = UtxoTxHistoryCtx; + type Result = (); + + async fn on_changed(self: Box, ctx: &mut Self::Ctx) -> StateResult { + ctx.coin.set_history_sync_state(HistorySyncState::NotStarted); + + if let Err(e) = ctx.storage.init(&ctx.coin.history_wallet_id()).await { + return Self::change_state(Stopped::storage_error(e)); + } + + Self::change_state(FetchingTxHashes::for_all_addresses()) + } +} + +// States have to be generic over storage type because BchAndSlpHistoryCtx is generic over it +struct FetchingTxHashes { + fetch_for_addresses: Option>, + phantom: std::marker::PhantomData<(Coin, Storage)>, +} + +impl FetchingTxHashes { + fn for_all_addresses() -> Self { + FetchingTxHashes { + fetch_for_addresses: None, + phantom: Default::default(), + } + } + + fn for_addresses(fetch_for_addresses: HashSet
) -> Self { + FetchingTxHashes { + fetch_for_addresses: Some(fetch_for_addresses), + phantom: Default::default(), + } + } +} + +impl TransitionFrom> for FetchingTxHashes {} +impl TransitionFrom> for FetchingTxHashes {} +impl TransitionFrom> for FetchingTxHashes {} + +#[async_trait] +impl State for FetchingTxHashes +where + Coin: UtxoTxHistoryOps, + Storage: TxHistoryStorage, +{ + type Ctx = UtxoTxHistoryCtx; + type Result = (); + + async fn on_changed(self: Box, ctx: &mut Self::Ctx) -> StateResult { + let wallet_id = ctx.coin.history_wallet_id(); + if let Err(e) = ctx.storage.init(&wallet_id).await { + return Self::change_state(Stopped::storage_error(e)); + } + + let fetch_for_addresses = match self.fetch_for_addresses { + Some(for_addresses) => for_addresses, + // `fetch_for_addresses` hasn't been specified. Fetch TX hashses for all addresses. + None => try_or_stop_unknown!(ctx.coin.my_addresses().await, "Error on getting my addresses"), + }; + + let maybe_tx_ids = ctx + .coin + .request_tx_history(ctx.metrics.clone(), &fetch_for_addresses) + .await; + match maybe_tx_ids { + RequestTxHistoryResult::Ok(all_tx_ids_with_height) => { + let filtering_addresses = + FilteringAddresses::from_iter(fetch_for_addresses.iter().map(DisplayAddress::display_address)); + + let in_storage = match ctx + .storage + .unique_tx_hashes_num_in_history(&wallet_id, filtering_addresses) + .await + { + Ok(num) => num, + Err(e) => return Self::change_state(Stopped::storage_error(e)), + }; + if all_tx_ids_with_height.len() > in_storage { + let txes_left = all_tx_ids_with_height.len() - in_storage; + let new_state_json = json!({ "transactions_left": txes_left }); + ctx.coin + .set_history_sync_state(HistorySyncState::InProgress(new_state_json)); + } + + Self::change_state(UpdatingUnconfirmedTxes::new( + fetch_for_addresses, + all_tx_ids_with_height, + )) + }, + RequestTxHistoryResult::HistoryTooLarge => Self::change_state(Stopped::history_too_large()), + RequestTxHistoryResult::Retry { error } => { + error!("Error {} on requesting tx history for {}", error, ctx.coin.ticker()); + Self::change_state(OnIoErrorCooldown::new(fetch_for_addresses)) + }, + RequestTxHistoryResult::CriticalError(e) => { + error!( + "Critical error {} on requesting tx history for {}", + e, + ctx.coin.ticker() + ); + Self::change_state(Stopped::unknown(e)) + }, + } + } +} + +/// An I/O cooldown before `FetchingTxHashes` state. +/// States have to be generic over storage type because `UtxoTxHistoryCtx` is generic over it. +struct OnIoErrorCooldown { + /// The list of addresses of those we need to fetch TX hashes at the upcoming `FetchingTxHashses` state. + fetch_for_addresses: HashSet
, + phantom: std::marker::PhantomData<(Coin, Storage)>, +} + +impl OnIoErrorCooldown { + fn new(fetch_for_addresses: HashSet
) -> Self { + OnIoErrorCooldown { + fetch_for_addresses, + phantom: Default::default(), + } + } +} + +impl TransitionFrom> for OnIoErrorCooldown {} +impl TransitionFrom> for OnIoErrorCooldown {} +impl TransitionFrom> for OnIoErrorCooldown {} + +#[async_trait] +impl State for OnIoErrorCooldown +where + Coin: UtxoTxHistoryOps, + Storage: TxHistoryStorage, +{ + type Ctx = UtxoTxHistoryCtx; + type Result = (); + + async fn on_changed(mut self: Box, ctx: &mut Self::Ctx) -> StateResult { + loop { + Timer::sleep(30.).await; + + // We need to check whose balance has changed in these 30 seconds. + let updated_addresses = match ctx.updated_addresses().await { + Ok(updated) => updated, + Err(e) => { + error!("Error {e:?} on balance fetching for the coin {}", ctx.coin.ticker()); + continue; + }, + }; + + // We still need to fetch TX hashes for [`OnIoErrorCooldown::fetch_for_addresses`], + // but now we also need to fetch TX hashes for new `updated_addresses`. + // Merge these two containers. + self.fetch_for_addresses.extend(updated_addresses); + + return Self::change_state(FetchingTxHashes::for_addresses(self.fetch_for_addresses)); + } + } +} + +// States have to be generic over storage type because BchAndSlpHistoryCtx is generic over it +struct WaitForHistoryUpdateTrigger { + phantom: std::marker::PhantomData<(Coin, Storage)>, +} + +impl WaitForHistoryUpdateTrigger { + fn new() -> Self { + WaitForHistoryUpdateTrigger { + phantom: Default::default(), + } + } +} + +impl TransitionFrom> + for WaitForHistoryUpdateTrigger +{ +} + +#[async_trait] +impl State for WaitForHistoryUpdateTrigger +where + Coin: UtxoTxHistoryOps, + Storage: TxHistoryStorage, +{ + type Ctx = UtxoTxHistoryCtx; + type Result = (); + + async fn on_changed(self: Box, ctx: &mut Self::Ctx) -> StateResult { + let wallet_id = ctx.coin.history_wallet_id(); + loop { + Timer::sleep(30.).await; + + let my_addresses = try_or_stop_unknown!(ctx.coin.my_addresses().await, "Error on getting my addresses"); + let for_addresses = to_filtering_addresses(&my_addresses); + + match ctx + .storage + .history_contains_unconfirmed_txes(&wallet_id, for_addresses) + .await + { + // Fetch TX hashses for all addresses. + Ok(true) => return Self::change_state(FetchingTxHashes::for_addresses(my_addresses)), + Ok(false) => (), + Err(e) => return Self::change_state(Stopped::storage_error(e)), + } + + let updated_addresses = match ctx.updated_addresses().await { + Ok(updated) => updated, + Err(e) => { + error!("Error {e:?} on balance fetching for the coin {}", ctx.coin.ticker()); + continue; + }, + }; + + if !updated_addresses.is_empty() { + // Fetch TX hashes for those addresses whose balance has changed only. + return Self::change_state(FetchingTxHashes::for_addresses(updated_addresses)); + } + } + } +} + +// States have to be generic over storage type because BchAndSlpHistoryCtx is generic over it +struct UpdatingUnconfirmedTxes { + /// The list of addresses for those we have requested [`UpdatingUnconfirmedTxes::all_tx_ids_with_height`] TX hashses + /// at the `FetchingTxHashes` state. + requested_for_addresses: HashSet
, + all_tx_ids_with_height: Vec<(H256Json, u64)>, + phantom: std::marker::PhantomData<(Coin, Storage)>, +} + +impl UpdatingUnconfirmedTxes { + fn new(requested_for_addresses: HashSet
, all_tx_ids_with_height: Vec<(H256Json, u64)>) -> Self { + UpdatingUnconfirmedTxes { + requested_for_addresses, + all_tx_ids_with_height, + phantom: Default::default(), + } + } +} + +impl TransitionFrom> for UpdatingUnconfirmedTxes {} + +#[async_trait] +impl State for UpdatingUnconfirmedTxes +where + Coin: UtxoTxHistoryOps, + Storage: TxHistoryStorage, +{ + type Ctx = UtxoTxHistoryCtx; + type Result = (); + + async fn on_changed(self: Box, ctx: &mut Self::Ctx) -> StateResult { + let wallet_id = ctx.coin.history_wallet_id(); + + let for_addresses = to_filtering_addresses(&self.requested_for_addresses); + let unconfirmed = match ctx + .storage + .get_unconfirmed_txes_from_history(&wallet_id, for_addresses) + .await + { + Ok(unconfirmed) => unconfirmed, + Err(e) => return Self::change_state(Stopped::storage_error(e)), + }; + + let txs_with_height: HashMap = self.all_tx_ids_with_height.clone().into_iter().collect(); + for mut tx in unconfirmed { + let found = match H256Json::from_str(&tx.tx_hash) { + Ok(unconfirmed_tx_hash) => txs_with_height.get(&unconfirmed_tx_hash), + Err(_) => None, + }; + + match found { + Some(height) => { + if *height > 0 { + match ctx.coin.get_block_timestamp(*height).await { + Ok(time) => tx.timestamp = time, + Err(_) => return Self::change_state(OnIoErrorCooldown::new(self.requested_for_addresses)), + }; + tx.block_height = *height; + if let Err(e) = ctx.storage.update_tx_in_history(&wallet_id, &tx).await { + return Self::change_state(Stopped::storage_error(e)); + } + } + }, + None => { + // This can potentially happen when unconfirmed tx is removed from mempool for some reason. + // Or if the hash is undecodable. We should remove it from storage too. + if let Err(e) = ctx.storage.remove_tx_from_history(&wallet_id, &tx.internal_id).await { + return Self::change_state(Stopped::storage_error(e)); + } + }, + } + } + + Self::change_state(FetchingTransactionsData::new( + self.requested_for_addresses, + self.all_tx_ids_with_height, + )) + } +} + +// States have to be generic over storage type because BchAndSlpHistoryCtx is generic over it +struct FetchingTransactionsData { + /// The list of addresses for those we have requested [`UpdatingUnconfirmedTxes::all_tx_ids_with_height`] TX hashses + /// at the `FetchingTxHashes` state. + requested_for_addresses: HashSet
, + all_tx_ids_with_height: Vec<(H256Json, u64)>, + phantom: std::marker::PhantomData<(Coin, Storage)>, +} + +impl TransitionFrom> for FetchingTransactionsData {} + +impl FetchingTransactionsData { + fn new(requested_for_addresses: HashSet
, all_tx_ids_with_height: Vec<(H256Json, u64)>) -> Self { + FetchingTransactionsData { + requested_for_addresses, + all_tx_ids_with_height, + phantom: Default::default(), + } + } +} + +#[async_trait] +impl State for FetchingTransactionsData +where + Coin: UtxoTxHistoryOps, + Storage: TxHistoryStorage, +{ + type Ctx = UtxoTxHistoryCtx; + type Result = (); + + async fn on_changed(self: Box, ctx: &mut Self::Ctx) -> StateResult { + let ticker = ctx.coin.ticker(); + let wallet_id = ctx.coin.history_wallet_id(); + + let my_addresses = try_or_stop_unknown!(ctx.coin.my_addresses().await, "Error on getting my addresses"); + + for (tx_hash, height) in self.all_tx_ids_with_height { + let tx_hash_string = format!("{:02x}", tx_hash); + match ctx.storage.history_has_tx_hash(&wallet_id, &tx_hash_string).await { + Ok(true) => continue, + Ok(false) => (), + Err(e) => return Self::change_state(Stopped::storage_error(e)), + } + + let block_height_and_time = if height > 0 { + let timestamp = match ctx.coin.get_block_timestamp(height).await { + Ok(time) => time, + Err(_) => return Self::change_state(OnIoErrorCooldown::new(self.requested_for_addresses)), + }; + Some(BlockHeightAndTime { height, timestamp }) + } else { + None + }; + let params = UtxoTxDetailsParams { + hash: &tx_hash, + block_height_and_time, + storage: &ctx.storage, + my_addresses: &my_addresses, + }; + let tx_details = match ctx.coin.tx_details_by_hash(params).await { + Ok(tx) => tx, + Err(e) => { + error!("Error on getting {ticker} tx details for hash {tx_hash:02x}: {e}"); + return Self::change_state(OnIoErrorCooldown::new(self.requested_for_addresses)); + }, + }; + + if let Err(e) = ctx.storage.add_transactions_to_history(&wallet_id, tx_details).await { + return Self::change_state(Stopped::storage_error(e)); + } + + // wait for for one second to reduce the number of requests to electrum servers + Timer::sleep(1.).await; + } + info!("Tx history fetching finished for {ticker}"); + ctx.coin.set_history_sync_state(HistorySyncState::Finished); + Self::change_state(WaitForHistoryUpdateTrigger::new()) + } +} + +#[derive(Debug)] +enum StopReason { + HistoryTooLarge, + StorageError(String), + UnknownError(String), +} + +struct Stopped { + phantom: std::marker::PhantomData<(Coin, Storage)>, + stop_reason: StopReason, +} + +impl Stopped { + fn history_too_large() -> Self { + Stopped { + phantom: Default::default(), + stop_reason: StopReason::HistoryTooLarge, + } + } + + fn storage_error(e: E) -> Self + where + E: std::fmt::Debug, + { + Stopped { + phantom: Default::default(), + stop_reason: StopReason::StorageError(format!("{:?}", e)), + } + } + + fn unknown(e: String) -> Self { + Stopped { + phantom: Default::default(), + stop_reason: StopReason::UnknownError(e), + } + } +} + +impl TransitionFrom> for Stopped {} +impl TransitionFrom> for Stopped {} +impl TransitionFrom> for Stopped {} +impl TransitionFrom> for Stopped {} + +#[async_trait] +impl LastState for Stopped +where + Coin: UtxoTxHistoryOps, + Storage: TxHistoryStorage, +{ + type Ctx = UtxoTxHistoryCtx; + type Result = (); + + async fn on_changed(self: Box, ctx: &mut Self::Ctx) -> Self::Result { + info!( + "Stopping tx history fetching for {}. Reason: {:?}", + ctx.coin.ticker(), + self.stop_reason + ); + let new_state_json = match self.stop_reason { + StopReason::HistoryTooLarge => json!({ + "code": utxo_common::HISTORY_TOO_LARGE_ERR_CODE, + "message": "Got `history too large` error from Electrum server. History is not available", + }), + reason => json!({ + "message": format!("{:?}", reason), + }), + }; + ctx.coin.set_history_sync_state(HistorySyncState::Error(new_state_json)); + } +} + +pub async fn bch_and_slp_history_loop( + coin: BchCoin, + storage: impl TxHistoryStorage, + metrics: MetricsArc, + current_balance: BigDecimal, +) { + let my_address = match coin.my_address() { + Ok(my_address) => my_address, + Err(e) => { + error!("{}", e); + return; + }, + }; + let mut balances = HashMap::new(); + balances.insert(my_address, current_balance); + drop_mutability!(balances); + + let ctx = UtxoTxHistoryCtx { + coin, + storage, + metrics, + balances, + }; + let state_machine: StateMachine<_, ()> = StateMachine::from_ctx(ctx); + state_machine.run(Init::new()).await; +} + +pub async fn utxo_history_loop( + coin: Coin, + storage: Storage, + metrics: MetricsArc, + current_balances: HashMap, +) where + Coin: UtxoTxHistoryOps, + Storage: TxHistoryStorage, +{ + let ctx = UtxoTxHistoryCtx { + coin, + storage, + metrics, + balances: current_balances, + }; + let state_machine: StateMachine<_, ()> = StateMachine::from_ctx(ctx); + state_machine.run(Init::new()).await; +} + +fn to_filtering_addresses(addresses: &HashSet
) -> FilteringAddresses { + FilteringAddresses::from_iter(addresses.iter().map(DisplayAddress::display_address)) +} diff --git a/mm2src/coins/utxo/utxo_wasm_tests.rs b/mm2src/coins/utxo/utxo_wasm_tests.rs index 9ba8949500..4eae4ed3e2 100644 --- a/mm2src/coins/utxo/utxo_wasm_tests.rs +++ b/mm2src/coins/utxo/utxo_wasm_tests.rs @@ -64,3 +64,9 @@ 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; } + +#[wasm_bindgen_test] +async fn test_hd_utxo_tx_history() { + let rpc_client = electrum_client_for_test(&["electrum1.cipig.net:30018", "electrum2.cipig.net:30018"]).await; + utxo_common_tests::test_hd_utxo_tx_history_impl(rpc_client).await; +} diff --git a/mm2src/coins/utxo/utxo_withdraw.rs b/mm2src/coins/utxo/utxo_withdraw.rs index 29c9001c26..68736253ad 100644 --- a/mm2src/coins/utxo/utxo_withdraw.rs +++ b/mm2src/coins/utxo/utxo_withdraw.rs @@ -125,9 +125,7 @@ where let conf = &self.coin().as_ref().conf; let req = self.request(); - let to = coin - .address_from_str(&req.to) - .map_to_mm(WithdrawError::InvalidAddress)?; + let to = coin.address_from_str(&req.to)?; let is_p2pkh = to.prefix == conf.pub_addr_prefix && to.t_addr_prefix == conf.pub_t_addr_prefix; let is_p2sh = to.prefix == conf.p2sh_addr_prefix && to.t_addr_prefix == conf.p2sh_t_addr_prefix; diff --git a/mm2src/coins/z_coin.rs b/mm2src/coins/z_coin.rs index 7e9f0dc536..f1a37355d2 100644 --- a/mm2src/coins/z_coin.rs +++ b/mm2src/coins/z_coin.rs @@ -6,8 +6,8 @@ use crate::utxo::utxo_builder::{UtxoCoinBuilderCommonOps, UtxoCoinWithIguanaPriv UtxoFieldsWithIguanaPrivKeyBuilder}; use crate::utxo::utxo_common::{addresses_from_script, big_decimal_from_sat, big_decimal_from_sat_unsigned, payment_script}; -use crate::utxo::{sat_from_big_decimal, utxo_common, ActualTxFee, AdditionalTxData, Address, BroadcastTxErr, - FeePolicy, GetUtxoListOps, HistoryUtxoTx, HistoryUtxoTxMap, MatureUnspentList, +use crate::utxo::{sat_from_big_decimal, utxo_common, ActualTxFee, AdditionalTxData, AddrFromStrError, Address, + BroadcastTxErr, FeePolicy, GetUtxoListOps, HistoryUtxoTx, HistoryUtxoTxMap, MatureUnspentList, RecentlySpentOutPointsGuard, UtxoActivationParams, UtxoAddressFormat, UtxoArc, UtxoCoinFields, UtxoCommonOps, UtxoFeeDetails, UtxoRpcMode, UtxoTxBroadcastOps, UtxoTxGenerationOps, VerboseTransactionFrom}; @@ -661,6 +661,7 @@ impl ZCoin { Ok(MyTxHistoryResponseV2 { coin: self.ticker().into(), + target: request.target, current_block, transactions, // Zcoin is activated only after the state is synced @@ -688,7 +689,7 @@ pub enum ZcoinRpcMode { }, } -#[derive(Deserialize)] +#[derive(Clone, Deserialize)] pub struct ZcoinActivationParams { pub mode: ZcoinRpcMode, pub required_confirmations: Option, @@ -1536,7 +1537,7 @@ impl UtxoCommonOps for ZCoin { utxo_common::my_public_key(self.as_ref()) } - fn address_from_str(&self, address: &str) -> Result { + fn address_from_str(&self, address: &str) -> MmResult { utxo_common::checked_address_from_str(self, address) } diff --git a/mm2src/coins_activation/Cargo.toml b/mm2src/coins_activation/Cargo.toml index 234ff102ef..3ccb2a00c3 100644 --- a/mm2src/coins_activation/Cargo.toml +++ b/mm2src/coins_activation/Cargo.toml @@ -9,15 +9,15 @@ edition = "2018" async-trait = "0.1" coins = { path = "../coins" } common = { path = "../common" } +crypto = { path = "../crypto" } +derive_more = "0.99" ethereum-types = { version = "0.4", default-features = false, features = ["std", "serialize"] } +futures = { version = "0.3", package = "futures", features = ["compat", "async-await"] } +hex = "0.4.2" mm2_core = { path = "../mm2_core" } mm2_err_handle = { path = "../mm2_err_handle" } mm2_metrics = { path = "../mm2_metrics" } mm2_number = { path = "../mm2_number" } -crypto = { path = "../crypto" } -derive_more = "0.99" -futures = { version = "0.3", package = "futures", features = ["compat", "async-await"] } -hex = "0.4.2" rpc = { path = "../mm2_bitcoin/rpc" } rpc_task = { path = "../rpc_task" } ser_error = { path = "../derives/ser_error" } diff --git a/mm2src/coins_activation/src/bch_with_tokens_activation.rs b/mm2src/coins_activation/src/bch_with_tokens_activation.rs index 0ce4580e12..fa4c317188 100644 --- a/mm2src/coins_activation/src/bch_with_tokens_activation.rs +++ b/mm2src/coins_activation/src/bch_with_tokens_activation.rs @@ -4,9 +4,9 @@ use crate::slp_token_activation::SlpActivationRequest; use async_trait::async_trait; use coins::my_tx_history_v2::TxHistoryStorage; use coins::utxo::bch::{bch_coin_from_conf_and_params, BchActivationRequest, BchCoin, CashAddrPrefix}; -use coins::utxo::bch_and_slp_tx_history::bch_and_slp_history_loop; use coins::utxo::rpc_clients::UtxoRpcError; use coins::utxo::slp::{SlpProtocolConf, SlpToken}; +use coins::utxo::utxo_tx_history_v2::bch_and_slp_history_loop; use coins::utxo::UtxoCommonOps; use coins::{CoinBalance, CoinProtocol, MarketCoinOps, MmCoin, PrivKeyNotAllowed, UnexpectedDerivationMethod}; use common::executor::spawn; diff --git a/mm2src/coins_activation/src/prelude.rs b/mm2src/coins_activation/src/prelude.rs index 63014af89e..57abf7c891 100644 --- a/mm2src/coins_activation/src/prelude.rs +++ b/mm2src/coins_activation/src/prelude.rs @@ -4,6 +4,7 @@ use coins::z_coin::ZcoinActivationParams; use coins::{coin_conf, CoinBalance, CoinProtocol, MmCoinEnum}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; +use mm2_number::BigDecimal; use serde_derive::Serialize; use serde_json::{self as json, Value as Json}; use std::collections::HashMap; @@ -25,6 +26,10 @@ impl TxHistory for ZcoinActivationParams { fn tx_history(&self) -> bool { false } } +pub trait GetAddressesBalances { + fn get_addresses_balances(&self) -> HashMap; +} + #[derive(Clone, Debug, Serialize)] #[serde(tag = "type", content = "data")] pub enum DerivationMethod { diff --git a/mm2src/coins_activation/src/standalone_coin/init_standalone_coin.rs b/mm2src/coins_activation/src/standalone_coin/init_standalone_coin.rs index 046eb91c3d..528a160a78 100644 --- a/mm2src/coins_activation/src/standalone_coin/init_standalone_coin.rs +++ b/mm2src/coins_activation/src/standalone_coin/init_standalone_coin.rs @@ -4,15 +4,21 @@ use crate::standalone_coin::init_standalone_coin_error::{CancelInitStandaloneCoi InitStandaloneCoinStatusError, InitStandaloneCoinUserActionError}; use async_trait::async_trait; +use coins::my_tx_history_v2::TxHistoryStorage; +use coins::tx_history_storage::{CreateTxHistoryStorageError, TxHistoryStorageBuilder}; use coins::{disable_coin, lp_coinfind, lp_register_coin, MmCoinEnum, RegisterCoinError, RegisterCoinParams}; use common::{log, SuccessResponse}; use crypto::trezor::trezor_rpc_task::RpcTaskHandle; +use futures::future::AbortHandle; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; +use mm2_metrics::MetricsArc; +use mm2_number::BigDecimal; use rpc_task::rpc_common::{CancelRpcTaskRequest, InitRpcTaskResponse, RpcTaskStatusRequest, RpcTaskUserActionRequest}; use rpc_task::{RpcTask, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, RpcTaskTypes}; use serde_derive::Deserialize; use serde_json::Value as Json; +use std::collections::HashMap; pub type InitStandaloneCoinResponse = InitRpcTaskResponse; pub type InitStandaloneCoinStatusRequest = RpcTaskStatusRequest; @@ -28,11 +34,12 @@ pub struct InitStandaloneCoinReq { #[async_trait] pub trait InitStandaloneCoinActivationOps: Into + Send + Sync + 'static { - type ActivationRequest: TxHistory + Sync + Send; + type ActivationRequest: TxHistory + Clone + Send + Sync; type StandaloneProtocol: TryFromCoinProtocol + Clone + Send + Sync; // The following types are related to `RpcTask` management. - type ActivationResult: serde::Serialize + Clone + CurrentBlock + Send + Sync + 'static; + type ActivationResult: serde::Serialize + Clone + CurrentBlock + GetAddressesBalances + Send + Sync + 'static; type ActivationError: From + + From + Into + SerMmErrorType + NotEqual @@ -62,6 +69,13 @@ pub trait InitStandaloneCoinActivationOps: Into + Send + Sync + 'sta task_handle: &InitStandaloneCoinTaskHandle, activation_request: &Self::ActivationRequest, ) -> Result>; + + fn start_history_background_fetching( + &self, + metrics: MetricsArc, + storage: impl TxHistoryStorage, + current_balances: HashMap, + ) -> Option; } pub async fn init_standalone_coin( @@ -190,8 +204,18 @@ where log::info!("{} current block {}", ticker, result.current_block()); let tx_history = self.request.activation_params.tx_history(); - - lp_register_coin(&self.ctx, coin.into(), RegisterCoinParams { ticker, tx_history }).await?; + if tx_history { + let current_balances = result.get_addresses_balances(); + if let Some(abort_handle) = coin.start_history_background_fetching( + self.ctx.metrics.clone(), + TxHistoryStorageBuilder::new(&self.ctx).build()?, + current_balances, + ) { + self.ctx.abort_handlers.lock().unwrap().push(abort_handle); + } + } + + lp_register_coin(&self.ctx, coin.into(), RegisterCoinParams { ticker }).await?; Ok(result) } diff --git a/mm2src/coins_activation/src/utxo_activation/common_impl.rs b/mm2src/coins_activation/src/utxo_activation/common_impl.rs index 303c32490b..d647ba3335 100644 --- a/mm2src/coins_activation/src/utxo_activation/common_impl.rs +++ b/mm2src/coins_activation/src/utxo_activation/common_impl.rs @@ -5,15 +5,23 @@ use crate::utxo_activation::init_utxo_standard_statuses::{UtxoStandardAwaitingSt use crate::utxo_activation::utxo_standard_activation_result::UtxoStandardActivationResult; use coins::coin_balance::EnableCoinBalanceOps; use coins::hd_pubkey::RpcTaskXPubExtractor; +use coins::my_tx_history_v2::TxHistoryStorage; +use coins::utxo::utxo_tx_history_v2::{utxo_history_loop, UtxoTxHistoryOps}; use coins::utxo::UtxoActivationParams; use coins::{MarketCoinOps, PrivKeyActivationPolicy, PrivKeyBuildPolicy}; +use common::executor::spawn; +use common::log::info; use crypto::hw_rpc_task::HwConnectStatuses; use crypto::CryptoCtx; use futures::compat::Future01CompatExt; +use futures::future::{abortable, AbortHandle}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; +use mm2_metrics::MetricsArc; +use mm2_number::BigDecimal; +use std::collections::HashMap; -pub async fn get_activation_result( +pub(crate) async fn get_activation_result( ctx: &MmArc, coin: &Coin, task_handle: &InitStandaloneCoinTaskHandle, @@ -54,7 +62,8 @@ where Ok(result) } -pub fn xpub_extractor_rpc_statuses() -> HwConnectStatuses { +pub(crate) fn xpub_extractor_rpc_statuses( +) -> HwConnectStatuses { HwConnectStatuses { on_connect: UtxoStandardInProgressStatus::WaitingForTrezorToConnect, on_connected: UtxoStandardInProgressStatus::ActivatingCoin, @@ -66,9 +75,32 @@ pub fn xpub_extractor_rpc_statuses() -> HwConnectStatuses PrivKeyBuildPolicy { +pub(crate) fn priv_key_build_policy( + crypto_ctx: &CryptoCtx, + activation_policy: PrivKeyActivationPolicy, +) -> PrivKeyBuildPolicy { match activation_policy { PrivKeyActivationPolicy::IguanaPrivKey => PrivKeyBuildPolicy::iguana_priv_key(crypto_ctx), PrivKeyActivationPolicy::Trezor => PrivKeyBuildPolicy::Trezor, } } + +pub(crate) fn start_history_background_fetching( + coin: Coin, + metrics: MetricsArc, + storage: impl TxHistoryStorage, + current_balances: HashMap, +) -> AbortHandle +where + Coin: UtxoTxHistoryOps, +{ + let ticker = coin.ticker().to_owned(); + + let (fut, abort_handle) = abortable(utxo_history_loop(coin, storage, metrics, current_balances)); + spawn(async move { + if let Err(e) = fut.await { + info!("'utxo_history_loop' stopped for {}, reason {}", ticker, e); + } + }); + abort_handle +} diff --git a/mm2src/coins_activation/src/utxo_activation/init_qtum_activation.rs b/mm2src/coins_activation/src/utxo_activation/init_qtum_activation.rs index 4266e2b0aa..9aaee6e307 100644 --- a/mm2src/coins_activation/src/utxo_activation/init_qtum_activation.rs +++ b/mm2src/coins_activation/src/utxo_activation/init_qtum_activation.rs @@ -2,20 +2,26 @@ use crate::context::CoinsActivationContext; use crate::prelude::TryFromCoinProtocol; use crate::standalone_coin::{InitStandaloneCoinActivationOps, InitStandaloneCoinTaskHandle, InitStandaloneCoinTaskManagerShared}; -use crate::utxo_activation::common_impl::{get_activation_result, priv_key_build_policy}; +use crate::utxo_activation::common_impl::{get_activation_result, priv_key_build_policy, + start_history_background_fetching}; use crate::utxo_activation::init_utxo_standard_activation_error::InitUtxoStandardError; use crate::utxo_activation::init_utxo_standard_statuses::{UtxoStandardAwaitingStatus, UtxoStandardInProgressStatus, UtxoStandardUserAction}; use crate::utxo_activation::utxo_standard_activation_result::UtxoStandardActivationResult; use async_trait::async_trait; +use coins::my_tx_history_v2::TxHistoryStorage; use coins::utxo::qtum::{QtumCoin, QtumCoinBuilder}; use coins::utxo::utxo_builder::UtxoCoinBuilder; use coins::utxo::UtxoActivationParams; use coins::CoinProtocol; use crypto::CryptoCtx; +use futures::future::AbortHandle; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; +use mm2_metrics::MetricsArc; +use mm2_number::BigDecimal; use serde_json::Value as Json; +use std::collections::HashMap; pub type QtumTaskManagerShared = InitStandaloneCoinTaskManagerShared; pub type QtumRpcTaskHandle = InitStandaloneCoinTaskHandle; @@ -75,4 +81,18 @@ impl InitStandaloneCoinActivationOps for QtumCoin { ) -> MmResult { get_activation_result(&ctx, self, task_handle, activation_request).await } + + fn start_history_background_fetching( + &self, + metrics: MetricsArc, + storage: impl TxHistoryStorage, + current_balances: HashMap, + ) -> Option { + Some(start_history_background_fetching( + self.clone(), + metrics, + storage, + current_balances, + )) + } } diff --git a/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_activation.rs b/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_activation.rs index a4af39902e..ea495f0b08 100644 --- a/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_activation.rs +++ b/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_activation.rs @@ -2,21 +2,26 @@ use crate::context::CoinsActivationContext; use crate::prelude::TryFromCoinProtocol; use crate::standalone_coin::{InitStandaloneCoinActivationOps, InitStandaloneCoinTaskHandle, InitStandaloneCoinTaskManagerShared}; -use crate::utxo_activation::common_impl::{get_activation_result, priv_key_build_policy}; +use crate::utxo_activation::common_impl::{get_activation_result, priv_key_build_policy, + start_history_background_fetching}; use crate::utxo_activation::init_utxo_standard_activation_error::InitUtxoStandardError; use crate::utxo_activation::init_utxo_standard_statuses::{UtxoStandardAwaitingStatus, UtxoStandardInProgressStatus, UtxoStandardUserAction}; use crate::utxo_activation::utxo_standard_activation_result::UtxoStandardActivationResult; use async_trait::async_trait; +use coins::my_tx_history_v2::TxHistoryStorage; use coins::utxo::utxo_builder::{UtxoArcBuilder, UtxoCoinBuilder}; use coins::utxo::utxo_standard::UtxoStandardCoin; use coins::utxo::{UtxoActivationParams, UtxoSyncStatus}; use coins::CoinProtocol; use crypto::CryptoCtx; -use futures::StreamExt; +use futures::{future::AbortHandle, StreamExt}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; +use mm2_metrics::MetricsArc; +use mm2_number::BigDecimal; use serde_json::Value as Json; +use std::collections::HashMap; pub type UtxoStandardTaskManagerShared = InitStandaloneCoinTaskManagerShared; pub type UtxoStandardRpcTaskHandle = InitStandaloneCoinTaskHandle; @@ -116,4 +121,18 @@ impl InitStandaloneCoinActivationOps for UtxoStandardCoin { ) -> MmResult { get_activation_result(&ctx, self, task_handle, activation_request).await } + + fn start_history_background_fetching( + &self, + metrics: MetricsArc, + storage: impl TxHistoryStorage, + current_balances: HashMap, + ) -> Option { + Some(start_history_background_fetching( + self.clone(), + metrics, + storage, + current_balances, + )) + } } diff --git a/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_activation_error.rs b/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_activation_error.rs index e34a1b12b5..0a4e355954 100644 --- a/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_activation_error.rs +++ b/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_activation_error.rs @@ -1,6 +1,7 @@ use crate::standalone_coin::InitStandaloneCoinError; use coins::coin_balance::EnableCoinBalanceError; use coins::hd_wallet::{NewAccountCreatingError, NewAddressDerivingError}; +use coins::tx_history_storage::CreateTxHistoryStorageError; use coins::utxo::utxo_builder::UtxoCoinBuildError; use coins::{BalanceError, RegisterCoinError}; use crypto::{CryptoInitError, HwError, HwRpcError}; @@ -41,6 +42,14 @@ impl From for InitUtxoStandardError { fn from(crypto_err: CryptoInitError) -> Self { InitUtxoStandardError::Internal(crypto_err.to_string()) } } +impl From for InitUtxoStandardError { + fn from(e: CreateTxHistoryStorageError) -> Self { + match e { + CreateTxHistoryStorageError::Internal(internal) => InitUtxoStandardError::Internal(internal), + } + } +} + impl From for InitStandaloneCoinError { fn from(e: InitUtxoStandardError) -> Self { match e { diff --git a/mm2src/coins_activation/src/utxo_activation/utxo_standard_activation_result.rs b/mm2src/coins_activation/src/utxo_activation/utxo_standard_activation_result.rs index b5eae67e45..aa54ec3698 100644 --- a/mm2src/coins_activation/src/utxo_activation/utxo_standard_activation_result.rs +++ b/mm2src/coins_activation/src/utxo_activation/utxo_standard_activation_result.rs @@ -1,14 +1,22 @@ -use crate::prelude::CurrentBlock; -use coins::coin_balance::EnableCoinBalance; +use crate::prelude::{CurrentBlock, GetAddressesBalances}; +use coins::coin_balance::CoinBalanceReport; +use mm2_number::BigDecimal; use serde_derive::Serialize; +use std::collections::HashMap; #[derive(Clone, Serialize)] pub struct UtxoStandardActivationResult { pub ticker: String, pub current_block: u64, - pub wallet_balance: EnableCoinBalance, + pub wallet_balance: CoinBalanceReport, } impl CurrentBlock for UtxoStandardActivationResult { fn current_block(&self) -> u64 { self.current_block } } + +impl GetAddressesBalances for UtxoStandardActivationResult { + fn get_addresses_balances(&self) -> HashMap { + self.wallet_balance.to_addresses_total_balances() + } +} diff --git a/mm2src/coins_activation/src/z_coin_activation.rs b/mm2src/coins_activation/src/z_coin_activation.rs index c1580d6dab..279fd9b72e 100644 --- a/mm2src/coins_activation/src/z_coin_activation.rs +++ b/mm2src/coins_activation/src/z_coin_activation.rs @@ -4,7 +4,9 @@ use crate::standalone_coin::{InitStandaloneCoinActivationOps, InitStandaloneCoin InitStandaloneCoinInitialStatus, InitStandaloneCoinTaskHandle, InitStandaloneCoinTaskManagerShared}; use async_trait::async_trait; -use coins::coin_balance::{EnableCoinBalance, IguanaWalletBalance}; +use coins::coin_balance::{CoinBalanceReport, IguanaWalletBalance}; +use coins::my_tx_history_v2::TxHistoryStorage; +use coins::tx_history_storage::CreateTxHistoryStorageError; use coins::z_coin::{z_coin_from_conf_and_params, BlockchainScanStopped, SyncStatus, ZCoin, ZCoinBuildError, ZcoinActivationParams, ZcoinProtocolInfo}; use coins::{BalanceError, CoinProtocol, MarketCoinOps, RegisterCoinError}; @@ -12,12 +14,16 @@ use crypto::hw_rpc_task::{HwRpcTaskAwaitingStatus, HwRpcTaskUserAction}; use crypto::CryptoInitError; use derive_more::Display; use futures::compat::Future01CompatExt; +use futures::future::AbortHandle; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; +use mm2_metrics::MetricsArc; +use mm2_number::BigDecimal; use rpc_task::RpcTaskError; use ser_error_derive::SerializeErrorType; use serde_derive::Serialize; use serde_json::Value as Json; +use std::collections::HashMap; use std::time::Duration; pub type ZcoinTaskManagerShared = InitStandaloneCoinTaskManagerShared; @@ -29,13 +35,19 @@ pub type ZcoinUserAction = HwRpcTaskUserAction; pub struct ZcoinActivationResult { pub ticker: String, pub current_block: u64, - pub wallet_balance: EnableCoinBalance, + pub wallet_balance: CoinBalanceReport, } impl CurrentBlock for ZcoinActivationResult { fn current_block(&self) -> u64 { self.current_block } } +impl GetAddressesBalances for ZcoinActivationResult { + fn get_addresses_balances(&self) -> HashMap { + self.wallet_balance.to_addresses_total_balances() + } +} + #[derive(Clone, Serialize)] #[non_exhaustive] pub enum ZcoinInProgressStatus { @@ -124,6 +136,14 @@ impl From for ZcoinInitError { fn from(e: BlockchainScanStopped) -> Self { ZcoinInitError::Internal(e.to_string()) } } +impl From for ZcoinInitError { + fn from(e: CreateTxHistoryStorageError) -> Self { + match e { + CreateTxHistoryStorageError::Internal(internal) => ZcoinInitError::Internal(internal), + } + } +} + impl From for InitStandaloneCoinError { fn from(err: ZcoinInitError) -> Self { match err { @@ -233,10 +253,20 @@ impl InitStandaloneCoinActivationOps for ZCoin { Ok(ZcoinActivationResult { ticker: self.ticker().into(), current_block, - wallet_balance: EnableCoinBalance::Iguana(IguanaWalletBalance { + wallet_balance: CoinBalanceReport::Iguana(IguanaWalletBalance { address: self.my_z_address_encoded(), balance, }), }) } + + /// Transaction history is fetching from a wallet database for `ZCoin`. + fn start_history_background_fetching( + &self, + _metrics: MetricsArc, + _storage: impl TxHistoryStorage, + _current_balances: HashMap, + ) -> Option { + None + } } diff --git a/mm2src/db_common/src/sql_query.rs b/mm2src/db_common/src/sql_query.rs index 0c1f496ba2..287bd8db47 100644 --- a/mm2src/db_common/src/sql_query.rs +++ b/mm2src/db_common/src/sql_query.rs @@ -69,6 +69,17 @@ impl<'a> SqlQuery<'a> { Ok(self) } + /// Add COUNT(DISTINCT field). + /// For more details see [`SqlBuilder::count`]. + /// + /// Please note the function validates the given `field`. + #[inline] + pub fn count_distinct(&mut self, field: S) -> SqlResult<&mut Self> { + let field = field.to_valid_sql_ident()?; + self.sql_builder.count(format!("DISTINCT {}", field)); + Ok(self) + } + /// Add field. /// For more details see [`SqlBuilder::field`]. /// diff --git a/mm2src/mm2_main/src/mm2_tests.rs b/mm2src/mm2_main/src/mm2_tests.rs index c632796ee0..90145b0eb9 100644 --- a/mm2src/mm2_main/src/mm2_tests.rs +++ b/mm2src/mm2_main/src/mm2_tests.rs @@ -684,7 +684,9 @@ fn test_check_balance_on_order_post() { // Enable coins. Print the replies in case we need the "address". log!( "enable_coins (bob): {:?}", - block_on(enable_coins_eth_electrum(&mm, &["http://eth1.cipig.net:8555"])) + block_on(enable_coins_eth_electrum(&mm, &[ + "https://mainnet.infura.io/v3/c01c1b4cf66642528547624e1d6d9d6b" + ])) ); // issue sell request by setting base/rel price @@ -3631,9 +3633,7 @@ fn test_convert_segwit_address() { "!convertaddress success but should be error: {}", rc.1 ); - assert!(rc - .1 - .contains("Invalid address: ltc1qdkwjk42dw6pryvs9sl0ht3pn3mxghuma64jst5")); + assert!(rc.1.contains("Cannot determine format")); } #[test] @@ -4375,7 +4375,7 @@ fn test_validateaddress_segwit() { assert!(!result["is_valid"].as_bool().unwrap()); let reason = result["reason"].as_str().unwrap(); log!("{}", reason); - assert!(reason.contains("Invalid address: bc1qdkwjk42dw6pryvs9sl0ht3pn3mxghuma64jst5")); + assert!(reason.contains("Cannot determine format")); block_on(mm_alice.stop()).unwrap(); } diff --git a/mm2src/mm2_main/src/mm2_tests/bch_and_slp_tests.rs b/mm2src/mm2_main/src/mm2_tests/bch_and_slp_tests.rs index 2c62d78f56..6badc77319 100644 --- a/mm2src/mm2_main/src/mm2_tests/bch_and_slp_tests.rs +++ b/mm2src/mm2_main/src/mm2_tests/bch_and_slp_tests.rs @@ -2,6 +2,13 @@ use super::*; use mm2_test_helpers::for_tests::{enable_bch_with_tokens, enable_slp, my_tx_history_v2, sign_message, verify_message, UtxoRpcMode}; +cfg_wasm32! { + use wasm_bindgen_test::*; + + wasm_bindgen_test_configure!(run_in_browser); +} + +#[cfg(not(target_arch = "wasm32"))] const T_BCH_ELECTRUMS: &[&str] = &[ "electroncash.de:50003", "tbch.loping.net:60001", @@ -10,6 +17,13 @@ const T_BCH_ELECTRUMS: &[&str] = &[ "testnet.imaginary.cash:50001", ]; +#[cfg(target_arch = "wasm32")] +const T_BCH_ELECTRUMS: &[&str] = &[ + "electroncash.de:60003", + "electroncash.de:60004", + "blackie.c3-soft.com:60004", +]; + fn t_bch_electrums_legacy_json() -> Vec { T_BCH_ELECTRUMS.into_iter().map(|url| json!({ "url": url })).collect() } #[test] @@ -374,49 +388,55 @@ async fn wait_till_history_has_records( expected_len: usize, for_coin: &str, paging: Option>, + timeout_s: u64, ) -> StandardHistoryV2Res { + let started_at = now_ms() / 1000; + let wait_until = started_at + timeout_s; loop { let history_json = my_tx_history_v2(mm, for_coin, expected_len, paging.clone()).await; let history: RpcV2Response = json::from_value(history_json).unwrap(); if history.result.transactions.len() >= expected_len { break history.result; } + + let now = now_ms() / 1000; + if wait_until < now { + panic!( + "Waited too long until {} for TX history loads {} transactions", + wait_until, expected_len + ); + } + Timer::sleep(1.).await; } } -#[test] -#[cfg(not(target_arch = "wasm32"))] -fn test_bch_and_slp_testnet_history() { +async fn test_bch_and_slp_testnet_history_impl() { + const PASSPHRASE: &str = "BCH SLP test"; + const TIMEOUT_S: u64 = 45; + let coins = json!([ {"coin":"tBCH","pubtype":0,"p2shtype":5,"mm2":1,"protocol":{"type":"BCH","protocol_data":{"slp_prefix":"slptest"}}, "address_format":{"format":"cashaddress","network":"bchtest"}}, {"coin":"USDF","protocol":{"type":"SLPTOKEN","protocol_data":{"decimals":4,"token_id":"bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7","platform":"tBCH","required_confirmations":1}}} ]); - let mm = MarketMakerIt::start( - json! ({ - "gui": "nogui", - "netid": 9998, - "myipaddr": env::var ("BOB_TRADE_IP") .ok(), - "rpcip": env::var ("BOB_TRADE_IP") .ok(), - "passphrase": "BCH SLP test", - "coins": coins, - "i_am_seed": true, - "rpc_password": "pass", - }), - "pass".into(), - local_start!("bob"), - ) - .unwrap(); - let (_dump_log, _dump_dashboard) = mm.mm_dump(); - log!("log path: {}", mm.log_path.display()); + let conf = Mm2TestConf::seednode(PASSPHRASE, &coins); + let mm = MarketMakerIt::start_async(conf.conf, conf.rpc_password, local_start!("bob")) + .await + .unwrap(); + + #[cfg(not(target_arch = "wasm32"))] + { + let (_dump_log, _dump_dashboard) = mm.mm_dump(); + log!("log path: {}", mm.log_path.display()); + } let rpc_mode = UtxoRpcMode::electrum(T_BCH_ELECTRUMS); let tx_history = true; - let enable_bch = block_on(enable_bch_with_tokens(&mm, "tBCH", &[], rpc_mode, tx_history)); + let enable_bch = enable_bch_with_tokens(&mm, "tBCH", &[], rpc_mode, tx_history).await; log!("enable_bch: {:?}", enable_bch); - let history = block_on(wait_till_history_has_records(&mm, 4, "tBCH", None)); + let history = wait_till_history_has_records(&mm, 4, "tBCH", None, TIMEOUT_S).await; log!("bch history: {:?}", history); let expected_internal_ids = vec![ @@ -434,19 +454,19 @@ fn test_bch_and_slp_testnet_history() { assert_eq!(expected_internal_ids, actual_ids); - let enable_usdf = block_on(enable_slp(&mm, "USDF")); + let enable_usdf = enable_slp(&mm, "USDF").await; log!("enable_usdf: {:?}", enable_usdf); let paging = common::PagingOptionsEnum::FromId("433b641bc89e1b59c22717918583c60ec98421805c8e85b064691705d9aeb970".into()); - let slp_history = block_on(wait_till_history_has_records(&mm, 4, "USDF", Some(paging))); + let slp_history = wait_till_history_has_records(&mm, 4, "USDF", Some(paging), TIMEOUT_S).await; log!("slp history: {:?}", slp_history); let expected_slp_ids = vec![ - "cd6ec10b0cd9747ddc66ac5c97c2d7b493e8cea191bc2d847b3498719d4bd989", + "babe9bd0dc1495dff0920da14a76311b744daadc9d01314f8bd4e2438c6b183b", "1c1e68357cf5a6dacb53881f13aa5d2048fe0d0fab24b76c9ec48f53884bed97", - "c4304b5ef4f1b88ed4939534a8ca9eca79f592939233174ae08002e8454e3f06", + "cd6ec10b0cd9747ddc66ac5c97c2d7b493e8cea191bc2d847b3498719d4bd989", "b0035434a1e7be5af2ed991ee2a21a90b271c5852a684a0b7d315c5a770d1b1c", ]; @@ -466,6 +486,17 @@ fn test_bch_and_slp_testnet_history() { } } +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_bch_and_slp_testnet_history() { block_on(test_bch_and_slp_testnet_history_impl()); } + +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen_test] +async fn test_bch_and_slp_testnet_history() { + common::log::wasm_log::register_wasm_log(); + test_bch_and_slp_testnet_history_impl().await; +} + #[test] #[cfg(not(target_arch = "wasm32"))] fn test_sign_verify_message_bch() { diff --git a/mm2src/mm2_main/src/mm2_tests/structs.rs b/mm2src/mm2_main/src/mm2_tests/structs.rs index 214201489c..fe1a74f54e 100644 --- a/mm2src/mm2_main/src/mm2_tests/structs.rs +++ b/mm2src/mm2_main/src/mm2_tests/structs.rs @@ -577,7 +577,7 @@ pub struct IguanaWalletBalance { pub balance: CoinBalance, } -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub enum Bip44Chain { External = 0, Internal = 1, @@ -607,6 +607,13 @@ pub struct HDAddressBalance { pub balance: CoinBalance, } +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct HDAccountAddressId { + pub account_id: u32, + pub chain: Bip44Chain, + pub address_id: u32, +} + #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields, tag = "wallet_type")] pub enum EnableCoinBalance { @@ -842,6 +849,7 @@ pub type ZcoinHistoryRes = MyTxHistoryV2Response; #[serde(deny_unknown_fields)] pub struct MyTxHistoryV2Response { pub coin: String, + pub target: MyTxHistoryTarget, pub current_block: u64, pub transactions: Vec, pub sync_status: Json, @@ -852,6 +860,16 @@ pub struct MyTxHistoryV2Response { pub paging_options: PagingOptionsEnum, } +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(tag = "type")] +#[serde(rename_all = "snake_case")] +pub enum MyTxHistoryTarget { + Iguana, + AccountId { account_id: u32 }, + AddressId(HDAccountAddressId), + AddressDerivationPath(String), +} + #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] pub struct UtxoFeeDetails { diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index 2372c228d3..5b02543c20 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -1128,9 +1128,24 @@ pub async fn enable_slp(mm: &MarketMakerIt, coin: &str) -> Json { json::from_str(&enable.1).unwrap() } +#[allow(clippy::upper_case_acronyms)] +#[derive(Serialize)] +pub enum ElectrumProtocol { + /// TCP + TCP, + /// SSL/TLS + SSL, + /// Insecure WebSocket. + WS, + /// Secure WebSocket. + WSS, +} + #[derive(Serialize)] pub struct ElectrumRpcRequest { pub url: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub protocol: Option, } #[derive(Serialize)] @@ -1140,10 +1155,25 @@ pub enum UtxoRpcMode { Electrum { servers: Vec }, } +#[cfg(not(target_arch = "wasm32"))] +fn electrum_servers_rpc(servers: &[&str]) -> Vec { + servers + .iter() + .map(|url| ElectrumRpcRequest { + url: url.to_string(), + protocol: None, + }) + .collect() +} + +#[cfg(target_arch = "wasm32")] fn electrum_servers_rpc(servers: &[&str]) -> Vec { servers .iter() - .map(|url| ElectrumRpcRequest { url: url.to_string() }) + .map(|url| ElectrumRpcRequest { + url: url.to_string(), + protocol: Some(ElectrumProtocol::WSS), + }) .collect() }