From cdf032b69c05dd03cf3501a2d8593f26536096a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Tue, 7 May 2024 14:26:48 +0300 Subject: [PATCH 01/20] refactor(memory): memory usage improvements (#2098) This commit improves memory usage a bit by doing preallocation when possible --- mm2src/adex_cli/src/scenarios/mm2_proc_mng.rs | 7 ++++--- mm2src/coins/eth.rs | 13 +++++++++---- mm2src/coins/lightning/ln_p2p.rs | 4 +--- mm2src/coins/lightning/ln_sql.rs | 4 ++-- mm2src/coins/lightning/ln_utils.rs | 2 +- mm2src/coins/nft/storage/wasm/wasm_storage.rs | 4 +++- mm2src/coins/qrc20.rs | 6 ++++-- mm2src/coins/tendermint/tendermint_coin.rs | 2 +- mm2src/coins/utxo/rpc_clients.rs | 2 +- mm2src/coins/utxo/utxo_common.rs | 2 +- mm2src/coins/utxo_signer/src/with_key_pair.rs | 2 +- .../coins/z_coin/storage/walletdb/wasm/storage.rs | 4 ++-- mm2src/coins/z_coin/z_rpc.rs | 2 +- mm2src/coins/z_coin/z_tx_history.rs | 2 +- .../src/eth_with_token_activation.rs | 2 +- mm2src/coins_activation/src/slp_token_activation.rs | 3 +-- mm2src/coins_activation/src/spl_token_activation.rs | 3 +-- .../src/tendermint_token_activation.rs | 3 +-- mm2src/common/shared_ref_counter/src/enable.rs | 3 +-- mm2src/mm2_bitcoin/keys/src/cashaddress.rs | 4 ++-- mm2src/mm2_bitcoin/spv_validation/src/lib.rs | 9 +++------ mm2src/mm2_main/src/lp_ordermatch.rs | 8 ++------ mm2src/mm2_main/src/lp_ordermatch/orderbook_rpc.rs | 6 +++--- mm2src/mm2_main/src/lp_stats.rs | 3 +-- mm2src/mm2_main/src/lp_swap/maker_swap.rs | 2 +- mm2src/mm2_main/src/lp_swap/taker_swap.rs | 2 +- .../src/rpc/lp_commands/lp_commands_legacy.rs | 12 ++++-------- mm2src/mm2_metrics/src/mm_metrics.rs | 2 +- mm2src/trezor/src/transport/udp.rs | 10 ++++++---- 29 files changed, 61 insertions(+), 67 deletions(-) diff --git a/mm2src/adex_cli/src/scenarios/mm2_proc_mng.rs b/mm2src/adex_cli/src/scenarios/mm2_proc_mng.rs index 36a864b5f8..7bc46126ba 100644 --- a/mm2src/adex_cli/src/scenarios/mm2_proc_mng.rs +++ b/mm2src/adex_cli/src/scenarios/mm2_proc_mng.rs @@ -347,10 +347,11 @@ pub(crate) fn get_status() { .filter(|line| line.contains("PID")) .last() { - let pid = found - .trim() + let chars = found.trim(); + + let pid = chars .matches(char::is_numeric) - .fold(String::default(), |mut pid, ch| { + .fold(String::with_capacity(chars.len()), |mut pid, ch| { pid.push_str(ch); pid }); diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 62514973e7..216f5262f9 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -638,8 +638,10 @@ impl EthCoinImpl { /// The id used to differentiate payments on Etomic swap smart contract pub(crate) fn etomic_swap_id(&self, time_lock: u32, secret_hash: &[u8]) -> Vec { - let mut input = vec![]; - input.extend_from_slice(&time_lock.to_le_bytes()); + let timelock_bytes = time_lock.to_le_bytes(); + + let mut input = Vec::with_capacity(timelock_bytes.len() + secret_hash.len()); + input.extend_from_slice(&timelock_bytes); input.extend_from_slice(secret_hash); sha256(&input).to_vec() } @@ -4082,8 +4084,11 @@ impl EthCoin { address: Address, ) -> Result> { let coin = || self; - let mut requests = Vec::new(); - for (token_ticker, info) in self.get_erc_tokens_infos() { + + let tokens = self.get_erc_tokens_infos(); + let mut requests = Vec::with_capacity(tokens.len()); + + for (token_ticker, info) in tokens { let fut = async move { let balance_as_u256 = coin() .get_token_balance_for_address(address, info.token_address) diff --git a/mm2src/coins/lightning/ln_p2p.rs b/mm2src/coins/lightning/ln_p2p.rs index 7db3a0b4e2..64c7c940aa 100644 --- a/mm2src/coins/lightning/ln_p2p.rs +++ b/mm2src/coins/lightning/ln_p2p.rs @@ -110,7 +110,6 @@ fn netaddress_from_ipaddr(addr: IpAddr, port: u16) -> Vec { if addr == Ipv4Addr::new(0, 0, 0, 0) || addr == Ipv4Addr::new(127, 0, 0, 1) { return Vec::new(); } - let mut addresses = Vec::new(); let address = match addr { IpAddr::V4(addr) => NetAddress::IPv4 { addr: u32::from(addr).to_be_bytes(), @@ -121,8 +120,7 @@ fn netaddress_from_ipaddr(addr: IpAddr, port: u16) -> Vec { port, }, }; - addresses.push(address); - addresses + vec![address] } pub async fn ln_node_announcement_loop( diff --git a/mm2src/coins/lightning/ln_sql.rs b/mm2src/coins/lightning/ln_sql.rs index 53eab1ac78..64bbb52ea7 100644 --- a/mm2src/coins/lightning/ln_sql.rs +++ b/mm2src/coins/lightning/ln_sql.rs @@ -1056,7 +1056,7 @@ mod tests { fn generate_random_channels(num: u64) -> Vec { let mut rng = rand::thread_rng(); - let mut channels = vec![]; + let mut channels = Vec::with_capacity(num.try_into().expect("Shouldn't overflow.")); let s = Secp256k1::new(); let mut bytes = [0; 32]; for _i in 0..num { @@ -1108,7 +1108,7 @@ mod tests { fn generate_random_payments(num: u64) -> Vec { let mut rng = rand::thread_rng(); - let mut payments = vec![]; + let mut payments = Vec::with_capacity(num.try_into().expect("Shouldn't overflow.")); let s = Secp256k1::new(); let mut bytes = [0; 32]; for _ in 0..num { diff --git a/mm2src/coins/lightning/ln_utils.rs b/mm2src/coins/lightning/ln_utils.rs index 88af1d68cc..9e12e52bf7 100644 --- a/mm2src/coins/lightning/ln_utils.rs +++ b/mm2src/coins/lightning/ln_utils.rs @@ -162,7 +162,7 @@ pub async fn init_channel_manager( let (channel_manager_blockhash, channel_manager, channelmonitors) = async_blocking(move || { let mut manager_file = File::open(persister.manager_path())?; - let mut channel_monitor_mut_references = Vec::new(); + let mut channel_monitor_mut_references = Vec::with_capacity(channelmonitors.len()); for (_, channel_monitor) in channelmonitors.iter_mut() { channel_monitor_mut_references.push(channel_monitor); } diff --git a/mm2src/coins/nft/storage/wasm/wasm_storage.rs b/mm2src/coins/nft/storage/wasm/wasm_storage.rs index 0578e6e82c..e5ea955918 100644 --- a/mm2src/coins/nft/storage/wasm/wasm_storage.rs +++ b/mm2src/coins/nft/storage/wasm/wasm_storage.rs @@ -44,6 +44,7 @@ where I: Iterator, { let mut filtered_nfts = Vec::new(); + for nft_table in nfts { let nft = nft_details_from_item(nft_table)?; match filters { @@ -82,6 +83,7 @@ where I: Iterator, { let mut filtered_transfers = Vec::new(); + for transfers_table in transfers { let transfer = transfer_details_from_item(transfers_table)?; match filters { @@ -705,7 +707,7 @@ impl NftTransferHistoryStorageOps for NftCacheIDBLocked<'_> { let table = db_transaction.table::().await?; let items = table.get_items("chain", chain.to_string()).await?; - let mut token_addresses = HashSet::new(); + let mut token_addresses = HashSet::with_capacity(items.len()); for (_item_id, item) in items.into_iter() { let transfer = transfer_details_from_item(item)?; token_addresses.insert(transfer.common.token_address); diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index d014e027e5..98b12031d7 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -1503,8 +1503,10 @@ impl MmCoin for Qrc20Coin { } pub fn qrc20_swap_id(time_lock: u32, secret_hash: &[u8]) -> Vec { - let mut input = vec![]; - input.extend_from_slice(&time_lock.to_le_bytes()); + let timelock_bytes = time_lock.to_le_bytes(); + let mut input = Vec::with_capacity(timelock_bytes.len() + secret_hash.len()); + + input.extend_from_slice(&timelock_bytes); input.extend_from_slice(secret_hash); sha256(&input).to_vec() } diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index cee6fc0f75..8885f41ad1 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -458,7 +458,7 @@ impl TendermintCommons for TendermintCoin { let platform_balance = big_decimal_from_sat_unsigned(platform_balance_denom, self.decimals); let ibc_assets_info = self.tokens_info.lock().clone(); - let mut requests = Vec::new(); + let mut requests = Vec::with_capacity(ibc_assets_info.len()); for (denom, info) in ibc_assets_info { let fut = async move { let balance_denom = self diff --git a/mm2src/coins/utxo/rpc_clients.rs b/mm2src/coins/utxo/rpc_clients.rs index 954b88c52f..b67946e40a 100644 --- a/mm2src/coins/utxo/rpc_clients.rs +++ b/mm2src/coins/utxo/rpc_clients.rs @@ -2172,7 +2172,7 @@ impl ElectrumClient { Ok(headers) => headers, Err(e) => return MmError::err(UtxoRpcError::InvalidResponse(format!("{:?}", e))), }; - let mut block_registry: HashMap = HashMap::new(); + let mut block_registry: HashMap = HashMap::with_capacity(block_headers.len()); let mut starting_height = from_height; for block_header in &block_headers { block_registry.insert(starting_height, block_header.clone()); diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index 6e5ee35510..c757e942db 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -2706,7 +2706,7 @@ async fn get_unspents_for_inputs( .map_err(|err| RawTransactionError::InvalidParam(err.to_string()))?; let mut prev_script = None; - let mut unspents_loaded = vec![]; + let mut unspents_loaded = Vec::with_capacity(inputs.len()); for input in inputs { let prev_tx = prev_txns_loaded diff --git a/mm2src/coins/utxo_signer/src/with_key_pair.rs b/mm2src/coins/utxo_signer/src/with_key_pair.rs index ada67e2b6d..151884a05e 100644 --- a/mm2src/coins/utxo_signer/src/with_key_pair.rs +++ b/mm2src/coins/utxo_signer/src/with_key_pair.rs @@ -45,7 +45,7 @@ pub fn sign_tx( signature_version: SignatureVersion, fork_id: u32, ) -> UtxoSignWithKeyPairResult { - let mut signed_inputs = vec![]; + let mut signed_inputs = Vec::with_capacity(unsigned.inputs.len()); match signature_version { SignatureVersion::WitnessV0 => { for (i, _) in unsigned.inputs.iter().enumerate() { diff --git a/mm2src/coins/z_coin/storage/walletdb/wasm/storage.rs b/mm2src/coins/z_coin/storage/walletdb/wasm/storage.rs index bf99dec6ca..9dbaf389a3 100644 --- a/mm2src/coins/z_coin/storage/walletdb/wasm/storage.rs +++ b/mm2src/coins/z_coin/storage/walletdb/wasm/storage.rs @@ -894,7 +894,7 @@ impl WalletRead for WalletIndexedDb { let accounts_table = db_transaction.table::().await?; let maybe_accounts = accounts_table.get_items("ticker", &self.ticker).await?; - let mut res_accounts: HashMap = HashMap::new(); + let mut res_accounts: HashMap = HashMap::with_capacity(maybe_accounts.len()); for (_, account) in maybe_accounts { let extfvk = decode_extended_full_viewing_key(self.params.hrp_sapling_extended_full_viewing_key(), &account.extfvk) @@ -1053,7 +1053,7 @@ impl WalletRead for WalletIndexedDb { // Retrieves a list of transaction IDs (id_tx) from the transactions table // that match the provided account ID and have not been spent (spent IS NULL). - let mut witnesses = vec![]; + let mut witnesses = Vec::with_capacity(maybe_witnesses.len()); for (_, witness) in maybe_witnesses { let id_note = witness.note.to_i64().unwrap(); let id_note = NoteId::ReceivedNoteId(id_note.to_i64().expect("invalid value")); diff --git a/mm2src/coins/z_coin/z_rpc.rs b/mm2src/coins/z_coin/z_rpc.rs index 04b37880e2..bd71d554c6 100644 --- a/mm2src/coins/z_coin/z_rpc.rs +++ b/mm2src/coins/z_coin/z_rpc.rs @@ -397,7 +397,7 @@ impl ZRpcOps for NativeClient { for height in start_block..=last_block { let block = self.get_block_by_height(height).await?; debug!("Got block {:?}", block); - let mut compact_txs = Vec::new(); + let mut compact_txs = Vec::with_capacity(block.tx.len()); // By default, CompactBlocks only contain CompactTxs for transactions that contain Sapling spends or outputs. // Create and push compact_tx during iteration. for (tx_id, hash_tx) in block.tx.iter().enumerate() { diff --git a/mm2src/coins/z_coin/z_tx_history.rs b/mm2src/coins/z_coin/z_tx_history.rs index 26b7f9c8ce..57eb2fdb4c 100644 --- a/mm2src/coins/z_coin/z_tx_history.rs +++ b/mm2src/coins/z_coin/z_tx_history.rs @@ -91,7 +91,7 @@ pub(crate) async fn fetch_tx_history_from_db( .await?; // Process transactions and construct tx_details - let mut tx_details = vec![]; + let mut tx_details = Vec::new(); for (tx_id, tx) in txs { if let Some((_, WalletDbBlocksTable { height, time, .. })) = blocks .iter() diff --git a/mm2src/coins_activation/src/eth_with_token_activation.rs b/mm2src/coins_activation/src/eth_with_token_activation.rs index fb0792704e..296cfcfd73 100644 --- a/mm2src/coins_activation/src/eth_with_token_activation.rs +++ b/mm2src/coins_activation/src/eth_with_token_activation.rs @@ -138,7 +138,7 @@ impl TokenInitializer for Erc20Initializer { &self, activation_params: Vec>, ) -> Result, MmError> { - let mut tokens = vec![]; + let mut tokens = Vec::with_capacity(activation_params.len()); for param in activation_params { let token: EthCoin = self .platform_coin diff --git a/mm2src/coins_activation/src/slp_token_activation.rs b/mm2src/coins_activation/src/slp_token_activation.rs index 85f017b0cc..f9abc166f4 100644 --- a/mm2src/coins_activation/src/slp_token_activation.rs +++ b/mm2src/coins_activation/src/slp_token_activation.rs @@ -100,8 +100,7 @@ impl TokenActivationOps for SlpToken { )?; let balance = token.my_coin_balance().await.mm_err(EnableSlpError::GetBalanceError)?; let my_address = token.my_address()?; - let mut balances = HashMap::new(); - balances.insert(my_address, balance); + let balances = HashMap::from([(my_address, balance)]); let init_result = SlpInitResult { balances, token_id: (*token.token_id()).into(), diff --git a/mm2src/coins_activation/src/spl_token_activation.rs b/mm2src/coins_activation/src/spl_token_activation.rs index 69782e11ac..2bf40b6a89 100644 --- a/mm2src/coins_activation/src/spl_token_activation.rs +++ b/mm2src/coins_activation/src/spl_token_activation.rs @@ -106,8 +106,7 @@ impl TokenActivationOps for SplToken { .await .map_err(|e| SplInitError::GetBalanceError(e.into_inner()))?; let my_address = token.my_address()?; - let mut balances = HashMap::new(); - balances.insert(my_address, balance); + let balances = HashMap::from([(my_address, balance)]); let init_result = SplInitResult { balances, token_contract_address: token.conf.token_contract_address.to_string(), diff --git a/mm2src/coins_activation/src/tendermint_token_activation.rs b/mm2src/coins_activation/src/tendermint_token_activation.rs index 58cc6b90c1..29598969af 100644 --- a/mm2src/coins_activation/src/tendermint_token_activation.rs +++ b/mm2src/coins_activation/src/tendermint_token_activation.rs @@ -65,8 +65,7 @@ impl TokenActivationOps for TendermintToken { .mm_err(|e| TendermintTokenInitError::CouldNotFetchBalance(e.to_string()))?; let my_address = token.my_address()?; - let mut balances = HashMap::new(); - balances.insert(my_address, balance); + let balances = HashMap::from([(my_address, balance)]); let init_result = TendermintTokenInitResult { balances, diff --git a/mm2src/common/shared_ref_counter/src/enable.rs b/mm2src/common/shared_ref_counter/src/enable.rs index 205b36aab7..581a5c1cc6 100644 --- a/mm2src/common/shared_ref_counter/src/enable.rs +++ b/mm2src/common/shared_ref_counter/src/enable.rs @@ -57,8 +57,7 @@ impl SharedRc { #[track_caller] pub fn new(inner: T) -> Self { let index = 0; - let mut existing_pointers = HashMap::new(); - existing_pointers.insert(index, Location::caller()); + let existing_pointers = HashMap::from([(index, Location::caller())]); Self { inner: Arc::new(inner), diff --git a/mm2src/mm2_bitcoin/keys/src/cashaddress.rs b/mm2src/mm2_bitcoin/keys/src/cashaddress.rs index b90772dde2..ea7f43c2d6 100644 --- a/mm2src/mm2_bitcoin/keys/src/cashaddress.rs +++ b/mm2src/mm2_bitcoin/keys/src/cashaddress.rs @@ -364,7 +364,7 @@ mod base32 { /// `encode` converts an input byte array into a base32 string. /// It expects the byte array to be 5-bit packed. pub fn encode(input: &[u8]) -> Result { - let mut output = String::new(); + let mut output = String::with_capacity(input.len()); for i in input { let i = *i as usize; @@ -380,7 +380,7 @@ mod base32 { /// `decode` takes a string in base32 format and returns a byte array that is /// 5-bit packed. pub fn decode(input: &str) -> Result, String> { - let mut output = Vec::new(); + let mut output = Vec::with_capacity(input.len()); for c in input.chars() { let cpos = c as usize; if cpos >= CHARSET_REV.len() { diff --git a/mm2src/mm2_bitcoin/spv_validation/src/lib.rs b/mm2src/mm2_bitcoin/spv_validation/src/lib.rs index 4c57937ba2..319cde9e4c 100644 --- a/mm2src/mm2_bitcoin/spv_validation/src/lib.rs +++ b/mm2src/mm2_bitcoin/spv_validation/src/lib.rs @@ -31,7 +31,7 @@ pub(crate) mod test_utils { use self::serde::Deserialize; - use std::{fs::File, io::Read, panic, string::String, vec, vec::Vec}; + use std::{panic, vec, vec::Vec}; #[derive(Deserialize)] pub(crate) struct TestCase { @@ -40,11 +40,8 @@ pub(crate) mod test_utils { } fn setup() -> serde_json::Value { - let mut file = File::open("./src/for_tests/spvTestVectors.json").unwrap(); - let mut data = String::new(); - file.read_to_string(&mut data).unwrap(); - - serde_json::from_str(&data).unwrap() + let data = include_str!("./for_tests/spvTestVectors.json"); + serde_json::from_str(data).unwrap() } fn to_test_case(val: &serde_json::Value) -> TestCase { diff --git a/mm2src/mm2_main/src/lp_ordermatch.rs b/mm2src/mm2_main/src/lp_ordermatch.rs index dd0ea20097..c2b2d44d0f 100644 --- a/mm2src/mm2_main/src/lp_ordermatch.rs +++ b/mm2src/mm2_main/src/lp_ordermatch.rs @@ -2275,16 +2275,12 @@ fn broadcast_keep_alive_for_pub(ctx: &MmArc, pubkey: &str, orderbook: &Orderbook }; for (alb_pair, root) in state.trie_roots.iter() { - let mut trie_roots = HashMap::new(); - if *root == H64::default() && *root == hashed_null_node::() { continue; } - trie_roots.insert(alb_pair.clone(), *root); - let message = new_protocol::PubkeyKeepAlive { - trie_roots, + trie_roots: HashMap::from([(alb_pair.clone(), *root)]), timestamp: now_sec(), }; @@ -5383,12 +5379,12 @@ pub struct HistoricalOrder { } pub async fn orders_kick_start(ctx: &MmArc) -> Result, String> { - let mut coins = HashSet::new(); let ordermatch_ctx = try_s!(OrdermatchContext::from_ctx(ctx)); let storage = MyOrdersStorage::new(ctx.clone()); let saved_maker_orders = try_s!(storage.load_active_maker_orders().await); let saved_taker_orders = try_s!(storage.load_active_taker_orders().await); + let mut coins = HashSet::with_capacity((saved_maker_orders.len() * 2) + (saved_taker_orders.len() * 2)); { let mut maker_orders_ctx = ordermatch_ctx.maker_orders_ctx.lock(); diff --git a/mm2src/mm2_main/src/lp_ordermatch/orderbook_rpc.rs b/mm2src/mm2_main/src/lp_ordermatch/orderbook_rpc.rs index f50f7f2f3b..d76af5dfba 100644 --- a/mm2src/mm2_main/src/lp_ordermatch/orderbook_rpc.rs +++ b/mm2src/mm2_main/src/lp_ordermatch/orderbook_rpc.rs @@ -95,7 +95,7 @@ pub async fn orderbook_rpc(ctx: MmArc, req: Json) -> Result>, S let mut asks = match orderbook.unordered.get(&(base_ticker.clone(), rel_ticker.clone())) { Some(uuids) => { - let mut orderbook_entries = Vec::new(); + let mut orderbook_entries = Vec::with_capacity(uuids.len()); for uuid in uuids { let ask = orderbook.order_set.get(uuid).ok_or(ERRL!( "Orderbook::unordered contains {:?} uuid that is not in Orderbook::order_set", @@ -114,7 +114,7 @@ pub async fn orderbook_rpc(ctx: MmArc, req: Json) -> Result>, S } orderbook_entries }, - None => Vec::new(), + None => vec![], }; asks.sort_unstable_by(|ask1, ask2| ask1.price_rat.cmp(&ask2.price_rat)); let (mut asks, total_asks_base_vol, total_asks_rel_vol) = build_aggregated_entries(asks); @@ -122,7 +122,7 @@ pub async fn orderbook_rpc(ctx: MmArc, req: Json) -> Result>, S let mut bids = match orderbook.unordered.get(&(rel_ticker, base_ticker)) { Some(uuids) => { - let mut orderbook_entries = vec![]; + let mut orderbook_entries = Vec::with_capacity(uuids.len()); for uuid in uuids { let bid = orderbook.order_set.get(uuid).ok_or(ERRL!( "Orderbook::unordered contains {:?} uuid that is not in Orderbook::order_set", diff --git a/mm2src/mm2_main/src/lp_stats.rs b/mm2src/mm2_main/src/lp_stats.rs index 8a064c1d70..cf87f68c3d 100644 --- a/mm2src/mm2_main/src/lp_stats.rs +++ b/mm2src/mm2_main/src/lp_stats.rs @@ -254,8 +254,7 @@ pub async fn start_version_stat_collection(ctx: MmArc, req: Json) -> NodeVersion let relay_addr = RelayAddress::from_str(&address)?; let multi_address = relay_addr.try_to_multiaddr(network_info)?; - let mut addresses = HashSet::new(); - addresses.insert(multi_address); + let addresses = HashSet::from([multi_address]); add_reserved_peer_addresses(&ctx, peer_id, addresses); } diff --git a/mm2src/mm2_main/src/lp_swap/maker_swap.rs b/mm2src/mm2_main/src/lp_swap/maker_swap.rs index fe0b50bd64..5e51fae9fa 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap.rs @@ -711,7 +711,7 @@ impl MakerSwap { }; drop(send_abort_handle); - let mut swap_events = vec![]; + let mut swap_events = Vec::with_capacity(2); let instructions = match payload.instructions() { Some(instructions) => { let maker_lock_duration = diff --git a/mm2src/mm2_main/src/lp_swap/taker_swap.rs b/mm2src/mm2_main/src/lp_swap/taker_swap.rs index 0e4d178b4d..e1727599e2 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_swap.rs @@ -1338,7 +1338,7 @@ impl TakerSwap { }; drop(abort_send_handle); - let mut swap_events = vec![]; + let mut swap_events = Vec::with_capacity(3); let instructions = match payload.instructions() { Some(instructions) => { match self diff --git a/mm2src/mm2_main/src/rpc/lp_commands/lp_commands_legacy.rs b/mm2src/mm2_main/src/rpc/lp_commands/lp_commands_legacy.rs index 16c65a2c01..f306ef859e 100644 --- a/mm2src/mm2_main/src/rpc/lp_commands/lp_commands_legacy.rs +++ b/mm2src/mm2_main/src/rpc/lp_commands/lp_commands_legacy.rs @@ -33,7 +33,6 @@ use mm2_rpc::data::legacy::{BalanceResponse, CoinInitResponse, Mm2RpcResult, MmV use serde_json::{self as json, Value as Json}; use std::borrow::Cow; use std::collections::HashSet; -use std::iter::Extend; use uuid::Uuid; use crate::mm2::lp_dispatcher::{dispatch_lp_event, StopCtxEvent}; @@ -114,20 +113,17 @@ pub async fn disable_coin(ctx: MmArc, req: Json) -> Result>, St }; // Proceed with disabling the coin/tokens. - let mut cancelled_orders = vec![]; log!("disabling {ticker} coin"); let cancelled_and_matching_orders = cancel_orders_by(&ctx, CancelBy::Coin { ticker: ticker.to_string(), }) .await; - match cancelled_and_matching_orders { - Ok((cancelled, _)) => { - cancelled_orders.extend(cancelled); - }, + let cancelled_orders = match cancelled_and_matching_orders { + Ok((cancelled, _)) => cancelled, Err(err) => { - return disable_coin_err(err, &still_matching_orders, &cancelled_orders, &active_swaps); + return disable_coin_err(err, &still_matching_orders, &[], &active_swaps); }, - } + }; if !coins_ctx.get_dependent_tokens(&ticker).await.is_empty() && !force_disable { coin.update_is_available(false); diff --git a/mm2src/mm2_metrics/src/mm_metrics.rs b/mm2src/mm2_metrics/src/mm_metrics.rs index 758280f420..9c77bc73c8 100644 --- a/mm2src/mm2_metrics/src/mm_metrics.rs +++ b/mm2src/mm2_metrics/src/mm_metrics.rs @@ -243,7 +243,7 @@ impl MmHistogram { /// Create new MmHistogram from `&[f64]`. pub(crate) fn to_json_quantiles(&self) -> HashMap { - let mut result = HashMap::new(); + let mut result = HashMap::with_capacity(3); result.insert("count".to_owned(), self.count as f64); result.insert("min".to_owned(), self.min); result.insert("max".to_owned(), self.max); diff --git a/mm2src/trezor/src/transport/udp.rs b/mm2src/trezor/src/transport/udp.rs index f1cbd1f832..b6e41d2558 100644 --- a/mm2src/trezor/src/transport/udp.rs +++ b/mm2src/trezor/src/transport/udp.rs @@ -51,7 +51,6 @@ impl UdpAvailableDevice { async fn find_devices() -> TrezorResult> { let debug = false; - let mut devices = Vec::new(); let dest = format!( "{}:{}", DEFAULT_HOST, @@ -59,15 +58,18 @@ async fn find_devices() -> TrezorResult> { ); let link = UdpLink::open(&dest).await?; + if link.ping().await? { - devices.push(UdpAvailableDevice { + Ok(vec![UdpAvailableDevice { + // model: Model::TrezorEmulator, debug, transport: UdpTransport { protocol: ProtocolV1 { link }, }, - }); + }]) + } else { + Ok(vec![]) } - Ok(devices) } /// An actual serial HID USB link to a device over which bytes can be sent. From 9a82349f14aa0acbd083f2787c1599ed7c80cbf6 Mon Sep 17 00:00:00 2001 From: dimxy Date: Tue, 7 May 2024 19:38:57 +0500 Subject: [PATCH 02/20] fix(utxo-swap): apply events occurred while taker down (#2114) This commit adds missed apply_event for events discovered while taker was offline, like the maker spent or watcher refunded the taker payment. --- mm2src/mm2_main/src/lp_swap/maker_swap.rs | 1 - mm2src/mm2_main/src/lp_swap/taker_restart.rs | 5 +- mm2src/mm2_main/src/lp_swap/taker_swap.rs | 8 +-- .../tests/docker_tests/swap_watcher_tests.rs | 49 +++++++++++++++++++ 4 files changed, 57 insertions(+), 6 deletions(-) diff --git a/mm2src/mm2_main/src/lp_swap/maker_swap.rs b/mm2src/mm2_main/src/lp_swap/maker_swap.rs index 5e51fae9fa..27b5bedd0e 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap.rs @@ -2125,7 +2125,6 @@ pub async fn run_maker_swap(swap: RunMakerSwapInput, ctx: MmArc) { ) } - #[cfg(target_arch = "wasm32")] if event.is_error() { error!("[swap uuid={uuid_str}] {event:?}"); } diff --git a/mm2src/mm2_main/src/lp_swap/taker_restart.rs b/mm2src/mm2_main/src/lp_swap/taker_restart.rs index 431d1a7c32..9c03e32524 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_restart.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_restart.rs @@ -15,7 +15,7 @@ use super::swap_watcher::{default_watcher_maker_payment_spend_factor, default_wa #[cfg(not(any(test, feature = "run-docker-tests")))] use common::now_sec; -pub async fn get_command_based_on_watcher_activity( +pub async fn get_command_based_on_maker_or_watcher_activity( ctx: &MmArc, swap: &TakerSwap, mut saved: TakerSavedSwap, @@ -136,6 +136,7 @@ pub async fn check_maker_payment_spend_and_add_event( timestamp: now_ms(), event, }; + swap.apply_event(to_save.event.clone()); saved.events.push(to_save); let new_swap = SavedSwap::Taker(saved); try_s!(new_swap.save_to_db(ctx).await); @@ -208,6 +209,7 @@ pub async fn add_taker_payment_spent_event( timestamp: now_ms(), event, }; + swap.apply_event(to_save.event.clone()); saved.events.push(to_save); Ok(()) } @@ -255,6 +257,7 @@ pub async fn add_taker_payment_refunded_by_watcher_event( timestamp: now_ms(), event, }; + swap.apply_event(to_save.event.clone()); saved.events.push(to_save); let new_swap = SavedSwap::Taker(saved); diff --git a/mm2src/mm2_main/src/lp_swap/taker_swap.rs b/mm2src/mm2_main/src/lp_swap/taker_swap.rs index e1727599e2..2114a63691 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_swap.rs @@ -13,7 +13,7 @@ use super::{broadcast_my_swap_status, broadcast_swap_message, broadcast_swap_msg use crate::mm2::lp_network::subscribe_to_topic; use crate::mm2::lp_ordermatch::TakerOrderBuilder; use crate::mm2::lp_swap::swap_v2_common::mark_swap_as_finished; -use crate::mm2::lp_swap::taker_restart::get_command_based_on_watcher_activity; +use crate::mm2::lp_swap::taker_restart::get_command_based_on_maker_or_watcher_activity; use crate::mm2::lp_swap::{broadcast_p2p_tx_msg, broadcast_swap_msg_every_delayed, tx_helper_topic, wait_for_maker_payment_conf_duration, TakerSwapWatcherData, MAX_STARTED_AT_DIFF}; use coins::lp_price::fetch_swap_coins_price; @@ -471,7 +471,6 @@ pub async fn run_taker_swap(swap: RunTakerSwapInput, ctx: MmArc) { ) } - #[cfg(target_arch = "wasm32")] if event.is_error() { error!("[swap uuid={uuid}] {event:?}"); } @@ -784,7 +783,7 @@ impl TakerSwap { #[inline] fn wait_refund_until(&self) -> u64 { self.r().data.taker_payment_lock + 3700 } - fn apply_event(&self, event: TakerSwapEvent) { + pub(crate) fn apply_event(&self, event: TakerSwapEvent) { match event { TakerSwapEvent::Started(data) => { self.w().data = data; @@ -2039,7 +2038,8 @@ impl TakerSwap { && maker_coin.is_supported_by_watchers() && saved.watcher_message_sent() { - command = get_command_based_on_watcher_activity(&ctx, &swap, saved, command).await?; + // discover events occurred while taker was offline, due to maker or watcher activity + command = get_command_based_on_maker_or_watcher_activity(&ctx, &swap, saved, command).await?; } drop_mutability!(command); diff --git a/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs b/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs index 22019abdb1..e3a84a749d 100644 --- a/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs @@ -666,6 +666,55 @@ fn test_taker_completes_swap_after_restart() { block_on(mm_bob.stop()).unwrap(); } +// Verifies https://github.com/KomodoPlatform/komodo-defi-framework/issues/2111 +#[test] +fn test_taker_completes_swap_after_taker_payment_spent_while_offline() { + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let mut mm_bob = run_maker_node(&coins, &[], &[]); + let (mut mm_alice, mut alice_conf) = run_taker_node(&coins, &[], &[&mm_bob.ip.to_string()]); + + let uuids = block_on(start_swaps( + &mut mm_bob, + &mut mm_alice, + &[("MYCOIN1", "MYCOIN")], + 25., + 25., + 2., + )); + + // stop taker after taker payment sent + block_on(mm_alice.wait_for_log(120., |log| log.contains("Taker payment tx"))).unwrap(); + alice_conf.conf["dbdir"] = mm_alice.folder.join("DB").to_str().unwrap().into(); + block_on(mm_alice.stop()).unwrap(); + + // wait for taker payment spent by maker + block_on(mm_bob.wait_for_log(120., |log| log.contains("Taker payment spend tx"))).unwrap(); + // and restart taker + let mut mm_alice = block_on(MarketMakerIt::start_with_envs( + alice_conf.conf, + alice_conf.rpc_password.clone(), + None, + &[], + )) + .unwrap(); + + let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); + log!("Alice log path: {}", mm_alice.log_path.display()); + enable_coin(&mm_alice, "MYCOIN"); + enable_coin(&mm_alice, "MYCOIN1"); + + block_on(wait_for_swaps_finish_and_check_status( + &mut mm_bob, + &mut mm_alice, + &uuids, + 2., + 25., + )); + + block_on(mm_alice.stop()).unwrap(); + block_on(mm_bob.stop()).unwrap(); +} + #[test] fn test_watcher_spends_maker_payment_utxo_utxo() { let alice_privkey = hex::encode(random_secp256k1_secret()); From f4b2403b246a596ecf184a42c39c0aff14af2c68 Mon Sep 17 00:00:00 2001 From: shamardy <39480341+shamardy@users.noreply.github.com> Date: Thu, 9 May 2024 15:59:33 +0300 Subject: [PATCH 03/20] fix(eth): remove my_address from sign_and_send_transaction_with_keypair (#2115) `sign_and_send_transaction_with_keypair` now uses the address provided from the caller instead of having `my_address` inside it. --- mm2src/coins/eth.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 216f5262f9..954814bf4a 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -2355,11 +2355,10 @@ async fn sign_and_send_transaction_with_keypair( data: Vec, gas: U256, ) -> Result { - let my_address = try_tx_s!(coin.derivation_method.single_addr_or_err().await); - let address_lock = coin.get_address_lock(my_address.to_string()).await; + let address_lock = coin.get_address_lock(address.to_string()).await; let _nonce_lock = address_lock.lock().await; let (signed, web3_instances_with_latest_nonce) = - sign_transaction_with_keypair(coin, key_pair, value, action, data, gas, my_address).await?; + sign_transaction_with_keypair(coin, key_pair, value, action, data, gas, address).await?; let bytes = Bytes(rlp::encode(&signed).to_vec()); info!(target: "sign-and-send", "send_raw_transaction…"); From 7f08caea2d490c21f1afa04dade2c9a12503128b Mon Sep 17 00:00:00 2001 From: shamardy <39480341+shamardy@users.noreply.github.com> Date: Thu, 9 May 2024 20:37:47 +0300 Subject: [PATCH 04/20] fix(tests): set txfee for some tbtc tests (#2116) Testnet bitcoin currently has high fees which causes some tests to fail due to not enough balance. Since failing tests don't send actual tbtc, this commit sets a fixed fee to fix these tests. --- mm2src/mm2_main/tests/mm2_tests/best_orders_tests.rs | 4 ++-- mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs | 3 +-- mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs | 8 ++++---- mm2src/mm2_test_helpers/src/for_tests.rs | 6 ++---- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/mm2src/mm2_main/tests/mm2_tests/best_orders_tests.rs b/mm2src/mm2_main/tests/mm2_tests/best_orders_tests.rs index 8daaac6e16..f1149c15f3 100644 --- a/mm2src/mm2_main/tests/mm2_tests/best_orders_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/best_orders_tests.rs @@ -272,12 +272,12 @@ fn test_best_orders_address_and_confirmations() { let bob_coins_config = json!([ {"coin":"RICK","asset":"RICK","rpcport":8923,"txversion":4,"overwintered":1,"required_confirmations":10,"requires_notarization":true,"protocol":{"type":"UTXO"}}, - {"coin":"tBTC","name":"tbitcoin","fname":"tBitcoin","rpcport":18332,"pubtype":111,"p2shtype":196,"wiftype":239,"segwit":true,"bech32_hrp":"tb","txfee":0,"estimate_fee_mode":"ECONOMICAL","mm2":1,"required_confirmations":5,"requires_notarization":false,"protocol":{"type":"UTXO"},"address_format":{"format":"segwit"}} + {"coin":"tBTC","name":"tbitcoin","fname":"tBitcoin","rpcport":18332,"pubtype":111,"p2shtype":196,"wiftype":239,"segwit":true,"bech32_hrp":"tb","txfee":1000,"mm2":1,"required_confirmations":5,"requires_notarization":false,"protocol":{"type":"UTXO"},"address_format":{"format":"segwit"}} ]); let alice_coins_config = json!([ rick_conf(), - {"coin":"tBTC","name":"tbitcoin","fname":"tBitcoin","rpcport":18332,"pubtype":111,"p2shtype":196,"wiftype":239,"segwit":true,"bech32_hrp":"tb","txfee":0,"estimate_fee_mode":"ECONOMICAL","mm2":1,"required_confirmations":0,"protocol":{"type":"UTXO"}} + {"coin":"tBTC","name":"tbitcoin","fname":"tBitcoin","rpcport":18332,"pubtype":111,"p2shtype":196,"wiftype":239,"segwit":true,"bech32_hrp":"tb","txfee":1000,"mm2":1,"required_confirmations":0,"protocol":{"type":"UTXO"}} ]); let mut mm_bob = MarketMakerIt::start( diff --git a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs index 1c360dba49..78833da08f 100644 --- a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs +++ b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs @@ -1292,8 +1292,7 @@ fn test_withdraw_segwit() { "wiftype": 239, "segwit": true, "bech32_hrp": "tb", - "txfee": 0, - "estimate_fee_mode": "ECONOMICAL", + "txfee": 1000, "mm2": 1, "required_confirmations": 0, "protocol": { diff --git a/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs b/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs index 40112ea591..bc3ec5e93d 100644 --- a/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs @@ -235,12 +235,12 @@ fn alice_can_see_the_active_order_after_orderbook_sync_segwit() { let bob_coins_config = json!([ {"coin":"RICK","asset":"RICK","rpcport":8923,"txversion":4,"overwintered":1,"protocol":{"type":"UTXO"}}, - {"coin":"tBTC","name":"tbitcoin","fname":"tBitcoin","rpcport":18332,"pubtype":111,"p2shtype":196,"wiftype":239,"segwit":true,"bech32_hrp":"tb","txfee":0,"estimate_fee_mode":"ECONOMICAL","mm2":1,"required_confirmations":0,"protocol":{"type":"UTXO"},"address_format":{"format":"segwit"}} + {"coin":"tBTC","name":"tbitcoin","fname":"tBitcoin","rpcport":18332,"pubtype":111,"p2shtype":196,"wiftype":239,"segwit":true,"bech32_hrp":"tb","txfee":1000,"mm2":1,"required_confirmations":0,"protocol":{"type":"UTXO"},"address_format":{"format":"segwit"}} ]); let alice_coins_config = json!([ {"coin":"RICK","asset":"RICK","rpcport":8923,"txversion":4,"overwintered":1,"protocol":{"type":"UTXO"}}, - {"coin":"tBTC","name":"tbitcoin","fname":"tBitcoin","rpcport":18332,"pubtype":111,"p2shtype":196,"wiftype":239,"segwit":true,"bech32_hrp":"tb","txfee":0,"estimate_fee_mode":"ECONOMICAL","mm2":1,"required_confirmations":0,"protocol":{"type":"UTXO"},"address_format":{"format":"segwit"}} + {"coin":"tBTC","name":"tbitcoin","fname":"tBitcoin","rpcport":18332,"pubtype":111,"p2shtype":196,"wiftype":239,"segwit":true,"bech32_hrp":"tb","txfee":1000,"mm2":1,"required_confirmations":0,"protocol":{"type":"UTXO"},"address_format":{"format":"segwit"}} ]); let mut mm_bob = MarketMakerIt::start( @@ -408,12 +408,12 @@ fn test_orderbook_segwit() { let bob_coins_config = json!([ {"coin":"RICK","asset":"RICK","rpcport":8923,"txversion":4,"overwintered":1,"protocol":{"type":"UTXO"}}, - {"coin":"tBTC","name":"tbitcoin","fname":"tBitcoin","rpcport":18332,"pubtype":111,"p2shtype":196,"wiftype":239,"segwit":true,"bech32_hrp":"tb","txfee":0,"estimate_fee_mode":"ECONOMICAL","mm2":1,"required_confirmations":0,"protocol":{"type":"UTXO"},"address_format":{"format":"segwit"}} + {"coin":"tBTC","name":"tbitcoin","fname":"tBitcoin","rpcport":18332,"pubtype":111,"p2shtype":196,"wiftype":239,"segwit":true,"bech32_hrp":"tb","txfee":1000,"mm2":1,"required_confirmations":0,"protocol":{"type":"UTXO"},"address_format":{"format":"segwit"}} ]); let alice_coins_config = json!([ {"coin":"RICK","asset":"RICK","rpcport":8923,"txversion":4,"overwintered":1,"protocol":{"type":"UTXO"}}, - {"coin":"tBTC","name":"tbitcoin","fname":"tBitcoin","rpcport":18332,"pubtype":111,"p2shtype":196,"wiftype":239,"segwit":true,"bech32_hrp":"tb","txfee":0,"estimate_fee_mode":"ECONOMICAL","mm2":1,"required_confirmations":0,"protocol":{"type":"UTXO"}} + {"coin":"tBTC","name":"tbitcoin","fname":"tBitcoin","rpcport":18332,"pubtype":111,"p2shtype":196,"wiftype":239,"segwit":true,"bech32_hrp":"tb","txfee":1000,"mm2":1,"required_confirmations":0,"protocol":{"type":"UTXO"}} ]); let mut mm_bob = MarketMakerIt::start( diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index 7c99beb924..3c65716e2e 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -699,8 +699,7 @@ pub fn tbtc_conf() -> Json { "wiftype": 239, "segwit": true, "bech32_hrp": "tb", - "txfee": 0, - "estimate_fee_mode": "ECONOMICAL", + "txfee": 1000, "required_confirmations": 0, "protocol": { "type": "UTXO" @@ -717,8 +716,7 @@ pub fn tbtc_segwit_conf() -> Json { "wiftype": 239, "segwit": true, "bech32_hrp": "tb", - "txfee": 0, - "estimate_fee_mode": "ECONOMICAL", + "txfee": 1000, "required_confirmations": 0, "derivation_path": "m/84'/1'", "address_format": { From 52326c4b48980e676eb304e91ad3b00c78495dbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Tue, 14 May 2024 05:26:23 +0300 Subject: [PATCH 05/20] feat(tendermint): pubkey-only activation and unsigned tx (#2088) This commit implements pubkey-only mode for the Tendermint protocol, which means we can use any external wallet for wallet and swap operations on Tendermint. Additionally, ibc_withdraw RPC is removed and withdraw is refactored for Tendermint to support IBC transfers by automatically finding IBC channels whenever possible. --- Cargo.lock | 7 + mm2src/coins/eth.rs | 14 +- mm2src/coins/eth/eth_tests.rs | 2 + mm2src/coins/eth/eth_withdraw.rs | 6 +- mm2src/coins/hd_wallet/errors.rs | 1 + mm2src/coins/lightning.rs | 2 +- mm2src/coins/lp_coins.rs | 60 +- mm2src/coins/my_tx_history_v2.rs | 10 +- mm2src/coins/qrc20.rs | 22 +- mm2src/coins/qrc20/history.rs | 11 +- mm2src/coins/qrc20/qrc20_tests.rs | 17 +- .../tendermint/ibc_transfer_channels.rs | 51 +- .../rpc_command/tendermint/ibc_withdraw.rs | 29 - mm2src/coins/rpc_command/tendermint/mod.rs | 4 +- mm2src/coins/sia.rs | 4 +- mm2src/coins/solana.rs | 9 +- .../coins/solana/solana_decode_tx_helpers.rs | 5 +- mm2src/coins/solana/solana_tests.rs | 14 +- mm2src/coins/solana/spl.rs | 11 +- mm2src/coins/solana/spl_tests.rs | 10 +- mm2src/coins/tendermint/ibc/transfer_v1.rs | 6 +- mm2src/coins/tendermint/mod.rs | 2 + mm2src/coins/tendermint/tendermint_coin.rs | 1027 ++++++++++------- mm2src/coins/tendermint/tendermint_token.rs | 254 +--- .../tendermint/tendermint_tx_history_v2.rs | 6 +- mm2src/coins/test_coin.rs | 4 +- .../sql_tx_history_storage_v2.rs | 18 +- .../tx_history_storage/tx_history_v2_tests.rs | 12 +- .../wasm/tx_history_storage_v2.rs | 29 +- mm2src/coins/utxo/bch.rs | 2 +- mm2src/coins/utxo/qtum.rs | 2 +- mm2src/coins/utxo/qtum_delegation.rs | 8 +- mm2src/coins/utxo/slp.rs | 9 +- mm2src/coins/utxo/utxo_common.rs | 32 +- mm2src/coins/utxo/utxo_standard.rs | 2 +- mm2src/coins/utxo/utxo_tests.rs | 17 +- mm2src/coins/utxo/utxo_tx_history_v2.rs | 4 +- mm2src/coins/utxo/utxo_withdraw.rs | 7 +- mm2src/coins/z_coin.rs | 9 +- .../src/tendermint_with_assets_activation.rs | 89 +- mm2src/common/common.rs | 3 + mm2src/common/expirable_map.rs | 6 +- mm2src/mm2_core/Cargo.toml | 8 + mm2src/mm2_core/src/data_asker.rs | 188 +++ mm2src/mm2_core/src/lib.rs | 1 + mm2src/mm2_core/src/mm_ctx.rs | 5 + mm2src/mm2_event_stream/src/lib.rs | 1 + mm2src/mm2_main/src/lp_swap/taker_swap.rs | 8 +- .../mm2_main/src/rpc/dispatcher/dispatcher.rs | 5 +- .../tests/docker_tests/docker_tests_inner.rs | 6 +- .../tests/docker_tests/qrc20_tests.rs | 16 +- .../tests/docker_tests/swap_watcher_tests.rs | 21 +- .../tests/mm2_tests/mm2_tests_inner.rs | 15 +- mm2src/mm2_test_helpers/src/for_tests.rs | 4 +- 54 files changed, 1310 insertions(+), 805 deletions(-) delete mode 100644 mm2src/coins/rpc_command/tendermint/ibc_withdraw.rs create mode 100644 mm2src/mm2_core/src/data_asker.rs diff --git a/Cargo.lock b/Cargo.lock index d71fc532c8..94a859920a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4382,6 +4382,7 @@ name = "mm2_core" version = "0.1.0" dependencies = [ "arrayref", + "async-std", "async-trait", "cfg-if 1.0.0", "common", @@ -4390,17 +4391,23 @@ dependencies = [ "futures 0.3.28", "gstuff", "hex", + "instant", "lazy_static", + "mm2_err_handle", "mm2_event_stream", "mm2_metrics", "mm2_rpc", "primitives", "rand 0.7.3", "rustls 0.21.10", + "ser_error", + "ser_error_derive", "serde", "serde_json", "shared_ref_counter", + "tokio", "uuid 1.2.2", + "wasm-bindgen-test", ] [[package]] diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 954814bf4a..9bf78e0f99 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -908,7 +908,7 @@ impl Deref for EthCoin { #[async_trait] impl SwapOps for EthCoin { - fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8]) -> TransactionFut { + fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionFut { let address = try_tx_fus!(addr_from_raw_pubkey(fee_addr)); Box::new( @@ -2929,8 +2929,10 @@ impl EthCoin { coin: self.ticker.clone(), fee_details: fee_details.map(|d| d.into()), block_height: trace.block_number, - tx_hash: format!("{:02x}", BytesJson(raw.hash.as_bytes().to_vec())), - tx_hex: BytesJson(rlp::encode(&raw).to_vec()), + tx: TransactionData::new_signed( + BytesJson(rlp::encode(&raw).to_vec()), + format!("{:02x}", BytesJson(raw.hash.as_bytes().to_vec())), + ), internal_id, timestamp: block.timestamp.into_or_max(), kmd_rewards: None, @@ -3300,8 +3302,10 @@ impl EthCoin { coin: self.ticker.clone(), fee_details: fee_details.map(|d| d.into()), block_height: block_number.as_u64(), - tx_hash: format!("{:02x}", BytesJson(raw.hash.as_bytes().to_vec())), - tx_hex: BytesJson(rlp::encode(&raw).to_vec()), + tx: TransactionData::new_signed( + BytesJson(rlp::encode(&raw).to_vec()), + format!("{:02x}", BytesJson(raw.hash.as_bytes().to_vec())), + ), internal_id: BytesJson(internal_id.to_vec()), timestamp: block.timestamp.into_or_max(), kmd_rewards: None, diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index 961f9a6e54..bf4172d518 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -251,6 +251,7 @@ fn test_withdraw_impl_manual_fee() { gas_price: 1.into(), }), memo: None, + ibc_source_channel: None, }; coin.get_balance().wait().unwrap(); @@ -297,6 +298,7 @@ fn test_withdraw_impl_fee_details() { gas_price: 1.into(), }), memo: None, + ibc_source_channel: None, }; coin.get_balance().wait().unwrap(); diff --git a/mm2src/coins/eth/eth_withdraw.rs b/mm2src/coins/eth/eth_withdraw.rs index d059065985..368d5336a8 100644 --- a/mm2src/coins/eth/eth_withdraw.rs +++ b/mm2src/coins/eth/eth_withdraw.rs @@ -4,7 +4,8 @@ use super::{checksum_address, get_eth_gas_details, u256_to_big_decimal, wei_from use crate::eth::{Action, Address, EthTxFeeDetails, KeyPair, SignedEthTx, UnSignedEthTx}; use crate::hd_wallet::{HDCoinWithdrawOps, HDWalletOps, WithdrawFrom, WithdrawSenderAddress}; use crate::rpc_command::init_withdraw::{WithdrawInProgressStatus, WithdrawTaskHandleShared}; -use crate::{BytesJson, CoinWithDerivationMethod, EthCoin, GetWithdrawSenderAddress, PrivKeyPolicy, TransactionDetails}; +use crate::{BytesJson, CoinWithDerivationMethod, EthCoin, GetWithdrawSenderAddress, PrivKeyPolicy, TransactionData, + TransactionDetails}; use async_trait::async_trait; use bip32::DerivationPath; use common::custom_futures::timeout::FutureTimerExt; @@ -308,8 +309,7 @@ where my_balance_change: &received_by_me - &spent_by_me, spent_by_me, received_by_me, - tx_hex, - tx_hash: tx_hash_str, + tx: TransactionData::new_signed(tx_hex, tx_hash_str), block_height: 0, fee_details: Some(fee_details.into()), coin: coin.ticker.clone(), diff --git a/mm2src/coins/hd_wallet/errors.rs b/mm2src/coins/hd_wallet/errors.rs index 6667fd2021..8b517bc609 100644 --- a/mm2src/coins/hd_wallet/errors.rs +++ b/mm2src/coins/hd_wallet/errors.rs @@ -156,6 +156,7 @@ impl From for AccountUpdatingError { fn from(e: HDWalletStorageError) -> Self { AccountUpdatingError::WalletStorageError(e) } } +#[derive(Display)] pub enum HDWithdrawError { UnexpectedFromAddress(String), UnknownAccount { account_id: u32 }, diff --git a/mm2src/coins/lightning.rs b/mm2src/coins/lightning.rs index f6ccf0363e..a740b009ae 100644 --- a/mm2src/coins/lightning.rs +++ b/mm2src/coins/lightning.rs @@ -610,7 +610,7 @@ impl LightningCoin { #[async_trait] impl SwapOps for LightningCoin { // Todo: This uses dummy data for now for the sake of swap P.O.C., this should be implemented probably after agreeing on how fees will work for lightning - fn send_taker_fee(&self, _fee_addr: &[u8], _dex_fee: DexFee, _uuid: &[u8]) -> TransactionFut { + fn send_taker_fee(&self, _fee_addr: &[u8], _dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionFut { let fut = async move { Ok(TransactionEnum::LightningPayment(PaymentHash([1; 32]))) }; Box::new(fut.boxed().compat()) } diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 9b6c465f7b..6f49f6ab17 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -1040,7 +1040,7 @@ pub enum WatcherRewardError { /// Swap operations (mostly based on the Hash/Time locked transactions implemented by coin wallets). #[async_trait] pub trait SwapOps { - fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, uuid: &[u8]) -> TransactionFut; + fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, uuid: &[u8], expire_at: u64) -> TransactionFut; fn send_maker_payment(&self, maker_payment_args: SendPaymentArgs<'_>) -> TransactionFut; @@ -1950,6 +1950,10 @@ pub trait GetWithdrawSenderAddress { ) -> MmResult, WithdrawError>; } +/// TODO: Avoid using a single request structure on every platform. +/// Instead, accept a generic type from withdraw implementations. +/// This way we won't have to update the payload for every platform when +/// one of them requires specific addition. #[derive(Clone, Deserialize)] pub struct WithdrawRequest { coin: String, @@ -1961,6 +1965,8 @@ pub struct WithdrawRequest { max: bool, fee: Option, memo: Option, + /// Tendermint specific field used for manually providing the IBC channel IDs. + ibc_source_channel: Option, /// Currently, this flag is used by ETH/ERC20 coins activated with MetaMask **only**. #[cfg(target_arch = "wasm32")] #[serde(default)] @@ -2015,6 +2021,7 @@ impl WithdrawRequest { max: true, fee: None, memo: None, + ibc_source_channel: None, #[cfg(target_arch = "wasm32")] broadcast: false, } @@ -2157,15 +2164,14 @@ pub enum TransactionType { token_id: Option, }, NftTransfer, + TendermintIBCTransfer, } /// Transaction details #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct TransactionDetails { - /// Raw bytes of signed transaction, this should be sent as is to `send_raw_transaction_bytes` RPC to broadcast the transaction - pub tx_hex: BytesJson, - /// Transaction hash in hexadecimal format - tx_hash: String, + #[serde(flatten)] + pub tx: TransactionData, /// Coins are sent from these addresses from: Vec, /// Coins are sent to these addresses @@ -2199,6 +2205,40 @@ pub struct TransactionDetails { memo: Option, } +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(untagged)] +pub enum TransactionData { + Signed { + /// Raw bytes of signed transaction, this should be sent as is to `send_raw_transaction_bytes` RPC to broadcast the transaction + tx_hex: BytesJson, + /// Transaction hash in hexadecimal format + tx_hash: String, + }, + /// This can contain entirely different data depending on the platform. + /// TODO: Perhaps using generics would be more suitable here? + Unsigned(Json), +} + +impl TransactionData { + pub fn new_signed(tx_hex: BytesJson, tx_hash: String) -> Self { Self::Signed { tx_hex, tx_hash } } + + pub fn new_unsigned(unsigned_tx_data: Json) -> Self { Self::Unsigned(unsigned_tx_data) } + + pub fn tx_hex(&self) -> Option<&BytesJson> { + match self { + TransactionData::Signed { tx_hex, .. } => Some(tx_hex), + TransactionData::Unsigned(_) => None, + } + } + + pub fn tx_hash(&self) -> Option<&str> { + match self { + TransactionData::Signed { tx_hash, .. } => Some(tx_hash), + TransactionData::Unsigned(_) => None, + } + } +} + #[derive(Clone, Copy, Debug)] pub struct BlockHeightAndTime { height: u64, @@ -2807,6 +2847,13 @@ pub enum WithdrawError { }, #[display(fmt = "Nft Protocol is not supported yet!")] NftProtocolNotSupported, + #[display(fmt = "'chain_registry_name' was not found in coins configuration for '{}'", _0)] + RegistryNameIsMissing(String), + #[display( + fmt = "IBC channel could not found for '{}' address. Consider providing it manually with 'ibc_source_channel' in the request.", + _0 + )] + IBCChannelCouldNotFound(String), } impl HttpStatusCode for WithdrawError { @@ -2832,6 +2879,8 @@ impl HttpStatusCode for WithdrawError { | WithdrawError::ContractTypeDoesntSupportNftWithdrawing(_) | WithdrawError::CoinDoesntSupportNftWithdraw { .. } | WithdrawError::NotEnoughNftsAmount { .. } + | WithdrawError::RegistryNameIsMissing(_) + | WithdrawError::IBCChannelCouldNotFound(_) | WithdrawError::MyAddressNotNftOwner { .. } => StatusCode::BAD_REQUEST, WithdrawError::HwError(_) => StatusCode::GONE, #[cfg(target_arch = "wasm32")] @@ -5381,6 +5430,7 @@ pub mod for_tests { max: false, fee, memo: None, + ibc_source_channel: None, }; let init = init_withdraw(ctx.clone(), withdraw_req).await.unwrap(); let timeout = wait_until_ms(150000); diff --git a/mm2src/coins/my_tx_history_v2.rs b/mm2src/coins/my_tx_history_v2.rs index 517eb1ca47..ea7ed4cf0d 100644 --- a/mm2src/coins/my_tx_history_v2.rs +++ b/mm2src/coins/my_tx_history_v2.rs @@ -5,8 +5,8 @@ use crate::tx_history_storage::{CreateTxHistoryStorageError, FilteringAddresses, use crate::utxo::utxo_common::big_decimal_from_sat_unsigned; use crate::MyAddressError; use crate::{coin_conf, lp_coinfind_or_err, BlockHeightAndTime, CoinFindError, HDPathAccountToAddressId, - HistorySyncState, MmCoin, MmCoinEnum, Transaction, TransactionDetails, TransactionType, TxFeeDetails, - UtxoRpcError}; + HistorySyncState, MmCoin, MmCoinEnum, Transaction, TransactionData, TransactionDetails, TransactionType, + TxFeeDetails, UtxoRpcError}; use async_trait::async_trait; use bitcrypto::sha256; use common::{calc_total_pages, ten, HttpStatusCode, PagingOptionsEnum, StatusCode}; @@ -237,13 +237,13 @@ impl<'a, Addr: Clone + DisplayAddress + Eq + std::hash::Hash, Tx: Transaction> T | TransactionType::RemoveDelegation | TransactionType::FeeForTokenTx | TransactionType::StandardTransfer - | TransactionType::NftTransfer => tx_hash.clone(), + | TransactionType::NftTransfer + | TransactionType::TendermintIBCTransfer => tx_hash.clone(), }; TransactionDetails { coin: self.coin, - tx_hex: self.tx.tx_hex().into(), - tx_hash: tx_hash.to_tx_hash(), + tx: TransactionData::new_signed(self.tx.tx_hex().into(), tx_hash.to_tx_hash()), from, to, total_amount: self.total_amount, diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index 98b12031d7..8c2367d14f 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -23,13 +23,13 @@ use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, Coi RawTransactionFut, RawTransactionRequest, RawTransactionResult, RefundError, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignRawTransactionRequest, SignatureResult, SpendPaymentArgs, SwapOps, TakerSwapMakerCoin, TradeFee, - TradePreimageError, TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionDetails, - TransactionEnum, TransactionErr, TransactionFut, TransactionResult, TransactionType, TxMarshalingErr, - UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, - ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, ValidateWatcherSpendInput, - VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, - WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, - WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest, WithdrawResult}; + TradePreimageError, TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionData, + TransactionDetails, TransactionEnum, TransactionErr, TransactionFut, TransactionResult, TransactionType, + TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, + ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, + ValidateWatcherSpendInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, + WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, + WatcherValidateTakerFeeInput, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest, WithdrawResult}; use async_trait::async_trait; use bitcrypto::{dhash160, sha256}; use chain::TransactionOutput; @@ -763,7 +763,7 @@ impl UtxoCommonOps for Qrc20Coin { #[async_trait] impl SwapOps for Qrc20Coin { - fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8]) -> TransactionFut { + fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionFut { let to_address = try_tx_fus!(self.contract_address_from_raw_pubkey(fee_addr)); let amount = try_tx_fus!(wei_from_big_decimal(&dex_fee.fee_amount().into(), self.utxo.decimals)); let transfer_output = @@ -1609,8 +1609,10 @@ async fn qrc20_withdraw(coin: Qrc20Coin, req: WithdrawRequest) -> WithdrawResult spent_by_me: qrc20_amount, received_by_me, my_balance_change, - tx_hash: signed.hash().reversed().to_vec().to_tx_hash(), - tx_hex: serialize(&signed).into(), + tx: TransactionData::new_signed( + serialize(&signed).into(), + signed.hash().reversed().to_vec().to_tx_hash(), + ), fee_details: Some(fee_details.into()), block_height: 0, coin: conf.ticker.clone(), diff --git a/mm2src/coins/qrc20/history.rs b/mm2src/coins/qrc20/history.rs index d49905b53d..af3c41f078 100644 --- a/mm2src/coins/qrc20/history.rs +++ b/mm2src/coins/qrc20/history.rs @@ -194,7 +194,10 @@ impl Qrc20Coin { let mut input_transactions = HistoryUtxoTxMap::new(); let qtum_details = try_s!(utxo_common::tx_details_by_hash(self, &tx_hash.0, &mut input_transactions).await); // Deserialize the UtxoTx to get a script pubkey - let qtum_tx: UtxoTx = try_s!(deserialize(qtum_details.tx_hex.as_slice()).map_err(|e| ERRL!("{:?}", e))); + let qtum_tx: UtxoTx = try_s!(deserialize( + try_s!(qtum_details.tx.tx_hex().ok_or("unexpected tx type")).as_slice() + ) + .map_err(|e| ERRL!("{:?}", e))); let miner_fee = { let total_qtum_fee = match qtum_details.fee_details { @@ -227,7 +230,11 @@ impl Qrc20Coin { miner_fee: BigDecimal, ) -> Result { let my_address = try_s!(self.utxo.derivation_method.single_addr_or_err().await); - let tx_hash: H256Json = try_s!(H256Json::from_str(&qtum_details.tx_hash)); + let tx_hash: H256Json = try_s!(H256Json::from_str(try_s!(qtum_details + .tx + .tx_hash() + .ok_or("unexpected tx type")))); + if qtum_tx.outputs.len() <= (receipt.output_index as usize) { return ERR!( "Length of the transaction {:?} outputs less than output_index {}", diff --git a/mm2src/coins/qrc20/qrc20_tests.rs b/mm2src/coins/qrc20/qrc20_tests.rs index 9df455a188..7c3a0aba27 100644 --- a/mm2src/coins/qrc20/qrc20_tests.rs +++ b/mm2src/coins/qrc20/qrc20_tests.rs @@ -93,6 +93,7 @@ fn test_withdraw_to_p2sh_address_should_fail() { max: false, fee: None, memo: None, + ibc_source_channel: None, }; let err = coin.withdraw(req).wait().unwrap_err().into_inner(); let expect = WithdrawError::InvalidAddress("QRC20 can be sent to P2PKH addresses only".to_owned()); @@ -133,6 +134,7 @@ fn test_withdraw_impl_fee_details() { gas_price: 40, }), memo: None, + ibc_source_channel: None, }; let tx_details = coin.withdraw(withdraw_req).wait().unwrap(); @@ -594,8 +596,7 @@ fn test_transfer_details_by_hash() { // qKVvtDqpnFGDxsDzck5jmLwdnD2jRH6aM8 is UTXO representation of 1549128bbfb33b997949b4105b6a6371c998e212 contract address let (_id, actual) = it.next().unwrap(); let expected = TransactionDetails { - tx_hex: tx_hex.clone(), - tx_hash: tx_hash_bytes.to_tx_hash(), + tx: TransactionData::new_signed(tx_hex.clone(), tx_hash_bytes.to_tx_hash()), from: vec!["qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG".into()], to: vec!["qKVvtDqpnFGDxsDzck5jmLwdnD2jRH6aM8".into()], total_amount: BigDecimal::from_str("0.003").unwrap(), @@ -619,8 +620,7 @@ fn test_transfer_details_by_hash() { let (_id, actual) = it.next().unwrap(); let expected = TransactionDetails { - tx_hex: tx_hex.clone(), - tx_hash: tx_hash_bytes.to_tx_hash(), + tx: TransactionData::new_signed(tx_hex.clone(), tx_hash_bytes.to_tx_hash()), from: vec!["qKVvtDqpnFGDxsDzck5jmLwdnD2jRH6aM8".into()], to: vec!["qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG".into()], total_amount: BigDecimal::from_str("0.00295").unwrap(), @@ -644,8 +644,7 @@ fn test_transfer_details_by_hash() { let (_id, actual) = it.next().unwrap(); let expected = TransactionDetails { - tx_hex: tx_hex.clone(), - tx_hash: tx_hash_bytes.to_tx_hash(), + tx: TransactionData::new_signed(tx_hex.clone(), tx_hash_bytes.to_tx_hash()), from: vec!["qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG".into()], to: vec!["qKVvtDqpnFGDxsDzck5jmLwdnD2jRH6aM8".into()], total_amount: BigDecimal::from_str("0.003").unwrap(), @@ -669,8 +668,7 @@ fn test_transfer_details_by_hash() { let (_id, actual) = it.next().unwrap(); let expected = TransactionDetails { - tx_hex: tx_hex.clone(), - tx_hash: tx_hash_bytes.to_tx_hash(), + tx: TransactionData::new_signed(tx_hex.clone(), tx_hash_bytes.to_tx_hash()), from: vec!["qKVvtDqpnFGDxsDzck5jmLwdnD2jRH6aM8".into()], to: vec!["qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG".into()], total_amount: BigDecimal::from_str("0.00295").unwrap(), @@ -694,8 +692,7 @@ fn test_transfer_details_by_hash() { let (_id, actual) = it.next().unwrap(); let expected = TransactionDetails { - tx_hex, - tx_hash: tx_hash_bytes.to_tx_hash(), + tx: TransactionData::new_signed(tx_hex, tx_hash_bytes.to_tx_hash()), from: vec!["qKVvtDqpnFGDxsDzck5jmLwdnD2jRH6aM8".into()], to: vec!["qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG".into()], total_amount: BigDecimal::from_str("0.00005000").unwrap(), diff --git a/mm2src/coins/rpc_command/tendermint/ibc_transfer_channels.rs b/mm2src/coins/rpc_command/tendermint/ibc_transfer_channels.rs index fce69042c6..e6e2cb4ea9 100644 --- a/mm2src/coins/rpc_command/tendermint/ibc_transfer_channels.rs +++ b/mm2src/coins/rpc_command/tendermint/ibc_transfer_channels.rs @@ -2,14 +2,14 @@ use common::HttpStatusCode; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::MmError; -use crate::{lp_coinfind_or_err, MmCoinEnum}; +use crate::{coin_conf, tendermint::get_ibc_transfer_channels}; pub type IBCTransferChannelsResult = Result>; #[derive(Clone, Deserialize)] pub struct IBCTransferChannelsRequest { - pub(crate) coin: String, - pub(crate) destination_chain_registry_name: String, + pub(crate) source_coin: String, + pub(crate) destination_coin: String, } #[derive(Clone, Serialize)] @@ -42,10 +42,17 @@ pub enum IBCTransferChannelsRequestError { _0 )] UnsupportedCoin(String), + #[display( + fmt = "'chain_registry_name' was not found in coins configuration for '{}' prefix. Either update the coins configuration or use 'ibc_source_channel' in the request.", + _0 + )] + RegistryNameIsMissing(String), #[display(fmt = "Could not find '{}' registry source.", _0)] RegistrySourceCouldNotFound(String), #[display(fmt = "Transport error: {}", _0)] Transport(String), + #[display(fmt = "Could not found channel for '{}'.", _0)] + CouldNotFindChannel(String), #[display(fmt = "Internal error: {}", _0)] InternalError(String), } @@ -56,7 +63,9 @@ impl HttpStatusCode for IBCTransferChannelsRequestError { IBCTransferChannelsRequestError::UnsupportedCoin(_) | IBCTransferChannelsRequestError::NoSuchCoin(_) => { common::StatusCode::BAD_REQUEST }, - IBCTransferChannelsRequestError::RegistrySourceCouldNotFound(_) => common::StatusCode::NOT_FOUND, + IBCTransferChannelsRequestError::CouldNotFindChannel(_) + | IBCTransferChannelsRequestError::RegistryNameIsMissing(_) + | IBCTransferChannelsRequestError::RegistrySourceCouldNotFound(_) => common::StatusCode::NOT_FOUND, IBCTransferChannelsRequestError::Transport(_) => common::StatusCode::SERVICE_UNAVAILABLE, IBCTransferChannelsRequestError::InternalError(_) => common::StatusCode::INTERNAL_SERVER_ERROR, } @@ -64,13 +73,31 @@ impl HttpStatusCode for IBCTransferChannelsRequestError { } pub async fn ibc_transfer_channels(ctx: MmArc, req: IBCTransferChannelsRequest) -> IBCTransferChannelsResult { - let coin = lp_coinfind_or_err(&ctx, &req.coin) - .await - .map_err(|_| IBCTransferChannelsRequestError::NoSuchCoin(req.coin.clone()))?; + let source_coin_conf = coin_conf(&ctx, &req.source_coin); + let source_registry_name = source_coin_conf + .get("protocol") + .unwrap_or(&serde_json::Value::Null) + .get("protocol_data") + .unwrap_or(&serde_json::Value::Null) + .get("chain_registry_name") + .map(|t| t.as_str().unwrap_or_default().to_owned()); - match coin { - MmCoinEnum::Tendermint(coin) => coin.get_ibc_transfer_channels(req).await, - MmCoinEnum::TendermintToken(token) => token.platform_coin.get_ibc_transfer_channels(req).await, - _ => MmError::err(IBCTransferChannelsRequestError::UnsupportedCoin(req.coin)), - } + let Some(source_registry_name) = source_registry_name else { + return MmError::err(IBCTransferChannelsRequestError::RegistryNameIsMissing(req.source_coin)); + }; + + let destination_coin_conf = coin_conf(&ctx, &req.destination_coin); + let destination_registry_name = destination_coin_conf + .get("protocol") + .unwrap_or(&serde_json::Value::Null) + .get("protocol_data") + .unwrap_or(&serde_json::Value::Null) + .get("chain_registry_name") + .map(|t| t.as_str().unwrap_or_default().to_owned()); + + let Some(destination_registry_name) = destination_registry_name else { + return MmError::err(IBCTransferChannelsRequestError::RegistryNameIsMissing(req.destination_coin)); + }; + + get_ibc_transfer_channels(source_registry_name, destination_registry_name).await } diff --git a/mm2src/coins/rpc_command/tendermint/ibc_withdraw.rs b/mm2src/coins/rpc_command/tendermint/ibc_withdraw.rs deleted file mode 100644 index 037823ee66..0000000000 --- a/mm2src/coins/rpc_command/tendermint/ibc_withdraw.rs +++ /dev/null @@ -1,29 +0,0 @@ -use common::Future01CompatExt; -use mm2_core::mm_ctx::MmArc; -use mm2_err_handle::prelude::MmError; -use mm2_number::BigDecimal; - -use crate::{lp_coinfind_or_err, MmCoinEnum, WithdrawError, WithdrawFee, WithdrawFrom, WithdrawResult}; - -#[derive(Clone, Deserialize)] -pub struct IBCWithdrawRequest { - pub(crate) ibc_source_channel: String, - pub(crate) from: Option, - pub(crate) coin: String, - pub(crate) to: String, - #[serde(default)] - pub(crate) amount: BigDecimal, - #[serde(default)] - pub(crate) max: bool, - pub(crate) memo: Option, - pub(crate) fee: Option, -} - -pub async fn ibc_withdraw(ctx: MmArc, req: IBCWithdrawRequest) -> WithdrawResult { - let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; - match coin { - MmCoinEnum::Tendermint(coin) => coin.ibc_withdraw(req).compat().await, - MmCoinEnum::TendermintToken(token) => token.ibc_withdraw(req).compat().await, - _ => MmError::err(WithdrawError::ActionNotAllowed(req.coin)), - } -} diff --git a/mm2src/coins/rpc_command/tendermint/mod.rs b/mm2src/coins/rpc_command/tendermint/mod.rs index d8211abeac..3e2b664aec 100644 --- a/mm2src/coins/rpc_command/tendermint/mod.rs +++ b/mm2src/coins/rpc_command/tendermint/mod.rs @@ -1,14 +1,12 @@ mod ibc_chains; mod ibc_transfer_channels; -mod ibc_withdraw; pub use ibc_chains::*; pub use ibc_transfer_channels::*; -pub use ibc_withdraw::*; // Global constants for interacting with https://github.com/KomodoPlatform/chain-registry repository // using `mm2_git` crate. pub(crate) const CHAIN_REGISTRY_REPO_OWNER: &str = "KomodoPlatform"; pub(crate) const CHAIN_REGISTRY_REPO_NAME: &str = "chain-registry"; -pub(crate) const CHAIN_REGISTRY_BRANCH: &str = "master"; +pub(crate) const CHAIN_REGISTRY_BRANCH: &str = "nucl"; pub(crate) const CHAIN_REGISTRY_IBC_DIR_NAME: &str = "_IBC"; diff --git a/mm2src/coins/sia.rs b/mm2src/coins/sia.rs index ceb7b78f20..7a9e0bb41b 100644 --- a/mm2src/coins/sia.rs +++ b/mm2src/coins/sia.rs @@ -377,7 +377,9 @@ impl MarketCoinOps for SiaCoin { #[async_trait] impl SwapOps for SiaCoin { - fn send_taker_fee(&self, _fee_addr: &[u8], _dex_fee: DexFee, _uuid: &[u8]) -> TransactionFut { unimplemented!() } + fn send_taker_fee(&self, _fee_addr: &[u8], _dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionFut { + unimplemented!() + } fn send_maker_payment(&self, _maker_payment_args: SendPaymentArgs) -> TransactionFut { unimplemented!() } diff --git a/mm2src/coins/solana.rs b/mm2src/coins/solana.rs index b897503006..f034d44e98 100644 --- a/mm2src/coins/solana.rs +++ b/mm2src/coins/solana.rs @@ -10,7 +10,7 @@ use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinFutSpawner, RawTransactionResult, RefundError, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignRawTransactionRequest, SignatureResult, SpendPaymentArgs, TakerSwapMakerCoin, TradePreimageFut, TradePreimageResult, TradePreimageValue, - TransactionDetails, TransactionFut, TransactionResult, TransactionType, TxMarshalingErr, + TransactionData, TransactionDetails, TransactionFut, TransactionResult, TransactionType, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, ValidateWatcherSpendInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherReward, WatcherRewardError, @@ -274,8 +274,7 @@ async fn withdraw_base_coin_impl(coin: SolanaCoin, req: WithdrawRequest) -> With }; let spent_by_me = &total_amount + &res.sol_required; Ok(TransactionDetails { - tx_hex: serialized_tx.into(), - tx_hash: tx.signatures[0].to_string(), + tx: TransactionData::new_signed(serialized_tx.into(), tx.signatures[0].to_string()), from: vec![coin.my_address.clone()], to: vec![req.to], total_amount: spent_by_me.clone(), @@ -481,7 +480,9 @@ impl MarketCoinOps for SolanaCoin { #[async_trait] impl SwapOps for SolanaCoin { - fn send_taker_fee(&self, _fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8]) -> TransactionFut { unimplemented!() } + fn send_taker_fee(&self, _fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionFut { + unimplemented!() + } fn send_maker_payment(&self, _maker_payment_args: SendPaymentArgs) -> TransactionFut { unimplemented!() } diff --git a/mm2src/coins/solana/solana_decode_tx_helpers.rs b/mm2src/coins/solana/solana_decode_tx_helpers.rs index 2ac0876809..bd22fc044e 100644 --- a/mm2src/coins/solana/solana_decode_tx_helpers.rs +++ b/mm2src/coins/solana/solana_decode_tx_helpers.rs @@ -1,6 +1,6 @@ extern crate serde_derive; -use crate::{NumConversResult, SolanaCoin, SolanaFeeDetails, TransactionDetails, TransactionType}; +use crate::{NumConversResult, SolanaCoin, SolanaFeeDetails, TransactionData, TransactionDetails, TransactionType}; use mm2_number::BigDecimal; use solana_sdk::native_token::lamports_to_sol; use std::convert::TryFrom; @@ -54,8 +54,7 @@ impl SolanaConfirmedTransaction { }; let fee = BigDecimal::try_from(lamports_to_sol(self.meta.fee))?; let tx = TransactionDetails { - tx_hex: Default::default(), - tx_hash: self.transaction.signatures[0].to_string(), + tx: TransactionData::new_signed(Default::default(), self.transaction.signatures[0].to_string()), from: vec![instruction.parsed.info.source.clone()], to: vec![instruction.parsed.info.destination.clone()], total_amount: amount, diff --git a/mm2src/coins/solana/solana_tests.rs b/mm2src/coins/solana/solana_tests.rs index 77a8f7fda4..fb1a7b958c 100644 --- a/mm2src/coins/solana/solana_tests.rs +++ b/mm2src/coins/solana/solana_tests.rs @@ -164,6 +164,7 @@ fn solana_transaction_simulations() { max: false, fee: None, memo: None, + ibc_source_channel: None, }) .compat(), ) @@ -192,6 +193,7 @@ fn solana_transaction_zero_balance() { max: false, fee: None, memo: None, + ibc_source_channel: None, }) .compat(), ); @@ -221,6 +223,7 @@ fn solana_transaction_simulations_not_enough_for_fees() { max: false, fee: None, memo: None, + ibc_source_channel: None, }) .compat(), ); @@ -255,6 +258,7 @@ fn solana_transaction_simulations_max() { max: true, fee: None, memo: None, + ibc_source_channel: None, }) .compat(), ) @@ -284,16 +288,22 @@ fn solana_test_transactions() { max: false, fee: None, memo: None, + ibc_source_channel: None, }) .compat(), ) .unwrap(); log!("{:?}", valid_tx_details); - let tx_str = hex::encode(&*valid_tx_details.tx_hex.0); + let tx_str = hex::encode(&*valid_tx_details.tx.tx_hex().unwrap().0); let res = block_on(sol_coin.send_raw_tx(&tx_str).compat()).unwrap(); - let res2 = block_on(sol_coin.send_raw_tx_bytes(&valid_tx_details.tx_hex.0).compat()).unwrap(); + let res2 = block_on( + sol_coin + .send_raw_tx_bytes(&valid_tx_details.tx.tx_hex().unwrap().0) + .compat(), + ) + .unwrap(); assert_eq!(res, res2); //log!("{:?}", res); diff --git a/mm2src/coins/solana/spl.rs b/mm2src/coins/solana/spl.rs index bfecd9351f..fc76657a78 100644 --- a/mm2src/coins/solana/spl.rs +++ b/mm2src/coins/solana/spl.rs @@ -8,8 +8,8 @@ use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinFutSpawner, ConfirmPayment RawTransactionRequest, RawTransactionResult, RefundError, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignRawTransactionRequest, SignatureResult, SolanaCoin, SpendPaymentArgs, TakerSwapMakerCoin, TradePreimageFut, TradePreimageResult, - TradePreimageValue, TransactionDetails, TransactionFut, TransactionResult, TransactionType, - TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, + TradePreimageValue, TransactionData, TransactionDetails, TransactionFut, TransactionResult, + TransactionType, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, ValidateWatcherSpendInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, @@ -149,8 +149,7 @@ async fn withdraw_spl_token_impl(coin: SplToken, req: WithdrawRequest) -> Withdr 0.into() }; Ok(TransactionDetails { - tx_hex: serialized_tx.into(), - tx_hash: tx.signatures[0].to_string(), + tx: TransactionData::new_signed(serialized_tx.into(), tx.signatures[0].to_string()), from: vec![coin.platform_coin.my_address.clone()], to: vec![req.to], total_amount: res.to_send.clone(), @@ -300,7 +299,9 @@ impl MarketCoinOps for SplToken { #[async_trait] impl SwapOps for SplToken { - fn send_taker_fee(&self, _fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8]) -> TransactionFut { unimplemented!() } + fn send_taker_fee(&self, _fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionFut { + unimplemented!() + } fn send_maker_payment(&self, _maker_payment_args: SendPaymentArgs) -> TransactionFut { unimplemented!() } diff --git a/mm2src/coins/solana/spl_tests.rs b/mm2src/coins/solana/spl_tests.rs index 9b6f985203..10943e6e33 100644 --- a/mm2src/coins/solana/spl_tests.rs +++ b/mm2src/coins/solana/spl_tests.rs @@ -113,6 +113,7 @@ fn test_spl_transactions() { max: false, fee: None, memo: None, + ibc_source_channel: None, }) .compat(), ) @@ -123,10 +124,15 @@ fn test_spl_transactions() { assert_eq!(valid_tx_details.coin, "USDC".to_string()); assert_ne!(valid_tx_details.timestamp, 0); - let tx_str = hex::encode(&*valid_tx_details.tx_hex.0); + let tx_str = hex::encode(&*valid_tx_details.tx.tx_hex().unwrap().0); let res = block_on(usdc_sol_coin.send_raw_tx(&tx_str).compat()).unwrap(); log!("{:?}", res); - let res2 = block_on(usdc_sol_coin.send_raw_tx_bytes(&valid_tx_details.tx_hex.0).compat()).unwrap(); + let res2 = block_on( + usdc_sol_coin + .send_raw_tx_bytes(&valid_tx_details.tx.tx_hex().unwrap().0) + .compat(), + ) + .unwrap(); assert_eq!(res, res2); } diff --git a/mm2src/coins/tendermint/ibc/transfer_v1.rs b/mm2src/coins/tendermint/ibc/transfer_v1.rs index e7bf37697f..c5780e32b7 100644 --- a/mm2src/coins/tendermint/ibc/transfer_v1.rs +++ b/mm2src/coins/tendermint/ibc/transfer_v1.rs @@ -1,6 +1,5 @@ use super::{ibc_proto::IBCTransferV1Proto, IBC_OUT_SOURCE_PORT, IBC_OUT_TIMEOUT_IN_NANOS}; use crate::tendermint::ibc::IBC_TRANSFER_TYPE_URL; -use common::number_type_casting::SafeTypeCastingNumbers; use cosmrs::proto::traits::TypeUrl; use cosmrs::{tx::Msg, AccountId, Coin, ErrorReport}; use std::convert::TryFrom; @@ -34,10 +33,7 @@ impl MsgTransfer { receiver: AccountId, token: Coin, ) -> Self { - let timestamp_as_nanos: u64 = common::get_local_duration_since_epoch() - .expect("get_local_duration_since_epoch shouldn't fail") - .as_nanos() - .into_or_max(); + let timestamp_as_nanos = common::get_utc_timestamp_nanos() as u64; Self { source_port: IBC_OUT_SOURCE_PORT.to_owned(), diff --git a/mm2src/coins/tendermint/mod.rs b/mm2src/coins/tendermint/mod.rs index a1fd9beb57..78009b5db8 100644 --- a/mm2src/coins/tendermint/mod.rs +++ b/mm2src/coins/tendermint/mod.rs @@ -11,6 +11,8 @@ mod tendermint_coin; mod tendermint_token; pub mod tendermint_tx_history_v2; +pub use cosmrs::tendermint::PublicKey as TendermintPublicKey; +pub use cosmrs::AccountId; pub use tendermint_coin::*; pub use tendermint_token::*; diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 8885f41ad1..9ea6bcdaaa 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -3,13 +3,12 @@ use super::htlc::{ClaimHtlcMsg, ClaimHtlcProto, CreateHtlcMsg, CreateHtlcProto, QueryHtlcResponse, TendermintHtlc, HTLC_STATE_COMPLETED, HTLC_STATE_OPEN, HTLC_STATE_REFUNDED}; use super::ibc::transfer_v1::MsgTransfer; use super::ibc::IBC_GAS_LIMIT_DEFAULT; -use super::rpc::*; +use super::{rpc::*, TENDERMINT_COIN_PROTOCOL_TYPE}; use crate::coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentResult}; -use crate::hd_wallet::HDPathAccountToAddressId; +use crate::hd_wallet::{HDPathAccountToAddressId, WithdrawFrom}; use crate::rpc_command::tendermint::{IBCChainRegistriesResponse, IBCChainRegistriesResult, IBCChainsRequestError, - IBCTransferChannel, IBCTransferChannelTag, IBCTransferChannelsRequest, - IBCTransferChannelsRequestError, IBCTransferChannelsResponse, - IBCTransferChannelsResult, IBCWithdrawRequest, CHAIN_REGISTRY_BRANCH, + IBCTransferChannel, IBCTransferChannelTag, IBCTransferChannelsRequestError, + IBCTransferChannelsResponse, IBCTransferChannelsResult, CHAIN_REGISTRY_BRANCH, CHAIN_REGISTRY_IBC_DIR_NAME, CHAIN_REGISTRY_REPO_NAME, CHAIN_REGISTRY_REPO_OWNER}; use crate::tendermint::ibc::IBC_OUT_SOURCE_PORT; use crate::utxo::sat_from_big_decimal; @@ -21,16 +20,17 @@ use crate::{big_decimal_from_sat_unsigned, BalanceError, BalanceFut, BigDecimal, PrivKeyPolicyNotAllowed, RawTransactionError, RawTransactionFut, RawTransactionRequest, RawTransactionRes, RawTransactionResult, RefundError, RefundPaymentArgs, RefundResult, RpcCommonOps, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignRawTransactionRequest, - SignatureError, SignatureResult, SpendPaymentArgs, SwapOps, TakerSwapMakerCoin, TradeFee, - TradePreimageError, TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionDetails, - TransactionEnum, TransactionErr, TransactionFut, TransactionResult, TransactionType, TxFeeDetails, - TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, + SignatureError, SignatureResult, SpendPaymentArgs, SwapOps, TakerSwapMakerCoin, ToBytes, TradeFee, + TradePreimageError, TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionData, + TransactionDetails, TransactionEnum, TransactionErr, TransactionFut, TransactionResult, TransactionType, + TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, ValidateWatcherSpendInput, VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest}; use async_std::prelude::FutureExt as AsyncStdFutureExt; use async_trait::async_trait; +use bip32::DerivationPath; use bitcrypto::{dhash160, sha256}; use common::executor::{abortable_queue::AbortableQueue, AbortableSystem}; use common::executor::{AbortedError, Timer}; @@ -59,8 +59,9 @@ use futures::lock::Mutex as AsyncMutex; use futures::{FutureExt, TryFutureExt}; use futures01::Future; use hex::FromHexError; +use instant::Duration; use itertools::Itertools; -use keys::KeyPair; +use keys::{KeyPair, Public}; use mm2_core::mm_ctx::{MmArc, MmWeak}; use mm2_err_handle::prelude::*; use mm2_git::{FileMetadata, GitController, GithubClient, RepositoryOperations, GITHUB_API_URI}; @@ -71,11 +72,11 @@ use rpc::v1::types::Bytes as BytesJson; use serde_json::{self as json, Value as Json}; use std::collections::HashMap; use std::convert::TryFrom; +use std::io; use std::num::NonZeroU32; use std::ops::Deref; use std::str::FromStr; use std::sync::{Arc, Mutex}; -use std::time::Duration; use uuid::Uuid; // ABCI Request Paths @@ -97,6 +98,7 @@ const ABCI_REQUEST_PROVE: bool = false; const DEFAULT_GAS_PRICE: f64 = 0.25; pub(super) const TIMEOUT_HEIGHT_DELTA: u64 = 100; pub const GAS_LIMIT_DEFAULT: u64 = 125_000; +pub const GAS_WANTED_BASE_VALUE: f64 = 50_000.; pub(crate) const TX_DEFAULT_MEMO: &str = ""; // https://github.com/irisnet/irismod/blob/5016c1be6fdbcffc319943f33713f4a057622f0a/modules/htlc/types/validation.go#L19-L22 @@ -105,7 +107,21 @@ const MIN_TIME_LOCK: i64 = 50; const ACCOUNT_SEQUENCE_ERR: &str = "incorrect account sequence"; -type TendermintPrivKeyPolicy = PrivKeyPolicy; +type TendermintPrivKeyPolicy = PrivKeyPolicy; + +pub struct TendermintKeyPair { + private_key_secret: Secp256k1Secret, + public_key: Public, +} + +impl TendermintKeyPair { + fn new(private_key_secret: Secp256k1Secret, public_key: Public) -> Self { + Self { + private_key_secret, + public_key, + } + } +} #[async_trait] pub trait TendermintCommons { @@ -183,6 +199,91 @@ impl TendermintConf { } } +pub enum TendermintActivationPolicy { + PrivateKey(PrivKeyPolicy), + PublicKey(PublicKey), +} + +impl TendermintActivationPolicy { + pub fn with_private_key_policy(private_key_policy: PrivKeyPolicy) -> Self { + Self::PrivateKey(private_key_policy) + } + + pub fn with_public_key(account_public_key: PublicKey) -> Self { Self::PublicKey(account_public_key) } + + fn generate_account_id(&self, account_prefix: &str) -> Result { + match self { + Self::PrivateKey(priv_key_policy) => { + let pk = priv_key_policy.activated_key().ok_or_else(|| { + ErrorReport::new(io::Error::new(io::ErrorKind::NotFound, "Activated key not found")) + })?; + + Ok( + account_id_from_privkey(pk.private_key_secret.as_slice(), account_prefix) + .map_err(|e| ErrorReport::new(io::Error::new(io::ErrorKind::InvalidData, e.to_string())))?, + ) + }, + + Self::PublicKey(account_public_key) => { + account_id_from_raw_pubkey(account_prefix, &account_public_key.to_bytes()) + }, + } + } + + fn public_key(&self) -> Result { + match self { + Self::PrivateKey(private_key_policy) => match private_key_policy { + PrivKeyPolicy::Iguana(pair) => PublicKey::from_raw_secp256k1(&pair.public_key.to_bytes()) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Couldn't generate public key")), + + PrivKeyPolicy::HDWallet { activated_key, .. } => { + PublicKey::from_raw_secp256k1(&activated_key.public_key.to_bytes()) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Couldn't generate public key")) + }, + + PrivKeyPolicy::Trezor => Err(io::Error::new( + io::ErrorKind::Unsupported, + "Trezor is not supported yet!", + )), + + #[cfg(target_arch = "wasm32")] + PrivKeyPolicy::Metamask(_) => unreachable!(), + }, + Self::PublicKey(account_public_key) => Ok(*account_public_key), + } + } + + pub(crate) fn activated_key_or_err(&self) -> Result<&Secp256k1Secret, MmError> { + match self { + Self::PrivateKey(private_key) => Ok(private_key.activated_key_or_err()?.private_key_secret.as_ref()), + Self::PublicKey(_) => MmError::err(PrivKeyPolicyNotAllowed::UnsupportedMethod( + "`activated_key_or_err` is not supported for pubkey-only activations".to_string(), + )), + } + } + + pub(crate) fn path_to_coin_or_err(&self) -> Result<&HDPathToCoin, MmError> { + match self { + Self::PrivateKey(private_key) => Ok(private_key.path_to_coin_or_err()?), + Self::PublicKey(_) => MmError::err(PrivKeyPolicyNotAllowed::UnsupportedMethod( + "`path_to_coin_or_err` is not supported for pubkey-only activations".to_string(), + )), + } + } + + pub(crate) fn hd_wallet_derived_priv_key_or_err( + &self, + path_to_address: &DerivationPath, + ) -> Result> { + match self { + Self::PrivateKey(pair) => pair.hd_wallet_derived_priv_key_or_err(path_to_address), + Self::PublicKey(_) => MmError::err(PrivKeyPolicyNotAllowed::UnsupportedMethod( + "`hd_wallet_derived_priv_key_or_err` is not supported for pubkey-only activations".to_string(), + )), + } + } +} + struct TendermintRpcClient(AsyncMutex); struct TendermintRpcClientImpl { @@ -225,7 +326,7 @@ pub struct TendermintCoinImpl { /// My address pub account_id: AccountId, pub(super) account_prefix: String, - pub(super) priv_key_policy: TendermintPrivKeyPolicy, + pub(super) activation_policy: TendermintActivationPolicy, pub(crate) decimals: u8, pub(super) denom: Denom, chain_id: ChainId, @@ -236,7 +337,7 @@ pub struct TendermintCoinImpl { pub(super) abortable_system: AbortableQueue, pub(crate) history_sync_state: Mutex, client: TendermintRpcClient, - chain_registry_name: Option, + pub(crate) chain_registry_name: Option, pub(crate) ctx: MmWeak, } @@ -282,6 +383,8 @@ pub enum TendermintInitErrorKind { #[display(fmt = "avg_blocktime must be in-between '0' and '255'.")] AvgBlockTimeInvalid, BalanceStreamInitError(String), + #[display(fmt = "Watcher features can not be used with pubkey-only activation policy.")] + CantUseWatchersWithPubkeyPolicy, } #[derive(Display, Debug, Serialize, SerializeErrorType)] @@ -397,10 +500,14 @@ impl From for AccountIdFromPubkeyHexErr { fn from(err: ErrorReport) -> Self { AccountIdFromPubkeyHexErr::CouldNotCreateAccountId(err) } } -pub fn account_id_from_pubkey_hex(prefix: &str, pubkey: &str) -> MmResult { +pub fn account_id_from_pubkey_hex(prefix: &str, pubkey: &str) -> Result { let pubkey_bytes = hex::decode(pubkey)?; - let pubkey_hash = dhash160(&pubkey_bytes); - Ok(AccountId::new(prefix, pubkey_hash.as_slice())?) + Ok(account_id_from_raw_pubkey(prefix, &pubkey_bytes)?) +} + +pub fn account_id_from_raw_pubkey(prefix: &str, pubkey: &[u8]) -> Result { + let pubkey_hash = dhash160(pubkey); + AccountId::new(prefix, pubkey_hash.as_slice()) } #[derive(Debug, Clone, PartialEq)] @@ -492,7 +599,7 @@ impl TendermintCoin { protocol_info: TendermintProtocolInfo, rpc_urls: Vec, tx_history: bool, - priv_key_policy: TendermintPrivKeyPolicy, + activation_policy: TendermintActivationPolicy, ) -> MmResult { if rpc_urls.is_empty() { return MmError::err(TendermintInitError { @@ -501,17 +608,11 @@ impl TendermintCoin { }); } - let priv_key = priv_key_policy.activated_key_or_err().mm_err(|e| TendermintInitError { - ticker: ticker.clone(), - kind: TendermintInitErrorKind::Internal(e.to_string()), - })?; - - let account_id = - account_id_from_privkey(priv_key.as_slice(), &protocol_info.account_prefix).mm_err(|kind| { - TendermintInitError { - ticker: ticker.clone(), - kind, - } + let account_id = activation_policy + .generate_account_id(&protocol_info.account_prefix) + .map_to_mm(|e| TendermintInitError { + ticker: ticker.clone(), + kind: TendermintInitErrorKind::CouldNotGenerateAccountId(e.to_string()), })?; let rpc_clients = clients_from_urls(rpc_urls.as_ref()).mm_err(|kind| TendermintInitError { @@ -551,7 +652,7 @@ impl TendermintCoin { ticker, account_id, account_prefix: protocol_info.account_prefix, - priv_key_policy, + activation_policy, decimals: protocol_info.decimals, denom, chain_id, @@ -566,247 +667,31 @@ impl TendermintCoin { }))) } - pub fn ibc_withdraw(&self, req: IBCWithdrawRequest) -> WithdrawFut { - let coin = self.clone(); - let fut = async move { - let to_address = - AccountId::from_str(&req.to).map_to_mm(|e| WithdrawError::InvalidAddress(e.to_string()))?; - - let (account_id, priv_key) = match req.from { - Some(from) => { - let path_to_coin = coin.priv_key_policy.path_to_coin_or_err()?; - let path_to_address = from.to_address_path(path_to_coin.coin_type())?; - let priv_key = coin - .priv_key_policy - .hd_wallet_derived_priv_key_or_err(&path_to_address.to_derivation_path(path_to_coin)?)?; - let account_id = account_id_from_privkey(priv_key.as_slice(), &coin.account_prefix) - .map_err(|e| WithdrawError::InternalError(e.to_string()))?; - (account_id, priv_key) - }, - None => (coin.account_id.clone(), *coin.priv_key_policy.activated_key_or_err()?), - }; - - let (balance_denom, balance_dec) = coin - .get_balance_as_unsigned_and_decimal(&account_id, &coin.denom, coin.decimals()) - .await?; - - // << BEGIN TX SIMULATION FOR FEE CALCULATION - let (amount_denom, amount_dec) = if req.max { - let amount_denom = balance_denom; - (amount_denom, big_decimal_from_sat_unsigned(amount_denom, coin.decimals)) - } else { - (sat_from_big_decimal(&req.amount, coin.decimals)?, req.amount.clone()) - }; - - if !coin.is_tx_amount_enough(coin.decimals, &amount_dec) { - return MmError::err(WithdrawError::AmountTooLow { - amount: amount_dec, - threshold: coin.min_tx_amount(), - }); - } - - let received_by_me = if to_address == account_id { - amount_dec - } else { - BigDecimal::default() - }; - - let memo = req.memo.unwrap_or_else(|| TX_DEFAULT_MEMO.into()); - - let msg_transfer = MsgTransfer::new_with_default_timeout( - req.ibc_source_channel.clone(), - account_id.clone(), - to_address.clone(), - Coin { - denom: coin.denom.clone(), - amount: amount_denom.into(), - }, - ) - .to_any() - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - - let current_block = coin - .current_block() - .compat() - .await - .map_to_mm(WithdrawError::Transport)?; - - let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; - // >> END TX SIMULATION FOR FEE CALCULATION - - let (_, gas_limit) = coin.gas_info_for_withdraw(&req.fee, IBC_GAS_LIMIT_DEFAULT); - - let fee_amount_u64 = coin - .calculate_account_fee_amount_as_u64( - &account_id, - &priv_key, - msg_transfer.clone(), - timeout_height, - memo.clone(), - req.fee, - ) - .await?; - let fee_amount_dec = big_decimal_from_sat_unsigned(fee_amount_u64, coin.decimals()); - - let fee_amount = Coin { - denom: coin.denom.clone(), - amount: fee_amount_u64.into(), - }; - - let fee = Fee::from_amount_and_gas(fee_amount, gas_limit); - - let (amount_denom, total_amount) = if req.max { - if balance_denom < fee_amount_u64 { - return MmError::err(WithdrawError::NotSufficientBalance { - coin: coin.ticker.clone(), - available: balance_dec, - required: fee_amount_dec, - }); - } - let amount_denom = balance_denom - fee_amount_u64; - (amount_denom, balance_dec) - } else { - let total = &req.amount + &fee_amount_dec; - if balance_dec < total { - return MmError::err(WithdrawError::NotSufficientBalance { - coin: coin.ticker.clone(), - available: balance_dec, - required: total, - }); - } - - (sat_from_big_decimal(&req.amount, coin.decimals)?, total) - }; - - let msg_transfer = MsgTransfer::new_with_default_timeout( - req.ibc_source_channel.clone(), - account_id.clone(), - to_address.clone(), - Coin { - denom: coin.denom.clone(), - amount: amount_denom.into(), - }, - ) - .to_any() - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - - let account_info = coin.account_info(&account_id).await?; - let tx_raw = coin - .any_to_signed_raw_tx(&priv_key, account_info, msg_transfer, fee, timeout_height, memo.clone()) - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - - let tx_bytes = tx_raw - .to_bytes() - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - - let hash = sha256(&tx_bytes); - Ok(TransactionDetails { - tx_hash: hex::encode_upper(hash.as_slice()), - tx_hex: tx_bytes.into(), - from: vec![account_id.to_string()], - to: vec![req.to], - my_balance_change: &received_by_me - &total_amount, - spent_by_me: total_amount.clone(), - total_amount, - received_by_me, - block_height: 0, - timestamp: 0, - fee_details: Some(TxFeeDetails::Tendermint(TendermintFeeDetails { - coin: coin.ticker.clone(), - amount: fee_amount_dec, - uamount: fee_amount_u64, - gas_limit, - })), - coin: coin.ticker.to_string(), - internal_id: hash.to_vec().into(), - kmd_rewards: None, - transaction_type: TransactionType::default(), - memo: Some(memo), - }) - }; - Box::new(fut.boxed().compat()) - } - - pub async fn get_ibc_transfer_channels(&self, req: IBCTransferChannelsRequest) -> IBCTransferChannelsResult { - #[derive(Deserialize)] - struct ChainRegistry { - channels: Vec, - } - - #[derive(Deserialize)] - struct ChannelInfo { - channel_id: String, - port_id: String, - } - - #[derive(Deserialize)] - struct IbcChannel { - chain_1: ChannelInfo, - #[allow(dead_code)] - chain_2: ChannelInfo, - ordering: String, - version: String, - tags: Option, - } - - let src_chain_registry_name = self.chain_registry_name.as_ref().or_mm_err(|| { - IBCTransferChannelsRequestError::InternalError(format!( - "`chain_registry_name` is not set for '{}'", - self.platform_ticker() - )) - })?; - - let source_filename = format!( - "{}-{}.json", - src_chain_registry_name, req.destination_chain_registry_name - ); - - let git_controller: GitController = GitController::new(GITHUB_API_URI); + /// Extracts corresponding IBC channel ID for `AccountId` from https://github.com/KomodoPlatform/chain-registry/tree/nucl. + pub(crate) async fn detect_channel_id_for_ibc_transfer( + &self, + to_address: &AccountId, + ) -> Result> { + let ctx = MmArc::from_weak(&self.ctx).ok_or_else(|| WithdrawError::InternalError("No context".to_owned()))?; - let metadata_list = git_controller - .client - .get_file_metadata_list( - CHAIN_REGISTRY_REPO_OWNER, - CHAIN_REGISTRY_REPO_NAME, - CHAIN_REGISTRY_BRANCH, - CHAIN_REGISTRY_IBC_DIR_NAME, - ) - .await - .map_err(|e| IBCTransferChannelsRequestError::Transport(format!("{:?}", e)))?; + let source_registry_name = self + .chain_registry_name + .clone() + .ok_or_else(|| WithdrawError::RegistryNameIsMissing(to_address.prefix().to_owned()))?; - let source_channel_file = metadata_list - .iter() - .find(|metadata| metadata.name == source_filename) - .or_mm_err(|| IBCTransferChannelsRequestError::RegistrySourceCouldNotFound(source_filename))?; + let destination_registry_name = chain_registry_name_from_account_prefix(&ctx, to_address.prefix()) + .ok_or_else(|| WithdrawError::RegistryNameIsMissing(to_address.prefix().to_owned()))?; - let mut registry_object = git_controller - .client - .deserialize_json_source::(source_channel_file.to_owned()) + let channels = get_ibc_transfer_channels(source_registry_name, destination_registry_name) .await - .map_err(|e| IBCTransferChannelsRequestError::Transport(format!("{:?}", e)))?; + .map_err(|_| WithdrawError::IBCChannelCouldNotFound(to_address.to_string()))?; - registry_object - .channels - .retain(|ch| ch.chain_1.port_id == *IBC_OUT_SOURCE_PORT); - - let result: Vec = registry_object - .channels - .iter() - .map(|ch| IBCTransferChannel { - channel_id: ch.chain_1.channel_id.clone(), - ordering: ch.ordering.clone(), - version: ch.version.clone(), - tags: ch.tags.clone().map(|t| IBCTransferChannelTag { - status: t.status, - preferred: t.preferred, - dex: t.dex, - }), - }) - .collect(); - - Ok(IBCTransferChannelsResponse { - ibc_transfer_channels: result, - }) + Ok(channels + .ibc_transfer_channels + .last() + .ok_or_else(|| WithdrawError::InternalError("channel list can not be empty".to_owned()))? + .channel_id + .clone()) } #[inline(always)] @@ -896,7 +781,38 @@ impl TendermintCoin { sha256(&htlc_id).to_string().to_uppercase() } - pub(super) async fn seq_safe_send_raw_tx_bytes( + async fn common_send_raw_tx_bytes( + &self, + tx_payload: Any, + fee: Fee, + timeout_height: u64, + memo: String, + timeout: Duration, + ) -> Result<(String, Raw), TransactionErr> { + // As there wouldn't be enough time to process the data, to mitigate potential edge problems (such as attempting to send transaction + // bytes half a second before expiration, which may take longer to send and result in the transaction amount being wasted due to a timeout), + // reduce the expiration time by 5 seconds. + let expiration = timeout - Duration::from_secs(5); + + match self.activation_policy { + TendermintActivationPolicy::PrivateKey(_) => { + try_tx_s!( + self.seq_safe_send_raw_tx_bytes(tx_payload, fee, timeout_height, memo) + .timeout(expiration) + .await + ) + }, + TendermintActivationPolicy::PublicKey(_) => { + try_tx_s!( + self.send_unsigned_tx_externally(tx_payload, fee, timeout_height, memo, expiration) + .timeout(expiration) + .await + ) + }, + } + } + + async fn seq_safe_send_raw_tx_bytes( &self, tx_payload: Any, fee: Fee, @@ -905,7 +821,7 @@ impl TendermintCoin { ) -> Result<(String, Raw), TransactionErr> { let (tx_id, tx_raw) = loop { let tx_raw = try_tx_s!(self.any_to_signed_raw_tx( - try_tx_s!(self.priv_key_policy.activated_key_or_err()), + try_tx_s!(self.activation_policy.activated_key_or_err()), try_tx_s!(self.account_info(&self.account_id).await), tx_payload.clone(), fee.clone(), @@ -929,6 +845,55 @@ impl TendermintCoin { Ok((tx_id, tx_raw)) } + async fn send_unsigned_tx_externally( + &self, + tx_payload: Any, + fee: Fee, + timeout_height: u64, + memo: String, + timeout: Duration, + ) -> Result<(String, Raw), TransactionErr> { + #[derive(Deserialize)] + struct TxHashData { + hash: String, + } + + let ctx = try_tx_s!(MmArc::from_weak(&self.ctx).ok_or(ERRL!("ctx must be initialized already"))); + + let account_info = try_tx_s!(self.account_info(&self.account_id).await); + let sign_doc = try_tx_s!(self.any_to_sign_doc(account_info, tx_payload, fee, timeout_height, memo)); + + let unsigned_tx = json!({ + "sign_doc": { + "body_bytes": sign_doc.body_bytes, + "auth_info_bytes": sign_doc.auth_info_bytes, + "chain_id": sign_doc.chain_id, + "account_number": sign_doc.account_number, + } + }); + + let data: TxHashData = try_tx_s!(ctx + .ask_for_data(&format!("TX_HASH:{}", self.ticker()), unsigned_tx, timeout) + .await + .map_err(|e| ERRL!("{}", e))); + + let tx = try_tx_s!(self.request_tx(data.hash.clone()).await.map_err(|e| ERRL!("{}", e))); + + let tx_raw_inner = TxRaw { + body_bytes: tx.body.as_ref().map(Message::encode_to_vec).unwrap_or_default(), + auth_info_bytes: tx.auth_info.as_ref().map(Message::encode_to_vec).unwrap_or_default(), + signatures: tx.signatures, + }; + + if sign_doc.body_bytes != tx_raw_inner.body_bytes { + return Err(crate::TransactionErr::Plain(ERRL!( + "Unsigned transaction don't match with the externally provided transaction." + ))); + } + + Ok((data.hash, Raw::from(tx_raw_inner))) + } + #[allow(deprecated)] pub(super) async fn calculate_fee( &self, @@ -937,9 +902,22 @@ impl TendermintCoin { memo: String, withdraw_fee: Option, ) -> MmResult { + let Ok(activated_priv_key) = self + .activation_policy + .activated_key_or_err() else { + let (gas_price, gas_limit) = self.gas_info_for_withdraw(&withdraw_fee, GAS_LIMIT_DEFAULT); + let amount = ((GAS_WANTED_BASE_VALUE * 1.5) * gas_price).ceil(); + + let fee_amount = Coin { + denom: self.platform_denom().clone(), + amount: (amount as u64).into(), + }; + + return Ok(Fee::from_amount_and_gas(fee_amount, gas_limit)); + }; + let (response, raw_response) = loop { let account_info = self.account_info(&self.account_id).await?; - let activated_priv_key = self.priv_key_policy.activated_key_or_err()?; let tx_bytes = self .gen_simulated_tx( account_info, @@ -1002,13 +980,19 @@ impl TendermintCoin { #[allow(deprecated)] pub(super) async fn calculate_account_fee_amount_as_u64( &self, - account_id: &AccountId, - priv_key: &Secp256k1Secret, msg: Any, timeout_height: u64, memo: String, withdraw_fee: Option, ) -> MmResult { + let account_id = &self.account_id; + let Ok(priv_key) = self + .activation_policy + .activated_key_or_err() else { + let (gas_price, _) = self.gas_info_for_withdraw(&withdraw_fee, 0); + return Ok(((GAS_WANTED_BASE_VALUE * 1.5) * gas_price).ceil() as u64); + }; + let (response, raw_response) = loop { let account_info = self.account_info(account_id).await?; let tx_bytes = self @@ -1117,6 +1101,82 @@ impl TendermintCoin { .map_to_mm(|e| TendermintCoinRpcError::InvalidResponse(format!("balance is not u64, err {}", e))) } + #[allow(clippy::result_large_err)] + pub(super) fn account_id_and_pk_for_withdraw( + &self, + withdraw_from: Option, + ) -> Result<(AccountId, Option), WithdrawError> { + if let TendermintActivationPolicy::PublicKey(_) = self.activation_policy { + return Ok((self.account_id.clone(), None)); + } + + match withdraw_from { + Some(from) => { + let path_to_coin = self + .activation_policy + .path_to_coin_or_err() + .map_err(|e| WithdrawError::InternalError(e.to_string()))?; + + let path_to_address = from + .to_address_path(path_to_coin.coin_type()) + .map_err(|e| WithdrawError::InternalError(e.to_string()))? + .to_derivation_path(path_to_coin) + .map_err(|e| WithdrawError::InternalError(e.to_string()))?; + + let priv_key = self + .activation_policy + .hd_wallet_derived_priv_key_or_err(&path_to_address) + .map_err(|e| WithdrawError::InternalError(e.to_string()))?; + + let account_id = account_id_from_privkey(priv_key.as_slice(), &self.account_prefix) + .map_err(|e| WithdrawError::InternalError(e.to_string()))?; + Ok((account_id, Some(priv_key))) + }, + None => { + let activated_key = self + .activation_policy + .activated_key_or_err() + .map_err(|e| WithdrawError::InternalError(e.to_string()))?; + + Ok((self.account_id.clone(), Some(*activated_key))) + }, + } + } + + pub(super) fn any_to_transaction_data( + &self, + maybe_pk: Option, + message: Any, + account_info: BaseAccount, + fee: Fee, + timeout_height: u64, + memo: String, + ) -> Result { + if let Some(priv_key) = maybe_pk { + let tx_raw = self.any_to_signed_raw_tx(&priv_key, account_info, message, fee, timeout_height, memo)?; + let tx_bytes = tx_raw.to_bytes()?; + let hash = sha256(&tx_bytes); + + Ok(TransactionData::new_signed( + tx_bytes.into(), + hex::encode_upper(hash.as_slice()), + )) + } else { + let sign_doc = self.any_to_sign_doc(account_info, message, fee, timeout_height, memo)?; + + let tx = json!({ + "sign_doc": { + "body_bytes": sign_doc.body_bytes, + "auth_info_bytes": sign_doc.auth_info_bytes, + "chain_id": sign_doc.chain_id, + "account_number": sign_doc.account_number, + } + }); + + Ok(TransactionData::Unsigned(tx)) + } + } + fn gen_create_htlc_tx( &self, denom: Denom, @@ -1189,6 +1249,20 @@ impl TendermintCoin { sign_doc.sign(&signkey) } + pub(super) fn any_to_sign_doc( + &self, + account_info: BaseAccount, + tx_payload: Any, + fee: Fee, + timeout_height: u64, + memo: String, + ) -> cosmrs::Result { + let tx_body = tx::Body::new(vec![tx_payload], memo, timeout_height as u32); + let pubkey = self.activation_policy.public_key()?.into(); + let auth_info = SignerInfo::single_direct(Some(pubkey), account_info.sequence).auth_info(fee); + SignDoc::new(&tx_body, &auth_info, &self.chain_id, account_info.account_number) + } + pub fn add_activated_token_info(&self, ticker: String, decimals: u8, denom: Denom) { self.tokens_info .lock() @@ -1318,11 +1392,12 @@ impl TendermintCoin { ); let (_tx_id, tx_raw) = try_tx_s!( - coin.seq_safe_send_raw_tx_bytes( + coin.common_send_raw_tx_bytes( create_htlc_tx.msg_payload.clone(), fee.clone(), timeout_height, TX_DEFAULT_MEMO.into(), + Duration::from_secs(time_lock_duration), ) .await ); @@ -1342,6 +1417,7 @@ impl TendermintCoin { denom: Denom, decimals: u8, uuid: &[u8], + expires_at: u64, ) -> TransactionFut { let memo = try_tx_fus!(Uuid::from_slice(uuid)).to_string(); let from_address = self.account_id.clone(); @@ -1370,9 +1446,16 @@ impl TendermintCoin { .await ); + let timeout = expires_at.checked_sub(now_sec()).unwrap_or_default(); let (_tx_id, tx_raw) = try_tx_s!( - coin.seq_safe_send_raw_tx_bytes(tx_payload.clone(), fee.clone(), timeout_height, memo.clone()) - .await + coin.common_send_raw_tx_bytes( + tx_payload.clone(), + fee.clone(), + timeout_height, + memo.clone(), + Duration::from_secs(timeout) + ) + .await ); Ok(TransactionEnum::CosmosTransaction(CosmosTransaction { @@ -1581,7 +1664,7 @@ impl TendermintCoin { drop_mutability!(sec); let to_address = account_id_from_pubkey_hex(&self.account_prefix, DEX_FEE_ADDR_PUBKEY) - .map_err(|e| MmError::new(TradePreimageError::InternalError(e.into_inner().to_string())))?; + .map_err(|e| MmError::new(TradePreimageError::InternalError(e.to_string())))?; let amount = sat_from_big_decimal(&amount, decimals)?; @@ -1605,10 +1688,6 @@ impl TendermintCoin { let fee_uamount = self .calculate_account_fee_amount_as_u64( - &self.account_id, - self.priv_key_policy - .activated_key_or_err() - .mm_err(|e| TradePreimageError::InternalError(e.to_string()))?, create_htlc_tx.msg_payload.clone(), timeout_height, TX_DEFAULT_MEMO.to_owned(), @@ -1633,7 +1712,7 @@ impl TendermintCoin { dex_fee_amount: DexFee, ) -> TradePreimageResult { let to_address = account_id_from_pubkey_hex(&self.account_prefix, DEX_FEE_ADDR_PUBKEY) - .map_err(|e| MmError::new(TradePreimageError::InternalError(e.into_inner().to_string())))?; + .map_err(|e| MmError::new(TradePreimageError::InternalError(e.to_string())))?; let amount = sat_from_big_decimal(&dex_fee_amount.fee_amount().into(), decimals)?; let current_block = self.current_block().compat().await.map_err(|e| { @@ -1657,16 +1736,7 @@ impl TendermintCoin { .map_err(|e| MmError::new(TradePreimageError::InternalError(e.to_string())))?; let fee_uamount = self - .calculate_account_fee_amount_as_u64( - &self.account_id, - self.priv_key_policy - .activated_key_or_err() - .mm_err(|e| TradePreimageError::InternalError(e.to_string()))?, - msg_send, - timeout_height, - TX_DEFAULT_MEMO.to_owned(), - None, - ) + .calculate_account_fee_amount_as_u64(msg_send, timeout_height, TX_DEFAULT_MEMO.to_owned(), None) .await?; let fee_amount = big_decimal_from_sat_unsigned(fee_uamount, decimals); @@ -1948,38 +2018,19 @@ impl MmCoin for TendermintCoin { let fut = async move { let to_address = AccountId::from_str(&req.to).map_to_mm(|e| WithdrawError::InvalidAddress(e.to_string()))?; - if to_address.prefix() != coin.account_prefix { - return MmError::err(WithdrawError::InvalidAddress(format!( - "expected {} address prefix", - coin.account_prefix - ))); - } - let (account_id, priv_key) = match req.from { - Some(from) => { - let path_to_coin = coin.priv_key_policy.path_to_coin_or_err()?; - let path_to_address = from.to_address_path(path_to_coin.coin_type())?; - let priv_key = coin - .priv_key_policy - .hd_wallet_derived_priv_key_or_err(&path_to_address.to_derivation_path(path_to_coin)?)?; - let account_id = account_id_from_privkey(priv_key.as_slice(), &coin.account_prefix) - .map_err(|e| WithdrawError::InternalError(e.to_string()))?; - (account_id, priv_key) - }, - None => (coin.account_id.clone(), *coin.priv_key_policy.activated_key_or_err()?), - }; + let is_ibc_transfer = to_address.prefix() != coin.account_prefix || req.ibc_source_channel.is_some(); + + let (account_id, maybe_pk) = coin.account_id_and_pk_for_withdraw(req.from)?; let (balance_denom, balance_dec) = coin .get_balance_as_unsigned_and_decimal(&account_id, &coin.denom, coin.decimals()) .await?; - // << BEGIN TX SIMULATION FOR FEE CALCULATION let (amount_denom, amount_dec) = if req.max { let amount_denom = balance_denom; (amount_denom, big_decimal_from_sat_unsigned(amount_denom, coin.decimals)) } else { - let total = req.amount.clone(); - (sat_from_big_decimal(&req.amount, coin.decimals)?, req.amount.clone()) }; @@ -1996,18 +2047,32 @@ impl MmCoin for TendermintCoin { BigDecimal::default() }; - let msg_send = MsgSend { - from_address: account_id.clone(), - to_address: to_address.clone(), - amount: vec![Coin { + let msg_payload = if is_ibc_transfer { + let channel_id = match req.ibc_source_channel { + Some(channel_id) => channel_id, + None => coin.detect_channel_id_for_ibc_transfer(&to_address).await?, + }; + + MsgTransfer::new_with_default_timeout(channel_id, account_id.clone(), to_address.clone(), Coin { denom: coin.denom.clone(), amount: amount_denom.into(), - }], + }) + .to_any() + } else { + MsgSend { + from_address: account_id.clone(), + to_address: to_address.clone(), + amount: vec![Coin { + denom: coin.denom.clone(), + amount: amount_denom.into(), + }], + } + .to_any() } - .to_any() .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; let memo = req.memo.unwrap_or_else(|| TX_DEFAULT_MEMO.into()); + let current_block = coin .current_block() .compat() @@ -2015,19 +2080,15 @@ impl MmCoin for TendermintCoin { .map_to_mm(WithdrawError::Transport)?; let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; - // >> END TX SIMULATION FOR FEE CALCULATION - let (_, gas_limit) = coin.gas_info_for_withdraw(&req.fee, GAS_LIMIT_DEFAULT); + let (_, gas_limit) = if is_ibc_transfer { + coin.gas_info_for_withdraw(&req.fee, IBC_GAS_LIMIT_DEFAULT) + } else { + coin.gas_info_for_withdraw(&req.fee, GAS_LIMIT_DEFAULT) + }; let fee_amount_u64 = coin - .calculate_account_fee_amount_as_u64( - &account_id, - &priv_key, - msg_send, - timeout_height, - memo.clone(), - req.fee, - ) + .calculate_account_fee_amount_as_u64(msg_payload.clone(), timeout_height, memo.clone(), req.fee) .await?; let fee_amount_dec = big_decimal_from_sat_unsigned(fee_amount_u64, coin.decimals()); @@ -2061,31 +2122,19 @@ impl MmCoin for TendermintCoin { (sat_from_big_decimal(&req.amount, coin.decimals)?, total) }; - let msg_send = MsgSend { - from_address: account_id.clone(), - to_address, - amount: vec![Coin { - denom: coin.denom.clone(), - amount: amount_denom.into(), - }], - } - .to_any() - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - let account_info = coin.account_info(&account_id).await?; - let tx_raw = coin - .any_to_signed_raw_tx(&priv_key, account_info, msg_send, fee, timeout_height, memo.clone()) - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - let tx_bytes = tx_raw - .to_bytes() + let tx = coin + .any_to_transaction_data(maybe_pk, msg_payload, account_info, fee, timeout_height, memo.clone()) .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - let hash = sha256(&tx_bytes); + let internal_id = { + let hex_vec = tx.tx_hex().cloned().unwrap_or_default().to_vec(); + sha256(&hex_vec).to_vec().into() + }; Ok(TransactionDetails { - tx_hash: hex::encode_upper(hash.as_slice()), - tx_hex: tx_bytes.into(), + tx, from: vec![account_id.to_string()], to: vec![req.to], my_balance_change: &received_by_me - &total_amount, @@ -2101,9 +2150,13 @@ impl MmCoin for TendermintCoin { gas_limit, })), coin: coin.ticker.to_string(), - internal_id: hash.to_vec().into(), + internal_id, kmd_rewards: None, - transaction_type: TransactionType::default(), + transaction_type: if is_ibc_transfer { + TransactionType::TendermintIBCTransfer + } else { + TransactionType::StandardTransfer + }, memo: Some(memo), }) }; @@ -2143,14 +2196,6 @@ impl MmCoin for TendermintCoin { fn validate_address(&self, address: &str) -> ValidateAddressResult { match AccountId::from_str(address) { - Ok(account) if account.prefix() != self.account_prefix => ValidateAddressResult { - is_valid: false, - reason: Some(format!( - "Expected {} account prefix, got {}", - self.account_prefix, - account.prefix() - )), - }, Ok(_) => ValidateAddressResult { is_valid: true, reason: None, @@ -2250,7 +2295,7 @@ impl MarketCoinOps for TendermintCoin { fn my_address(&self) -> MmResult { Ok(self.account_id.to_string()) } async fn get_public_key(&self) -> Result> { - let key = SigningKey::from_slice(self.priv_key_policy.activated_key_or_err()?.as_slice()) + let key = SigningKey::from_slice(self.activation_policy.activated_key_or_err()?.as_slice()) .expect("privkey validity is checked on coin creation"); Ok(key.public_key().to_string()) } @@ -2444,7 +2489,7 @@ impl MarketCoinOps for TendermintCoin { fn display_priv_key(&self) -> Result { Ok(self - .priv_key_policy + .activation_policy .activated_key_or_err() .map_err(|e| e.to_string())? .to_string()) @@ -2456,19 +2501,25 @@ impl MarketCoinOps for TendermintCoin { #[inline] fn min_trading_vol(&self) -> MmNumber { self.min_tx_amount().into() } - fn is_trezor(&self) -> bool { self.priv_key_policy.is_trezor() } + fn is_trezor(&self) -> bool { + match &self.activation_policy { + TendermintActivationPolicy::PrivateKey(pk) => pk.is_trezor(), + TendermintActivationPolicy::PublicKey(_) => false, + } + } } #[async_trait] #[allow(unused_variables)] impl SwapOps for TendermintCoin { - fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, uuid: &[u8]) -> TransactionFut { + fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, uuid: &[u8], expire_at: u64) -> TransactionFut { self.send_taker_fee_for_denom( fee_addr, dex_fee.fee_amount().into(), self.denom.clone(), self.decimals, uuid, + expire_at, ) } @@ -2520,6 +2571,11 @@ impl SwapOps for TendermintCoin { let htlc_id = self.calculate_htlc_id(htlc.sender(), htlc.to(), &amount, maker_spends_payment_args.secret_hash); let claim_htlc_tx = try_tx_s!(self.gen_claim_htlc_tx(htlc_id, maker_spends_payment_args.secret)); + let timeout = maker_spends_payment_args + .time_lock + .checked_sub(now_sec()) + .unwrap_or_default(); + let coin = self.clone(); let current_block = try_tx_s!(self.current_block().compat().await); let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; @@ -2535,11 +2591,12 @@ impl SwapOps for TendermintCoin { ); let (_tx_id, tx_raw) = try_tx_s!( - self.seq_safe_send_raw_tx_bytes( + coin.common_send_raw_tx_bytes( claim_htlc_tx.msg_payload.clone(), fee.clone(), timeout_height, TX_DEFAULT_MEMO.into(), + Duration::from_secs(timeout), ) .await ); @@ -2574,7 +2631,12 @@ impl SwapOps for TendermintCoin { let htlc_id = self.calculate_htlc_id(htlc.sender(), htlc.to(), &amount, taker_spends_payment_args.secret_hash); + let timeout = taker_spends_payment_args + .time_lock + .checked_sub(now_sec()) + .unwrap_or_default(); let claim_htlc_tx = try_tx_s!(self.gen_claim_htlc_tx(htlc_id, taker_spends_payment_args.secret)); + let coin = self.clone(); let current_block = try_tx_s!(self.current_block().compat().await); let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; @@ -2590,11 +2652,12 @@ impl SwapOps for TendermintCoin { ); let (tx_id, tx_raw) = try_tx_s!( - self.seq_safe_send_raw_tx_bytes( + coin.common_send_raw_tx_bytes( claim_htlc_tx.msg_payload.clone(), fee.clone(), timeout_height, TX_DEFAULT_MEMO.into(), + Duration::from_secs(timeout), ) .await ); @@ -2705,9 +2768,9 @@ impl SwapOps for TendermintCoin { } #[inline] - fn derive_htlc_key_pair(&self, swap_unique_data: &[u8]) -> KeyPair { + fn derive_htlc_key_pair(&self, _swap_unique_data: &[u8]) -> KeyPair { key_pair_from_secret( - self.priv_key_policy + self.activation_policy .activated_key_or_err() .expect("valid priv key") .as_ref(), @@ -2716,8 +2779,8 @@ impl SwapOps for TendermintCoin { } #[inline] - fn derive_htlc_pubkey(&self, swap_unique_data: &[u8]) -> Vec { - self.derive_htlc_key_pair(swap_unique_data).public_slice().to_vec() + fn derive_htlc_pubkey(&self, _swap_unique_data: &[u8]) -> Vec { + self.activation_policy.public_key().expect("valid pubkey").to_bytes() } fn validate_other_pubkey(&self, raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { @@ -2854,7 +2917,16 @@ pub fn tendermint_priv_key_policy( path_to_address: HDPathAccountToAddressId, ) -> MmResult { match priv_key_build_policy { - PrivKeyBuildPolicy::IguanaPrivKey(iguana) => Ok(TendermintPrivKeyPolicy::Iguana(iguana)), + PrivKeyBuildPolicy::IguanaPrivKey(iguana) => { + let mm2_internal_key_pair = key_pair_from_secret(iguana.as_ref()).mm_err(|e| TendermintInitError { + ticker: ticker.to_string(), + kind: TendermintInitErrorKind::Internal(e.to_string()), + })?; + + let tendermint_pair = TendermintKeyPair::new(iguana, *mm2_internal_key_pair.public()); + + Ok(TendermintPrivKeyPolicy::Iguana(tendermint_pair)) + }, PrivKeyBuildPolicy::GlobalHDAccount(global_hd) => { let path_to_coin = conf.derivation_path.as_ref().or_mm_err(|| TendermintInitError { ticker: ticker.to_string(), @@ -2872,9 +2944,18 @@ pub fn tendermint_priv_key_policy( kind: TendermintInitErrorKind::InvalidPrivKey(e.to_string()), })?; let bip39_secp_priv_key = global_hd.root_priv_key().clone(); + let pubkey = Public::from_slice(&bip39_secp_priv_key.public_key().to_bytes()).map_to_mm(|e| { + TendermintInitError { + ticker: ticker.to_string(), + kind: TendermintInitErrorKind::Internal(e.to_string()), + } + })?; + + let tendermint_pair = TendermintKeyPair::new(activated_priv_key, pubkey); + Ok(TendermintPrivKeyPolicy::HDWallet { path_to_coin: path_to_coin.clone(), - activated_key: activated_priv_key, + activated_key: tendermint_pair, bip39_secp_priv_key, }) }, @@ -2889,6 +2970,125 @@ pub fn tendermint_priv_key_policy( } } +pub(crate) fn chain_registry_name_from_account_prefix(ctx: &MmArc, prefix: &str) -> Option { + let Some(coins) = ctx.conf["coins"].as_array() else { + return None; + }; + + for coin in coins { + let protocol = coin + .get("protocol") + .unwrap_or(&serde_json::Value::Null) + .get("type") + .unwrap_or(&serde_json::Value::Null) + .as_str(); + + if protocol != Some(TENDERMINT_COIN_PROTOCOL_TYPE) { + continue; + } + + let coin_account_prefix = coin + .get("protocol") + .unwrap_or(&serde_json::Value::Null) + .get("protocol_data") + .unwrap_or(&serde_json::Value::Null) + .get("account_prefix") + .map(|t| t.as_str().unwrap_or_default()); + + if coin_account_prefix == Some(prefix) { + return coin + .get("protocol") + .unwrap_or(&serde_json::Value::Null) + .get("protocol_data") + .unwrap_or(&serde_json::Value::Null) + .get("chain_registry_name") + .map(|t| t.as_str().unwrap_or_default().to_owned()); + } + } + + None +} + +pub async fn get_ibc_transfer_channels( + source_registry_name: String, + destination_registry_name: String, +) -> IBCTransferChannelsResult { + #[derive(Deserialize)] + struct ChainRegistry { + channels: Vec, + } + + #[derive(Deserialize)] + struct ChannelInfo { + channel_id: String, + port_id: String, + } + + #[derive(Deserialize)] + struct IbcChannel { + #[allow(dead_code)] + chain_1: ChannelInfo, + chain_2: ChannelInfo, + ordering: String, + version: String, + tags: Option, + } + + let source_filename = format!("{}-{}.json", source_registry_name, destination_registry_name); + let git_controller: GitController = GitController::new(GITHUB_API_URI); + + let metadata_list = git_controller + .client + .get_file_metadata_list( + CHAIN_REGISTRY_REPO_OWNER, + CHAIN_REGISTRY_REPO_NAME, + CHAIN_REGISTRY_BRANCH, + CHAIN_REGISTRY_IBC_DIR_NAME, + ) + .await + .map_err(|e| IBCTransferChannelsRequestError::Transport(format!("{:?}", e)))?; + + let source_channel_file = metadata_list + .iter() + .find(|metadata| metadata.name == source_filename) + .or_mm_err(|| IBCTransferChannelsRequestError::RegistrySourceCouldNotFound(source_filename))?; + + let mut registry_object = git_controller + .client + .deserialize_json_source::(source_channel_file.to_owned()) + .await + .map_err(|e| IBCTransferChannelsRequestError::Transport(format!("{:?}", e)))?; + + registry_object + .channels + .retain(|ch| ch.chain_2.port_id == *IBC_OUT_SOURCE_PORT); + + let result: Vec = registry_object + .channels + .iter() + .map(|ch| IBCTransferChannel { + channel_id: ch.chain_2.channel_id.clone(), + ordering: ch.ordering.clone(), + version: ch.version.clone(), + tags: ch.tags.clone().map(|t| IBCTransferChannelTag { + status: t.status, + preferred: t.preferred, + dex: t.dex, + }), + }) + .collect(); + + if result.is_empty() { + return MmError::err(IBCTransferChannelsRequestError::CouldNotFindChannel( + destination_registry_name, + )); + } + + Ok(IBCTransferChannelsResponse { + ibc_transfer_channels: result, + }) +} + #[cfg(test)] pub mod tendermint_coin_tests { use super::*; @@ -2981,7 +3181,9 @@ pub mod tendermint_coin_tests { }; let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); - let priv_key_policy = TendermintPrivKeyPolicy::Iguana(key_pair.private().secret); + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); let coin = block_on(TendermintCoin::init( &ctx, @@ -2990,7 +3192,7 @@ pub mod tendermint_coin_tests { protocol_conf, rpc_urls, false, - priv_key_policy, + activation_policy, )) .unwrap(); @@ -3030,11 +3232,12 @@ pub mod tendermint_coin_tests { .unwrap() }); - let send_tx_fut = coin.seq_safe_send_raw_tx_bytes( + let send_tx_fut = coin.common_send_raw_tx_bytes( create_htlc_tx.msg_payload.clone(), fee, timeout_height, TX_DEFAULT_MEMO.into(), + Duration::from_secs(10), ); block_on(async { send_tx_fut.await.unwrap(); @@ -3075,8 +3278,13 @@ pub mod tendermint_coin_tests { .unwrap() }); - let send_tx_fut = - coin.seq_safe_send_raw_tx_bytes(claim_htlc_tx.msg_payload, fee, timeout_height, TX_DEFAULT_MEMO.into()); + let send_tx_fut = coin.common_send_raw_tx_bytes( + claim_htlc_tx.msg_payload, + fee, + timeout_height, + TX_DEFAULT_MEMO.into(), + Duration::from_secs(30), + ); let (tx_id, _tx_raw) = block_on(async { send_tx_fut.await.unwrap() }); @@ -3098,7 +3306,9 @@ pub mod tendermint_coin_tests { }; let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); - let priv_key_policy = TendermintPrivKeyPolicy::Iguana(key_pair.private().secret); + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); let coin = block_on(TendermintCoin::init( &ctx, @@ -3107,7 +3317,7 @@ pub mod tendermint_coin_tests { protocol_conf, rpc_urls, false, - priv_key_policy, + activation_policy, )) .unwrap(); @@ -3158,7 +3368,9 @@ pub mod tendermint_coin_tests { }; let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); - let priv_key_policy = TendermintPrivKeyPolicy::Iguana(key_pair.private().secret); + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); let coin = block_on(TendermintCoin::init( &ctx, @@ -3167,7 +3379,7 @@ pub mod tendermint_coin_tests { protocol_conf, rpc_urls, false, - priv_key_policy, + activation_policy, )) .unwrap(); @@ -3229,7 +3441,9 @@ pub mod tendermint_coin_tests { }; let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); - let priv_key_policy = TendermintPrivKeyPolicy::Iguana(key_pair.private().secret); + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); let coin = block_on(TendermintCoin::init( &ctx, @@ -3238,7 +3452,7 @@ pub mod tendermint_coin_tests { protocol_conf, rpc_urls, false, - priv_key_policy, + activation_policy, )) .unwrap(); @@ -3423,7 +3637,9 @@ pub mod tendermint_coin_tests { }; let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); - let priv_key_policy = TendermintPrivKeyPolicy::Iguana(key_pair.private().secret); + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); let coin = block_on(TendermintCoin::init( &ctx, @@ -3432,7 +3648,7 @@ pub mod tendermint_coin_tests { protocol_conf, rpc_urls, false, - priv_key_policy, + activation_policy, )) .unwrap(); @@ -3503,7 +3719,9 @@ pub mod tendermint_coin_tests { }; let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); - let priv_key_policy = TendermintPrivKeyPolicy::Iguana(key_pair.private().secret); + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); let coin = block_on(TendermintCoin::init( &ctx, @@ -3512,7 +3730,7 @@ pub mod tendermint_coin_tests { protocol_conf, rpc_urls, false, - priv_key_policy, + activation_policy, )) .unwrap(); @@ -3576,7 +3794,9 @@ pub mod tendermint_coin_tests { }; let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); - let priv_key_policy = TendermintPrivKeyPolicy::Iguana(key_pair.private().secret); + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); let coin = block_on(TendermintCoin::init( &ctx, @@ -3585,7 +3805,7 @@ pub mod tendermint_coin_tests { protocol_conf, rpc_urls, false, - priv_key_policy, + activation_policy, )) .unwrap(); @@ -3645,7 +3865,9 @@ pub mod tendermint_coin_tests { let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc(); let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); - let priv_key_policy = TendermintPrivKeyPolicy::Iguana(key_pair.private().secret); + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); let coin = common::block_on(TendermintCoin::init( &ctx, @@ -3654,7 +3876,7 @@ pub mod tendermint_coin_tests { protocol_conf, rpc_urls, false, - priv_key_policy, + activation_policy, )) .unwrap(); @@ -3697,7 +3919,9 @@ pub mod tendermint_coin_tests { let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc(); let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); - let priv_key_policy = TendermintPrivKeyPolicy::Iguana(key_pair.private().secret); + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); let coin = common::block_on(TendermintCoin::init( &ctx, @@ -3706,7 +3930,7 @@ pub mod tendermint_coin_tests { protocol_conf, rpc_urls, false, - priv_key_policy, + activation_policy, )) .unwrap(); @@ -3742,4 +3966,27 @@ pub mod tendermint_coin_tests { block_on(coin.wait_for_confirmations(confirm_payment_input).compat()).unwrap_err(); } } + + #[test] + fn test_generate_account_id() { + let key_pair = key_pair_from_seed("best seed").unwrap(); + + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let pb = PublicKey::from_raw_secp256k1(&key_pair.public().to_bytes()).unwrap(); + + let pk_activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); + // Derive account id from the private key. + let pk_account_id = pk_activation_policy.generate_account_id("cosmos").unwrap(); + assert_eq!( + pk_account_id.to_string(), + "cosmos1aghdjgt5gzntzqgdxdzhjfry90upmtfsy2wuwp" + ); + + let pb_activation_policy = TendermintActivationPolicy::with_public_key(pb); + // Derive account id from the public key. + let pb_account_id = pb_activation_policy.generate_account_id("cosmos").unwrap(); + // Public and private keys are from the same keypair, account ids must be equal. + assert_eq!(pk_account_id, pb_account_id); + } } diff --git a/mm2src/coins/tendermint/tendermint_token.rs b/mm2src/coins/tendermint/tendermint_token.rs index 2e109291b7..b167bdd8c3 100644 --- a/mm2src/coins/tendermint/tendermint_token.rs +++ b/mm2src/coins/tendermint/tendermint_token.rs @@ -5,8 +5,6 @@ use super::ibc::IBC_GAS_LIMIT_DEFAULT; use super::{TendermintCoin, TendermintFeeDetails, GAS_LIMIT_DEFAULT, MIN_TX_SATOSHIS, TIMEOUT_HEIGHT_DELTA, TX_DEFAULT_MEMO}; use crate::coin_errors::ValidatePaymentResult; -use crate::rpc_command::tendermint::IBCWithdrawRequest; -use crate::tendermint::account_id_from_privkey; use crate::utxo::utxo_common::big_decimal_from_sat; use crate::{big_decimal_from_sat_unsigned, utxo::sat_from_big_decimal, BalanceFut, BigDecimal, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, ConfirmPaymentInput, FeeApproxStage, @@ -104,174 +102,19 @@ impl TendermintToken { }; Ok(TendermintToken(Arc::new(token_impl))) } - - pub fn ibc_withdraw(&self, req: IBCWithdrawRequest) -> WithdrawFut { - let platform = self.platform_coin.clone(); - let token = self.clone(); - let fut = async move { - let to_address = - AccountId::from_str(&req.to).map_to_mm(|e| WithdrawError::InvalidAddress(e.to_string()))?; - - let (account_id, priv_key) = match req.from { - Some(from) => { - let path_to_coin = platform.priv_key_policy.path_to_coin_or_err()?; - let path_to_address = from.to_address_path(path_to_coin.coin_type())?; - let priv_key = platform - .priv_key_policy - .hd_wallet_derived_priv_key_or_err(&path_to_address.to_derivation_path(path_to_coin)?)?; - let account_id = account_id_from_privkey(priv_key.as_slice(), &platform.account_prefix) - .map_err(|e| WithdrawError::InternalError(e.to_string()))?; - (account_id, priv_key) - }, - None => ( - platform.account_id.clone(), - *platform.priv_key_policy.activated_key_or_err()?, - ), - }; - - let (base_denom_balance, base_denom_balance_dec) = platform - .get_balance_as_unsigned_and_decimal(&account_id, &platform.denom, token.decimals()) - .await?; - - let (balance_denom, balance_dec) = platform - .get_balance_as_unsigned_and_decimal(&account_id, &token.denom, token.decimals()) - .await?; - - let (amount_denom, amount_dec, total_amount) = if req.max { - ( - balance_denom, - big_decimal_from_sat_unsigned(balance_denom, token.decimals), - balance_dec, - ) - } else { - if balance_dec < req.amount { - return MmError::err(WithdrawError::NotSufficientBalance { - coin: token.ticker.clone(), - available: balance_dec, - required: req.amount, - }); - } - - ( - sat_from_big_decimal(&req.amount, token.decimals())?, - req.amount.clone(), - req.amount, - ) - }; - - if !platform.is_tx_amount_enough(token.decimals, &amount_dec) { - return MmError::err(WithdrawError::AmountTooLow { - amount: amount_dec, - threshold: token.min_tx_amount(), - }); - } - - let received_by_me = if to_address == account_id { - amount_dec - } else { - BigDecimal::default() - }; - - let memo = req.memo.unwrap_or_else(|| TX_DEFAULT_MEMO.into()); - - let msg_transfer = MsgTransfer::new_with_default_timeout( - req.ibc_source_channel.clone(), - account_id.clone(), - to_address.clone(), - Coin { - denom: token.denom.clone(), - amount: amount_denom.into(), - }, - ) - .to_any() - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - - let current_block = token - .current_block() - .compat() - .await - .map_to_mm(WithdrawError::Transport)?; - - let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; - - let (_, gas_limit) = platform.gas_info_for_withdraw(&req.fee, IBC_GAS_LIMIT_DEFAULT); - - let fee_amount_u64 = platform - .calculate_account_fee_amount_as_u64( - &account_id, - &priv_key, - msg_transfer.clone(), - timeout_height, - memo.clone(), - req.fee, - ) - .await?; - - let fee_amount_dec = big_decimal_from_sat_unsigned(fee_amount_u64, platform.decimals()); - - if base_denom_balance < fee_amount_u64 { - return MmError::err(WithdrawError::NotSufficientPlatformBalanceForFee { - coin: platform.ticker().to_string(), - available: base_denom_balance_dec, - required: fee_amount_dec, - }); - } - - let fee_amount = Coin { - denom: platform.denom.clone(), - amount: fee_amount_u64.into(), - }; - - let fee = Fee::from_amount_and_gas(fee_amount, gas_limit); - - let account_info = platform.account_info(&account_id).await?; - let tx_raw = platform - .any_to_signed_raw_tx(&priv_key, account_info, msg_transfer, fee, timeout_height, memo.clone()) - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - - let tx_bytes = tx_raw - .to_bytes() - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - - let hash = sha256(&tx_bytes); - Ok(TransactionDetails { - tx_hash: hex::encode_upper(hash.as_slice()), - tx_hex: tx_bytes.into(), - from: vec![account_id.to_string()], - to: vec![req.to], - my_balance_change: &received_by_me - &total_amount, - spent_by_me: total_amount.clone(), - total_amount, - received_by_me, - block_height: 0, - timestamp: 0, - fee_details: Some(TxFeeDetails::Tendermint(TendermintFeeDetails { - coin: platform.ticker().to_string(), - amount: fee_amount_dec, - uamount: fee_amount_u64, - gas_limit, - })), - coin: token.ticker.clone(), - internal_id: hash.to_vec().into(), - kmd_rewards: None, - transaction_type: TransactionType::default(), - memo: Some(memo), - }) - }; - Box::new(fut.boxed().compat()) - } } #[async_trait] #[allow(unused_variables)] impl SwapOps for TendermintToken { - fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, uuid: &[u8]) -> TransactionFut { + fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, uuid: &[u8], expire_at: u64) -> TransactionFut { self.platform_coin.send_taker_fee_for_denom( fee_addr, dex_fee.fee_amount().into(), self.denom.clone(), self.decimals, uuid, + expire_at, ) } @@ -417,7 +260,7 @@ impl SwapOps for TendermintToken { #[inline] fn derive_htlc_pubkey(&self, swap_unique_data: &[u8]) -> Vec { - self.derive_htlc_key_pair(swap_unique_data).public_slice().to_vec() + self.platform_coin.derive_htlc_pubkey(swap_unique_data) } fn validate_other_pubkey(&self, raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { @@ -624,7 +467,7 @@ impl MarketCoinOps for TendermintToken { #[inline] fn min_trading_vol(&self) -> MmNumber { self.min_tx_amount().into() } - fn is_trezor(&self) -> bool { self.platform_coin.priv_key_policy.is_trezor() } + fn is_trezor(&self) -> bool { self.platform_coin.is_trezor() } } #[async_trait] @@ -640,29 +483,10 @@ impl MmCoin for TendermintToken { let fut = async move { let to_address = AccountId::from_str(&req.to).map_to_mm(|e| WithdrawError::InvalidAddress(e.to_string()))?; - if to_address.prefix() != platform.account_prefix { - return MmError::err(WithdrawError::InvalidAddress(format!( - "expected {} address prefix", - platform.account_prefix - ))); - } - let (account_id, priv_key) = match req.from { - Some(from) => { - let path_to_coin = platform.priv_key_policy.path_to_coin_or_err()?; - let path_to_address = from.to_address_path(path_to_coin.coin_type())?; - let priv_key = platform - .priv_key_policy - .hd_wallet_derived_priv_key_or_err(&path_to_address.to_derivation_path(path_to_coin)?)?; - let account_id = account_id_from_privkey(priv_key.as_slice(), &platform.account_prefix) - .map_err(|e| WithdrawError::InternalError(e.to_string()))?; - (account_id, priv_key) - }, - None => ( - platform.account_id.clone(), - *platform.priv_key_policy.activated_key_or_err()?, - ), - }; + let is_ibc_transfer = to_address.prefix() != platform.account_prefix || req.ibc_source_channel.is_some(); + + let (account_id, maybe_pk) = platform.account_id_and_pk_for_withdraw(req.from)?; let (base_denom_balance, base_denom_balance_dec) = platform .get_balance_as_unsigned_and_decimal(&account_id, &platform.denom, token.decimals()) @@ -707,15 +531,28 @@ impl MmCoin for TendermintToken { BigDecimal::default() }; - let msg_send = MsgSend { - from_address: account_id.clone(), - to_address, - amount: vec![Coin { + let msg_payload = if is_ibc_transfer { + let channel_id = match req.ibc_source_channel { + Some(channel_id) => channel_id, + None => platform.detect_channel_id_for_ibc_transfer(&to_address).await?, + }; + + MsgTransfer::new_with_default_timeout(channel_id, account_id.clone(), to_address.clone(), Coin { denom: token.denom.clone(), amount: amount_denom.into(), - }], + }) + .to_any() + } else { + MsgSend { + from_address: account_id.clone(), + to_address: to_address.clone(), + amount: vec![Coin { + denom: token.denom.clone(), + amount: amount_denom.into(), + }], + } + .to_any() } - .to_any() .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; let memo = req.memo.unwrap_or_else(|| TX_DEFAULT_MEMO.into()); @@ -727,17 +564,14 @@ impl MmCoin for TendermintToken { let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; - let (_, gas_limit) = platform.gas_info_for_withdraw(&req.fee, GAS_LIMIT_DEFAULT); + let (_, gas_limit) = if is_ibc_transfer { + platform.gas_info_for_withdraw(&req.fee, IBC_GAS_LIMIT_DEFAULT) + } else { + platform.gas_info_for_withdraw(&req.fee, GAS_LIMIT_DEFAULT) + }; let fee_amount_u64 = platform - .calculate_account_fee_amount_as_u64( - &account_id, - &priv_key, - msg_send.clone(), - timeout_height, - memo.clone(), - req.fee, - ) + .calculate_account_fee_amount_as_u64(msg_payload.clone(), timeout_height, memo.clone(), req.fee) .await?; let fee_amount_dec = big_decimal_from_sat_unsigned(fee_amount_u64, platform.decimals()); @@ -758,18 +592,18 @@ impl MmCoin for TendermintToken { let fee = Fee::from_amount_and_gas(fee_amount, gas_limit); let account_info = platform.account_info(&account_id).await?; - let tx_raw = platform - .any_to_signed_raw_tx(&priv_key, account_info, msg_send, fee, timeout_height, memo.clone()) - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - let tx_bytes = tx_raw - .to_bytes() + let tx = platform + .any_to_transaction_data(maybe_pk, msg_payload, account_info, fee, timeout_height, memo.clone()) .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - let hash = sha256(&tx_bytes); + let internal_id = { + let hex_vec = tx.tx_hex().cloned().unwrap_or_default().to_vec(); + sha256(&hex_vec).to_vec().into() + }; + Ok(TransactionDetails { - tx_hash: hex::encode_upper(hash.as_slice()), - tx_hex: tx_bytes.into(), + tx, from: vec![account_id.to_string()], to: vec![req.to], my_balance_change: &received_by_me - &total_amount, @@ -785,9 +619,13 @@ impl MmCoin for TendermintToken { gas_limit, })), coin: token.ticker.clone(), - internal_id: hash.to_vec().into(), + internal_id, kmd_rewards: None, - transaction_type: TransactionType::default(), + transaction_type: if is_ibc_transfer { + TransactionType::TendermintIBCTransfer + } else { + TransactionType::StandardTransfer + }, memo: Some(memo), }) }; diff --git a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs index 86a2b40ab4..c9abaaa398 100644 --- a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs +++ b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs @@ -5,7 +5,8 @@ use crate::tendermint::htlc::CustomTendermintMsgType; use crate::tendermint::TendermintFeeDetails; use crate::tx_history_storage::{GetTxHistoryFilters, WalletId}; use crate::utxo::utxo_common::big_decimal_from_sat_unsigned; -use crate::{HistorySyncState, MarketCoinOps, MmCoin, TransactionDetails, TransactionType, TxFeeDetails}; +use crate::{HistorySyncState, MarketCoinOps, MmCoin, TransactionData, TransactionDetails, TransactionType, + TxFeeDetails}; use async_trait::async_trait; use bitcrypto::sha256; use common::executor::Timer; @@ -705,8 +706,7 @@ where received_by_me: tx_amounts.received_by_me, // This can be 0 since it gets remapped in `coins::my_tx_history_v2` my_balance_change: BigDecimal::default(), - tx_hash: tx_hash.to_string(), - tx_hex: msg.into(), + tx: TransactionData::new_signed(msg.into(), tx_hash.to_string()), fee_details: Some(TxFeeDetails::Tendermint(fee_details.clone())), block_height: tx.height.into(), coin: transfer_details.denom.clone(), diff --git a/mm2src/coins/test_coin.rs b/mm2src/coins/test_coin.rs index 684edc6a29..84eb9d6c0e 100644 --- a/mm2src/coins/test_coin.rs +++ b/mm2src/coins/test_coin.rs @@ -114,7 +114,9 @@ impl MarketCoinOps for TestCoin { #[async_trait] #[mockable] impl SwapOps for TestCoin { - fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, uuid: &[u8]) -> TransactionFut { unimplemented!() } + fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, uuid: &[u8], _expire_at: u64) -> TransactionFut { + unimplemented!() + } fn send_maker_payment(&self, _maker_payment_args: SendPaymentArgs) -> TransactionFut { unimplemented!() } 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 49993e4c6a..0ce85dae9a 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 @@ -447,24 +447,26 @@ impl TxHistoryStorage for SqliteTxHistoryStorage { let sql_transaction = conn.transaction()?; for tx in transactions { - let tx_hash = tx.tx_hash.clone(); + let Some(tx_hash) = tx.tx.tx_hash() else {continue}; + let Some(tx_hex) = tx.tx.tx_hex().cloned() else {continue}; + let tx_hex = format!("{:02x}", tx_hex); + let internal_id = format!("{:02x}", tx.internal_id); let confirmation_status = ConfirmationStatus::from_block_height(tx.block_height); let token_id = token_id_from_tx_type(&tx.transaction_type); let tx_json = json::to_string(&tx).expect("serialization should not fail"); - let tx_hex = format!("{:02x}", tx.tx_hex); - let tx_cache_params = [&tx_hash, &tx_hex]; + let tx_cache_params = [tx_hash, &tx_hex]; sql_transaction.execute(&insert_tx_in_cache_sql(&wallet_id)?, tx_cache_params)?; let params = [ tx_hash, - internal_id.clone(), - tx.block_height.to_string(), - confirmation_status.to_sql_param_str(), - token_id, - tx_json, + &internal_id, + &tx.block_height.to_string(), + &confirmation_status.to_sql_param_str(), + &token_id, + &tx_json, ]; sql_transaction.execute(&insert_tx_in_history_sql(&wallet_id)?, params)?; 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 ab4a2a7e85..2ffcc9760d 100644 --- a/mm2src/coins/tx_history_storage/tx_history_v2_tests.rs +++ b/mm2src/coins/tx_history_storage/tx_history_v2_tests.rs @@ -363,24 +363,24 @@ async fn test_add_and_get_tx_from_cache_impl() { let tx = get_bch_tx_details("6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69"); storage - .add_tx_to_cache(&wallet_id_1, &tx.tx_hash, &tx.tx_hex) + .add_tx_to_cache(&wallet_id_1, tx.tx.tx_hash().unwrap(), tx.tx.tx_hex().unwrap()) .await .unwrap(); let tx_hex_from_1 = storage - .tx_bytes_from_cache(&wallet_id_1, &tx.tx_hash) + .tx_bytes_from_cache(&wallet_id_1, tx.tx.tx_hash().unwrap()) .await .unwrap() .unwrap(); - assert_eq!(tx_hex_from_1, tx.tx_hex); + assert_eq!(&tx_hex_from_1, tx.tx.tx_hex().unwrap()); // Since `wallet_id_1` and `wallet_id_2` wallets have the same `ticker`, the wallets must have one transaction cache. let tx_hex_from_2 = storage - .tx_bytes_from_cache(&wallet_id_2, &tx.tx_hash) + .tx_bytes_from_cache(&wallet_id_2, tx.tx.tx_hash().unwrap()) .await .unwrap() .unwrap(); - assert_eq!(tx_hex_from_2, tx.tx_hex); + assert_eq!(&tx_hex_from_2, tx.tx.tx_hex().unwrap()); } async fn test_get_raw_tx_bytes_on_add_transactions_impl() { @@ -401,7 +401,7 @@ async fn test_get_raw_tx_bytes_on_add_transactions_impl() { let mut tx2 = tx1.clone(); tx2.internal_id = BytesJson(vec![1; 32]); - let expected_tx_hex = tx1.tx_hex.clone(); + let expected_tx_hex = tx1.tx.tx_hex().unwrap().clone(); let transactions = [tx1, tx2]; storage 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 b55b04ad86..acc6d338e2 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 @@ -59,13 +59,15 @@ impl TxHistoryStorage for IndexedDbTxHistoryStorage { let cache_table = db_transaction.table::().await?; for tx in transactions { + let Some(tx_hash) = tx.tx.tx_hash() else { continue }; + let history_item = TxHistoryTableV2::from_tx_details(wallet_id.clone(), &tx)?; history_table.add_item(&history_item).await?; - let cache_item = TxCacheTableV2::from_tx_details(wallet_id.clone(), &tx); + let cache_item = TxCacheTableV2::from_tx_details(wallet_id.clone(), &tx)?; let index_keys = MultiIndex::new(TxCacheTableV2::COIN_TX_HASH_INDEX) .with_value(&wallet_id.ticker)? - .with_value(&tx.tx_hash)?; + .with_value(tx_hash)?; // `TxHistoryTableV2::tx_hash` is not a unique field, but `TxCacheTableV2::tx_hash` is unique. // So we use `DbTable::add_item_or_ignore_by_unique_multi_index` instead of `DbTable::add_item` // since `transactions` may contain txs with same `tx_hash` but different `internal_id`. @@ -396,12 +398,17 @@ impl TxHistoryTableV2 { const WALLET_ID_TOKEN_ID_INDEX: &'static str = "wallet_id_token_id"; fn from_tx_details(wallet_id: WalletId, tx: &TransactionDetails) -> WasmTxHistoryResult { + let tx_hash = tx + .tx + .tx_hash() + .ok_or_else(|| WasmTxHistoryError::NotSupported("Unsupported type of TransactionDetails".to_string()))?; + let details_json = json::to_value(tx).map_to_mm(|e| WasmTxHistoryError::ErrorSerializing(e.to_string()))?; let hd_wallet_rmd160 = wallet_id.hd_wallet_rmd160_or_exclude(); Ok(TxHistoryTableV2 { coin: wallet_id.ticker, hd_wallet_rmd160, - tx_hash: tx.tx_hash.clone(), + tx_hash: tx_hash.to_string(), internal_id: tx.internal_id.clone(), block_height: BeBigUint::from(tx.block_height), confirmation_status: ConfirmationStatus::from_block_height(tx.block_height), @@ -458,12 +465,18 @@ impl TxCacheTableV2 { /// * tx_hash - transaction hash const COIN_TX_HASH_INDEX: &'static str = "coin_tx_hash"; - fn from_tx_details(wallet_id: WalletId, tx: &TransactionDetails) -> TxCacheTableV2 { - TxCacheTableV2 { - coin: wallet_id.ticker, - tx_hash: tx.tx_hash.clone(), - tx_hex: tx.tx_hex.clone(), + fn from_tx_details(wallet_id: WalletId, tx: &TransactionDetails) -> WasmTxHistoryResult { + if let (Some(tx_hash), Some(tx_hex)) = (tx.tx.tx_hash(), tx.tx.tx_hex()) { + return Ok(TxCacheTableV2 { + coin: wallet_id.ticker, + tx_hash: tx_hash.to_string(), + tx_hex: tx_hex.clone(), + }); } + + MmError::err(WasmTxHistoryError::NotSupported( + "Unsupported type of TransactionDetails".to_string(), + )) } } diff --git a/mm2src/coins/utxo/bch.rs b/mm2src/coins/utxo/bch.rs index c832d1a75d..d5f3efe6c0 100644 --- a/mm2src/coins/utxo/bch.rs +++ b/mm2src/coins/utxo/bch.rs @@ -870,7 +870,7 @@ impl UtxoCommonOps for BchCoin { #[async_trait] impl SwapOps for BchCoin { #[inline] - fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8]) -> TransactionFut { + fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionFut { utxo_common::send_taker_fee(self.clone(), fee_addr, dex_fee) } diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index 9d1b16723d..e46ee28330 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -524,7 +524,7 @@ impl UtxoStandardOps for QtumCoin { #[async_trait] impl SwapOps for QtumCoin { #[inline] - fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8]) -> TransactionFut { + fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionFut { utxo_common::send_taker_fee(self.clone(), fee_addr, dex_fee) } diff --git a/mm2src/coins/utxo/qtum_delegation.rs b/mm2src/coins/utxo/qtum_delegation.rs index 4a62adcd26..b7cfff7c1c 100644 --- a/mm2src/coins/utxo/qtum_delegation.rs +++ b/mm2src/coins/utxo/qtum_delegation.rs @@ -8,7 +8,7 @@ use crate::utxo::utxo_common::{big_decimal_from_sat_unsigned, UtxoTxBuilder}; use crate::utxo::{qtum, utxo_common, Address, GetUtxoListOps, UtxoCommonOps}; use crate::utxo::{PrivKeyPolicyNotAllowed, UTXO_LOCK}; use crate::{DelegationError, DelegationFut, DelegationResult, MarketCoinOps, StakingInfos, StakingInfosError, - StakingInfosFut, StakingInfosResult, TransactionDetails, TransactionType}; + StakingInfosFut, StakingInfosResult, TransactionData, TransactionDetails, TransactionType}; use bitcrypto::dhash256; use common::now_sec; use derive_more::Display; @@ -324,8 +324,10 @@ impl QtumCoin { let my_balance_change = &received_by_me - &spent_by_me; Ok(TransactionDetails { - tx_hex: serialize(&generated_tx.signed).into(), - tx_hash: generated_tx.signed.hash().reversed().to_vec().to_tx_hash(), + tx: TransactionData::new_signed( + serialize(&generated_tx.signed).into(), + generated_tx.signed.hash().reversed().to_vec().to_tx_hash(), + ), from: vec![my_address_string], to: vec![to_address], total_amount: qtum_amount, diff --git a/mm2src/coins/utxo/slp.rs b/mm2src/coins/utxo/slp.rs index 4138d3b7a8..eb5f3c1a70 100644 --- a/mm2src/coins/utxo/slp.rs +++ b/mm2src/coins/utxo/slp.rs @@ -20,8 +20,8 @@ use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, C RawTransactionResult, RefundError, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignRawTransactionRequest, SignatureResult, SpendPaymentArgs, SwapOps, SwapTxTypeWithSecretHash, TakerSwapMakerCoin, TradeFee, TradePreimageError, - TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionDetails, TransactionEnum, - TransactionErr, TransactionFut, TransactionResult, TxFeeDetails, TxMarshalingErr, + TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionData, TransactionDetails, + TransactionEnum, TransactionErr, TransactionFut, TransactionResult, TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentInput, ValidateWatcherSpendInput, VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, @@ -1214,7 +1214,7 @@ impl MarketCoinOps for SlpToken { #[async_trait] impl SwapOps for SlpToken { - fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8]) -> TransactionFut { + fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionFut { let coin = self.clone(); let fee_pubkey = try_tx_fus!(Public::from_slice(fee_addr)); let script_pubkey = ScriptBuilder::build_p2pkh(&fee_pubkey.address_hash().into()).into(); @@ -1722,9 +1722,8 @@ impl MmCoin for SlpToken { let tx_hash: BytesJson = signed.hash().reversed().take().to_vec().into(); let details = TransactionDetails { - tx_hex: serialize(&signed).into(), internal_id: tx_hash.clone(), - tx_hash: tx_hash.to_tx_hash(), + tx: TransactionData::new_signed(serialize(&signed).into(), tx_hash.to_tx_hash()), from: vec![my_address_string], to: vec![to_address], total_amount, diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index c757e942db..615d046b28 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -19,15 +19,15 @@ use crate::{scan_for_new_addresses_impl, CanRefundHtlc, CoinBalance, CoinWithDer SearchForSwapTxSpendInput, SendMakerPaymentArgs, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SendTakerFundingArgs, SignRawTransactionEnum, SignRawTransactionRequest, SignUtxoTransactionParams, SignatureError, SignatureResult, SpendMakerPaymentArgs, SpendPaymentArgs, SwapOps, - SwapTxTypeWithSecretHash, TradePreimageValue, TransactionFut, TransactionResult, TxFeeDetails, TxGenError, - TxMarshalingErr, TxPreimageWithSig, ValidateAddressResult, ValidateOtherPubKeyErr, ValidatePaymentFut, - ValidatePaymentInput, ValidateSwapV2TxError, ValidateSwapV2TxResult, ValidateTakerFundingArgs, - ValidateTakerFundingSpendPreimageError, ValidateTakerFundingSpendPreimageResult, - ValidateTakerPaymentSpendPreimageError, ValidateTakerPaymentSpendPreimageResult, - ValidateWatcherSpendInput, VerificationError, VerificationResult, WatcherSearchForSwapTxSpendInput, - WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawResult, WithdrawSenderAddress, - EARLY_CONFIRMATION_ERR_LOG, INVALID_RECEIVER_ERR_LOG, INVALID_REFUND_TX_ERR_LOG, INVALID_SCRIPT_ERR_LOG, - INVALID_SENDER_ERR_LOG, OLD_TRANSACTION_ERR_LOG}; + SwapTxTypeWithSecretHash, TradePreimageValue, TransactionData, TransactionFut, TransactionResult, + TxFeeDetails, TxGenError, TxMarshalingErr, TxPreimageWithSig, ValidateAddressResult, + ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, ValidateSwapV2TxError, + ValidateSwapV2TxResult, ValidateTakerFundingArgs, ValidateTakerFundingSpendPreimageError, + ValidateTakerFundingSpendPreimageResult, ValidateTakerPaymentSpendPreimageError, + ValidateTakerPaymentSpendPreimageResult, ValidateWatcherSpendInput, VerificationError, VerificationResult, + WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, + WithdrawResult, WithdrawSenderAddress, EARLY_CONFIRMATION_ERR_LOG, INVALID_RECEIVER_ERR_LOG, + INVALID_REFUND_TX_ERR_LOG, INVALID_SCRIPT_ERR_LOG, INVALID_SENDER_ERR_LOG, OLD_TRANSACTION_ERR_LOG}; use crate::{MmCoinEnum, WatcherReward, WatcherRewardError}; use base64::engine::general_purpose::STANDARD; use base64::Engine; @@ -3243,7 +3243,7 @@ where let mut history_map: HashMap = history .into_iter() .filter_map(|tx| { - let tx_hash = H256Json::from_str(&tx.tx_hash).ok()?; + let tx_hash = H256Json::from_str(tx.tx.tx_hash()?).ok()?; Some((tx_hash, tx)) }) .collect(); @@ -3693,8 +3693,7 @@ pub async fn tx_details_by_hash( spent_by_me: big_decimal_from_sat_unsigned(spent_by_me, coin.as_ref().decimals), my_balance_change: big_decimal_from_sat(received_by_me as i64 - spent_by_me as i64, coin.as_ref().decimals), total_amount: big_decimal_from_sat_unsigned(input_amount, coin.as_ref().decimals), - tx_hash: tx.hash().reversed().to_vec().to_tx_hash(), - tx_hex: verbose_tx.hex, + tx: TransactionData::new_signed(verbose_tx.hex, tx.hash().reversed().to_vec().to_tx_hash()), fee_details: Some(fee_details.into()), block_height: verbose_tx.height.unwrap_or(0), coin: ticker.clone(), @@ -3749,14 +3748,19 @@ pub async fn update_kmd_rewards( where T: UtxoCommonOps + UtxoStandardOps + MarketCoinOps, { + let (Some(tx_hex), Some(tx_hash)) = (tx_details.tx.tx_hex(), tx_details.tx.tx_hash()) else { + return MmError::err(UtxoRpcError::Internal("Invalid TransactionDetails".to_string())); + }; + if !tx_details.should_update_kmd_rewards() { let error = "There is no need to update KMD rewards".to_owned(); return MmError::err(UtxoRpcError::Internal(error)); } - let tx: UtxoTx = deserialize(tx_details.tx_hex.as_slice()).map_to_mm(|e| { + + let tx: UtxoTx = deserialize(tx_hex.as_slice()).map_to_mm(|e| { UtxoRpcError::Internal(format!( "Error deserializing the {:?} transaction hex: {:?}", - tx_details.tx_hash, e + tx_hash, e )) })?; let kmd_rewards = coin.calc_interest_of_tx(&tx, input_transactions).await?; diff --git a/mm2src/coins/utxo/utxo_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index 93c0535b74..cab85c2363 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -301,7 +301,7 @@ impl UtxoStandardOps for UtxoStandardCoin { #[async_trait] impl SwapOps for UtxoStandardCoin { #[inline] - fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8]) -> TransactionFut { + fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionFut { utxo_common::send_taker_fee(self.clone(), fee_addr, dex_fee) } diff --git a/mm2src/coins/utxo/utxo_tests.rs b/mm2src/coins/utxo/utxo_tests.rs index a358d224aa..fc3a00202b 100644 --- a/mm2src/coins/utxo/utxo_tests.rs +++ b/mm2src/coins/utxo/utxo_tests.rs @@ -597,6 +597,7 @@ fn test_withdraw_impl_set_fixed_fee() { amount: "0.1".parse().unwrap(), }), memo: None, + ibc_source_channel: None, }; let expected = Some( UtxoFeeDetails { @@ -639,6 +640,7 @@ fn test_withdraw_impl_sat_per_kb_fee() { amount: "0.1".parse().unwrap(), }), memo: None, + ibc_source_channel: None, }; // The resulting transaction size might be 244 or 245 bytes depending on signature size // MM2 always expects the worst case during fee calculation @@ -684,6 +686,7 @@ fn test_withdraw_impl_sat_per_kb_fee_amount_equal_to_max() { amount: "0.1".parse().unwrap(), }), memo: None, + ibc_source_channel: None, }; let tx_details = coin.withdraw(withdraw_req).wait().unwrap(); // The resulting transaction size might be 210 or 211 bytes depending on signature size @@ -731,6 +734,7 @@ fn test_withdraw_impl_sat_per_kb_fee_amount_equal_to_max_dust_included_to_fee() amount: "0.09999999".parse().unwrap(), }), memo: None, + ibc_source_channel: None, }; let tx_details = coin.withdraw(withdraw_req).wait().unwrap(); // The resulting transaction size might be 210 or 211 bytes depending on signature size @@ -778,6 +782,7 @@ fn test_withdraw_impl_sat_per_kb_fee_amount_over_max() { amount: "0.1".parse().unwrap(), }), memo: None, + ibc_source_channel: None, }; coin.withdraw(withdraw_req).wait().unwrap_err(); } @@ -812,6 +817,7 @@ fn test_withdraw_impl_sat_per_kb_fee_max() { amount: "0.1".parse().unwrap(), }), memo: None, + ibc_source_channel: None, }; // The resulting transaction size might be 210 or 211 bytes depending on signature size // MM2 always expects the worst case during fee calculation @@ -872,6 +878,7 @@ fn test_withdraw_kmd_rewards_impl( max: false, fee: None, memo: None, + ibc_source_channel: None, }; let expected_fee = TxFeeDetails::Utxo(UtxoFeeDetails { coin: Some("KMD".into()), @@ -947,6 +954,7 @@ fn test_withdraw_rick_rewards_none() { max: false, fee: None, memo: None, + ibc_source_channel: None, }; let expected_fee = TxFeeDetails::Utxo(UtxoFeeDetails { coin: Some(TEST_COIN_NAME.into()), @@ -3105,9 +3113,10 @@ fn test_withdraw_to_p2pkh() { max: false, fee: None, memo: None, + ibc_source_channel: None, }; let tx_details = coin.withdraw(withdraw_req).wait().unwrap(); - let transaction: UtxoTx = deserialize(tx_details.tx_hex.as_slice()).unwrap(); + let transaction: UtxoTx = deserialize(tx_details.tx.tx_hex().unwrap().as_slice()).unwrap(); let output_script: Script = transaction.outputs[0].script_pubkey.clone().into(); let expected_script = Builder::build_p2pkh(p2pkh_address.hash()); @@ -3157,9 +3166,10 @@ fn test_withdraw_to_p2sh() { max: false, fee: None, memo: None, + ibc_source_channel: None, }; let tx_details = coin.withdraw(withdraw_req).wait().unwrap(); - let transaction: UtxoTx = deserialize(tx_details.tx_hex.as_slice()).unwrap(); + let transaction: UtxoTx = deserialize(tx_details.tx.tx_hex().unwrap().as_slice()).unwrap(); let output_script: Script = transaction.outputs[0].script_pubkey.clone().into(); let expected_script = Builder::build_p2sh(p2sh_address.hash()); @@ -3209,9 +3219,10 @@ fn test_withdraw_to_p2wpkh() { max: false, fee: None, memo: None, + ibc_source_channel: None, }; let tx_details = coin.withdraw(withdraw_req).wait().unwrap(); - let transaction: UtxoTx = deserialize(tx_details.tx_hex.as_slice()).unwrap(); + let transaction: UtxoTx = deserialize(tx_details.tx.tx_hex().unwrap().as_slice()).unwrap(); let output_script: Script = transaction.outputs[0].script_pubkey.clone().into(); let expected_script = Builder::build_p2wpkh(p2wpkh_address.hash()).expect("valid p2wpkh script"); diff --git a/mm2src/coins/utxo/utxo_tx_history_v2.rs b/mm2src/coins/utxo/utxo_tx_history_v2.rs index 6cbd2877b8..5c88b315f5 100644 --- a/mm2src/coins/utxo/utxo_tx_history_v2.rs +++ b/mm2src/coins/utxo/utxo_tx_history_v2.rs @@ -514,7 +514,9 @@ where 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) { + let Some(tx_hash) = tx.tx.tx_hash() else {continue}; + + let found = match H256Json::from_str(tx_hash) { Ok(unconfirmed_tx_hash) => txs_with_height.get(&unconfirmed_tx_hash), Err(_) => None, }; diff --git a/mm2src/coins/utxo/utxo_withdraw.rs b/mm2src/coins/utxo/utxo_withdraw.rs index 41350a1be2..30e7463605 100644 --- a/mm2src/coins/utxo/utxo_withdraw.rs +++ b/mm2src/coins/utxo/utxo_withdraw.rs @@ -2,8 +2,8 @@ use crate::rpc_command::init_withdraw::{WithdrawInProgressStatus, WithdrawTaskHa use crate::utxo::utxo_common::{big_decimal_from_sat, UtxoTxBuilder}; use crate::utxo::{output_script, sat_from_big_decimal, ActualTxFee, Address, FeePolicy, GetUtxoListOps, PrivKeyPolicy, UtxoAddressFormat, UtxoCoinFields, UtxoCommonOps, UtxoFeeDetails, UtxoTx, UTXO_LOCK}; -use crate::{CoinWithDerivationMethod, GetWithdrawSenderAddress, MarketCoinOps, TransactionDetails, WithdrawError, - WithdrawFee, WithdrawRequest, WithdrawResult}; +use crate::{CoinWithDerivationMethod, GetWithdrawSenderAddress, MarketCoinOps, TransactionData, TransactionDetails, + WithdrawError, WithdrawFee, WithdrawRequest, WithdrawResult}; use async_trait::async_trait; use chain::TransactionOutput; use common::log::info; @@ -213,8 +213,7 @@ where spent_by_me: big_decimal_from_sat(data.spent_by_me as i64, decimals), received_by_me: big_decimal_from_sat(data.received_by_me as i64, decimals), my_balance_change: big_decimal_from_sat(data.received_by_me as i64 - data.spent_by_me as i64, decimals), - tx_hash: signed.hash().reversed().to_vec().to_tx_hash(), - tx_hex, + tx: TransactionData::new_signed(tx_hex, signed.hash().reversed().to_vec().to_tx_hash()), fee_details: Some(fee_details.into()), block_height: 0, coin: ticker, diff --git a/mm2src/coins/z_coin.rs b/mm2src/coins/z_coin.rs index 1614a3ee6d..a8a1dc796a 100644 --- a/mm2src/coins/z_coin.rs +++ b/mm2src/coins/z_coin.rs @@ -33,8 +33,8 @@ use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, Coi RawTransactionFut, RawTransactionRequest, RawTransactionResult, RefundError, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignRawTransactionRequest, SignatureError, SignatureResult, SpendPaymentArgs, SwapOps, TakerSwapMakerCoin, - TradeFee, TradePreimageFut, TradePreimageResult, TradePreimageValue, Transaction, TransactionDetails, - TransactionEnum, TransactionFut, TransactionResult, TxFeeDetails, TxMarshalingErr, + TradeFee, TradePreimageFut, TradePreimageResult, TradePreimageValue, Transaction, TransactionData, + TransactionDetails, TransactionEnum, TransactionFut, TransactionResult, TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, ValidateWatcherSpendInput, VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, @@ -1200,7 +1200,7 @@ impl MarketCoinOps for ZCoin { #[async_trait] impl SwapOps for ZCoin { - fn send_taker_fee(&self, _fee_addr: &[u8], dex_fee: DexFee, uuid: &[u8]) -> TransactionFut { + fn send_taker_fee(&self, _fee_addr: &[u8], dex_fee: DexFee, uuid: &[u8], _expire_at: u64) -> TransactionFut { let selfi = self.clone(); let uuid = uuid.to_owned(); let fut = async move { @@ -1996,8 +1996,7 @@ impl InitWithdrawCoin for ZCoin { let spent_by_me = big_decimal_from_sat_unsigned(data.spent_by_me, self.decimals()); Ok(TransactionDetails { - tx_hex: tx_bytes.into(), - tx_hash: hex::encode(&tx_hash), + tx: TransactionData::new_signed(tx_bytes.into(), hex::encode(&tx_hash)), from: vec![self.z_fields.my_z_addr_encoded.clone()], to: vec![req.to], my_balance_change: &received_by_me - &spent_by_me, diff --git a/mm2src/coins_activation/src/tendermint_with_assets_activation.rs b/mm2src/coins_activation/src/tendermint_with_assets_activation.rs index 521e6776d0..d413175364 100644 --- a/mm2src/coins_activation/src/tendermint_with_assets_activation.rs +++ b/mm2src/coins_activation/src/tendermint_with_assets_activation.rs @@ -11,9 +11,10 @@ use async_trait::async_trait; use coins::hd_wallet::HDPathAccountToAddressId; use coins::my_tx_history_v2::TxHistoryStorage; use coins::tendermint::tendermint_tx_history_v2::tendermint_history_loop; -use coins::tendermint::{tendermint_priv_key_policy, TendermintCoin, TendermintCommons, TendermintConf, - TendermintInitError, TendermintInitErrorKind, TendermintProtocolInfo, TendermintToken, - TendermintTokenActivationParams, TendermintTokenInitError, TendermintTokenProtocolInfo}; +use coins::tendermint::{tendermint_priv_key_policy, TendermintActivationPolicy, TendermintCoin, TendermintCommons, + TendermintConf, TendermintInitError, TendermintInitErrorKind, TendermintProtocolInfo, + TendermintPublicKey, TendermintToken, TendermintTokenActivationParams, + TendermintTokenInitError, TendermintTokenProtocolInfo}; use coins::{CoinBalance, CoinProtocol, MarketCoinOps, MmCoin, MmCoinEnum, PrivKeyBuildPolicy}; use common::executor::{AbortSettings, SpawnAbortable}; use common::{true_f, Future01CompatExt}; @@ -23,7 +24,7 @@ use mm2_event_stream::behaviour::{EventBehaviour, EventInitStatus}; use mm2_event_stream::EventStreamConfiguration; use mm2_number::BigDecimal; use rpc_task::RpcTaskHandleShared; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value as Json; use std::collections::{HashMap, HashSet}; @@ -48,6 +49,53 @@ pub struct TendermintActivationParams { /// /account'/change/address_index`. #[serde(default)] pub path_to_address: HDPathAccountToAddressId, + #[serde(default)] + #[serde(deserialize_with = "deserialize_account_public_key")] + with_pubkey: Option, +} + +fn deserialize_account_public_key<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let value: Json = serde::Deserialize::deserialize(deserializer)?; + + match value { + Json::Object(mut map) => { + if let Some(type_) = map.remove("type") { + if let Some(value) = map.remove("value") { + match type_.as_str() { + Some("ed25519") => { + let value: Vec = value + .as_array() + .unwrap() + .iter() + .map(|i| i.as_u64().unwrap() as u8) + .collect(); + Ok(Some(TendermintPublicKey::from_raw_ed25519(&value).unwrap())) + }, + Some("secp256k1") => { + let value: Vec = value + .as_array() + .unwrap() + .iter() + .map(|i| i.as_u64().unwrap() as u8) + .collect(); + Ok(Some(TendermintPublicKey::from_raw_secp256k1(&value).unwrap())) + }, + _ => Err(serde::de::Error::custom( + "Unsupported pubkey algorithm. Use one of ['ed25519', 'secp256k1']", + )), + } + } else { + Err(serde::de::Error::custom("Missing field 'value'.")) + } + } else { + Err(serde::de::Error::custom("Missing field 'type'.")) + } + }, + _ => Err(serde::de::Error::custom("Invalid data.")), + } } impl TxHistory for TendermintActivationParams { @@ -187,18 +235,27 @@ impl PlatformCoinWithTokensActivationOps for TendermintCoin { ) -> Result> { let conf = TendermintConf::try_from_json(&ticker, coin_conf)?; - let priv_key_build_policy = - PrivKeyBuildPolicy::detect_priv_key_policy(&ctx).mm_err(|e| TendermintInitError { - ticker: ticker.clone(), - kind: TendermintInitErrorKind::Internal(e.to_string()), - })?; + let activation_policy = if let Some(pubkey) = activation_request.with_pubkey { + if ctx.is_watcher() || ctx.use_watchers() { + return MmError::err(TendermintInitError { + ticker: ticker.clone(), + kind: TendermintInitErrorKind::CantUseWatchersWithPubkeyPolicy, + }); + } + + TendermintActivationPolicy::with_public_key(pubkey) + } else { + let private_key_policy = + PrivKeyBuildPolicy::detect_priv_key_policy(&ctx).mm_err(|e| TendermintInitError { + ticker: ticker.clone(), + kind: TendermintInitErrorKind::Internal(e.to_string()), + })?; + + let tendermint_private_key_policy = + tendermint_priv_key_policy(&conf, &ticker, private_key_policy, activation_request.path_to_address)?; - let priv_key_policy = tendermint_priv_key_policy( - &conf, - &ticker, - priv_key_build_policy, - activation_request.path_to_address, - )?; + TendermintActivationPolicy::with_private_key_policy(tendermint_private_key_policy) + }; TendermintCoin::init( &ctx, @@ -207,7 +264,7 @@ impl PlatformCoinWithTokensActivationOps for TendermintCoin { protocol_conf, activation_request.rpc_urls, activation_request.tx_history, - priv_key_policy, + activation_policy, ) .await } diff --git a/mm2src/common/common.rs b/mm2src/common/common.rs index 8008fd6502..552abae1d4 100644 --- a/mm2src/common/common.rs +++ b/mm2src/common/common.rs @@ -1054,6 +1054,9 @@ impl Default for PagingOptionsEnum { #[inline(always)] pub fn get_utc_timestamp() -> i64 { Utc::now().timestamp() } +#[inline(always)] +pub fn get_utc_timestamp_nanos() -> i64 { Utc::now().timestamp_nanos() } + #[inline(always)] pub fn get_local_duration_since_epoch() -> Result { SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) diff --git a/mm2src/common/expirable_map.rs b/mm2src/common/expirable_map.rs index 73ebc1f580..9b85dea84c 100644 --- a/mm2src/common/expirable_map.rs +++ b/mm2src/common/expirable_map.rs @@ -38,6 +38,10 @@ impl ExpirableMap { #[inline] pub fn new() -> Self { Self(FxHashMap::default()) } + /// Returns the associated value if present. + #[inline] + pub fn get(&mut self, k: &K) -> Option<&V> { self.0.get(k).map(|v| &v.value) } + /// Inserts a key-value pair with an expiration duration. /// /// If a value already exists for the given key, it will be updated and then @@ -54,7 +58,7 @@ impl ExpirableMap { /// Removes expired entries from the map. pub fn clear_expired_entries(&mut self) { self.0.retain(|_k, v| Instant::now() < v.expires_at); } - // Removes a key-value pair from the map and returns the associated value if present. + /// Removes a key-value pair from the map and returns the associated value if present. #[inline] pub fn remove(&mut self, k: &K) -> Option { self.0.remove(k).map(|v| v.value) } } diff --git a/mm2src/mm2_core/Cargo.toml b/mm2src/mm2_core/Cargo.toml index db1ed1043d..943dcac280 100644 --- a/mm2src/mm2_core/Cargo.toml +++ b/mm2src/mm2_core/Cargo.toml @@ -8,6 +8,7 @@ doctest = false [dependencies] arrayref = "0.3" +async-std = { version = "1.5", features = ["unstable"] } async-trait = "0.1" cfg-if = "1.0" common = { path = "../common" } @@ -16,19 +17,26 @@ derive_more = "0.99" futures = { version = "0.3", package = "futures", features = ["compat", "async-await", "thread-pool"] } hex = "0.4.2" lazy_static = "1.4" +mm2_err_handle = { path = "../mm2_err_handle" } mm2_event_stream = { path = "../mm2_event_stream" } mm2_metrics = { path = "../mm2_metrics" } primitives = { path = "../mm2_bitcoin/primitives" } rand = { version = "0.7", features = ["std", "small_rng", "wasm-bindgen"] } serde = "1" +ser_error = { path = "../derives/ser_error" } +ser_error_derive = { path = "../derives/ser_error_derive" } serde_json = { version = "1", features = ["preserve_order", "raw_value"] } shared_ref_counter = { path = "../common/shared_ref_counter" } uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } [target.'cfg(target_arch = "wasm32")'.dependencies] gstuff = { version = "0.7", features = ["nightly"] } +instant = { version = "0.1.12", features = ["wasm-bindgen"] } mm2_rpc = { path = "../mm2_rpc", features = [ "rpc_facilities" ] } +wasm-bindgen-test = { version = "0.3.2" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] rustls = { version = "0.21", default-features = false } gstuff = { version = "0.7", features = ["nightly"] } +instant = "0.1.12" +tokio = { version = "1.20", features = ["io-util", "rt-multi-thread", "net"] } diff --git a/mm2src/mm2_core/src/data_asker.rs b/mm2src/mm2_core/src/data_asker.rs new file mode 100644 index 0000000000..5d01310630 --- /dev/null +++ b/mm2src/mm2_core/src/data_asker.rs @@ -0,0 +1,188 @@ +use common::expirable_map::ExpirableMap; +use common::{HttpStatusCode, StatusCode}; +use derive_more::Display; +use futures::channel::oneshot; +use futures::lock::Mutex as AsyncMutex; +use instant::Duration; +use mm2_err_handle::prelude::*; +use mm2_event_stream::Event; +use ser_error_derive::SerializeErrorType; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use std::sync::atomic::{self, AtomicUsize}; +use std::sync::Arc; + +use crate::mm_ctx::{MmArc, MmCtx}; + +const EVENT_NAME: &str = "DATA_NEEDED"; + +#[derive(Clone, Debug, Default)] +pub struct DataAsker { + data_id: Arc, + awaiting_asks: Arc>>>, +} + +#[derive(Debug, Display)] +pub enum AskForDataError { + #[display( + fmt = "Expected JSON data, but given(from data provider) one was not deserializable: {:?}", + _0 + )] + DeserializationError(serde_json::Error), + Internal(String), +} + +impl MmCtx { + pub async fn ask_for_data( + &self, + data_type: &str, + data: Input, + timeout: Duration, + ) -> Result> + where + Input: Serialize, + Output: DeserializeOwned, + { + if data_type.contains(char::is_whitespace) { + return MmError::err(AskForDataError::Internal(format!( + "data_type can not contain whitespace, but got {data_type}" + ))); + } + + let data_id = self.data_asker.data_id.fetch_add(1, atomic::Ordering::SeqCst); + let (sender, receiver) = futures::channel::oneshot::channel::(); + + // We don't want to hold the lock, so call this in an inner-scope. + { + self.data_asker + .awaiting_asks + .lock() + .await + .insert(data_id, sender, timeout); + } + + let input = json!({ + "data_id": data_id, + "timeout_secs": timeout.as_secs(), + "data": data + }); + + self.stream_channel_controller + .broadcast(Event::new(format!("{EVENT_NAME}:{data_type}"), input.to_string())) + .await; + + match receiver.await { + Ok(response) => match serde_json::from_value::(response) { + Ok(value) => Ok(value), + Err(error) => MmError::err(AskForDataError::DeserializationError(error)), + }, + Err(error) => MmError::err(AskForDataError::Internal(format!( + "Sender channel is not alive. {error}" + ))), + } + } +} + +#[derive(Clone, Debug, Deserialize)] +pub struct SendAskedDataRequest { + data_id: usize, + data: serde_json::Value, +} + +#[derive(Debug, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum SendAskedDataError { + #[display(fmt = "No data was asked for id={}", _0)] + NotFound(usize), + #[display(fmt = "Internal error: {}", _0)] + Internal(String), +} + +impl HttpStatusCode for SendAskedDataError { + fn status_code(&self) -> StatusCode { + match self { + SendAskedDataError::NotFound(_) => StatusCode::NOT_FOUND, + SendAskedDataError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +pub async fn send_asked_data_rpc( + ctx: MmArc, + asked_data: SendAskedDataRequest, +) -> Result> { + let mut awaiting_asks = ctx.data_asker.awaiting_asks.lock().await; + awaiting_asks.clear_expired_entries(); + + match awaiting_asks.remove(&asked_data.data_id) { + Some(sender) => { + sender.send(asked_data.data).map_to_mm(|_| { + SendAskedDataError::Internal("Receiver channel is not alive. Most likely timed out.".to_owned()) + })?; + Ok(true) + }, + None => MmError::err(SendAskedDataError::NotFound(asked_data.data_id)), + } +} + +#[cfg(test)] +mod tests { + use crate::mm_ctx::MmCtxBuilder; + use common::block_on; + use common::executor::Timer; + use instant::Duration; + use serde::Deserialize; + use serde_json::json; + use std::thread; + + #[test] + fn simulate_ask_and_send_data() { + let ctx = MmCtxBuilder::new().into_mm_arc(); + let ctx_clone = ctx.clone(); + + #[derive(Clone, Debug, Deserialize)] + struct DummyType { + name: String, + } + + thread::scope(|scope| { + scope.spawn(move || { + let output: DummyType = + block_on(ctx.ask_for_data("TEST", serde_json::Value::Null, Duration::from_secs(3))).unwrap(); + let output2: DummyType = + block_on(ctx.ask_for_data("TEST", serde_json::Value::Null, Duration::from_secs(3))).unwrap(); + + // Assert values sent from the other thread. + assert_eq!(&output.name, "Onur"); + assert_eq!(&output2.name, "Reggi"); + }); + + scope.spawn(move || { + // Wait until we ask for data from the other thread. + common::block_on(Timer::sleep(1.)); + + let data = super::SendAskedDataRequest { + data_id: 0, + data: json!({ + "name": "Onur".to_owned() + }), + }; + + block_on(super::send_asked_data_rpc(ctx_clone.clone(), data)).unwrap(); + + // Wait until we ask for data from the other thread. + common::block_on(Timer::sleep(1.)); + + let data = super::SendAskedDataRequest { + data_id: 1, + data: json!({ + "name": "Reggi".to_owned() + }), + }; + + block_on(super::send_asked_data_rpc(ctx_clone, data)).unwrap(); + }); + }); + } +} diff --git a/mm2src/mm2_core/src/lib.rs b/mm2src/mm2_core/src/lib.rs index 3eb5ecc6ae..98425279c2 100644 --- a/mm2src/mm2_core/src/lib.rs +++ b/mm2src/mm2_core/src/lib.rs @@ -1,6 +1,7 @@ use derive_more::Display; use rand::{thread_rng, Rng}; +pub mod data_asker; pub mod event_dispatcher; pub mod mm_ctx; diff --git a/mm2src/mm2_core/src/mm_ctx.rs b/mm2src/mm2_core/src/mm_ctx.rs index 85857f07a8..af0a6adece 100644 --- a/mm2src/mm2_core/src/mm_ctx.rs +++ b/mm2src/mm2_core/src/mm_ctx.rs @@ -20,6 +20,8 @@ use std::future::Future; use std::ops::Deref; use std::sync::{Arc, Mutex}; +use crate::data_asker::DataAsker; + cfg_wasm32! { use mm2_rpc::wasm_rpc::WasmRpcSender; use crate::DbNamespaceId; @@ -76,6 +78,8 @@ pub struct MmCtx { pub rpc_started: Constructible, /// Controller for continuously streaming data using streaming channels of `mm2_event_stream`. pub stream_channel_controller: Controller, + /// Data transfer bridge between server and client where server (which is the mm2 runtime) initiates the request. + pub(crate) data_asker: DataAsker, /// Configuration of event streaming used for SSE. pub event_stream_configuration: Option, /// True if the MarketMaker instance needs to stop. @@ -150,6 +154,7 @@ impl MmCtx { initialized: Constructible::default(), rpc_started: Constructible::default(), stream_channel_controller: Controller::new(), + data_asker: DataAsker::default(), event_stream_configuration: None, stop: Constructible::default(), ffi_handle: Constructible::default(), diff --git a/mm2src/mm2_event_stream/src/lib.rs b/mm2src/mm2_event_stream/src/lib.rs index 2d0cb6bda0..1dc15bcd53 100644 --- a/mm2src/mm2_event_stream/src/lib.rs +++ b/mm2src/mm2_event_stream/src/lib.rs @@ -35,6 +35,7 @@ impl Event { #[derive(Deserialize, Eq, Hash, PartialEq)] pub enum EventName { /// Indicates a change in the balance of a coin. + #[serde(rename = "COIN_BALANCE")] CoinBalance, /// Event triggered at regular intervals to indicate that the system is operational. HEARTBEAT, diff --git a/mm2src/mm2_main/src/lp_swap/taker_swap.rs b/mm2src/mm2_main/src/lp_swap/taker_swap.rs index 2114a63691..ac3164a58f 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_swap.rs @@ -1262,11 +1262,11 @@ impl TakerSwap { } async fn send_taker_fee(&self) -> Result<(Option, Vec), String> { - let timeout = self.r().data.started_at + self.r().data.lock_duration / 3; + let expire_at = self.r().data.started_at + self.r().data.lock_duration / 3; let now = now_sec(); - if now > timeout { + if now > expire_at { return Ok((Some(TakerSwapCommand::Finish), vec![ - TakerSwapEvent::TakerFeeSendFailed(ERRL!("Timeout {} > {}", now, timeout).into()), + TakerSwapEvent::TakerFeeSendFailed(ERRL!("Timeout {} > {}", now, expire_at).into()), ])); } @@ -1274,7 +1274,7 @@ impl TakerSwap { dex_fee_amount_from_taker_coin(self.taker_coin.deref(), &self.r().data.maker_coin, &self.taker_amount); let fee_tx = self .taker_coin - .send_taker_fee(&DEX_FEE_ADDR_RAW_PUBKEY, fee_amount, self.uuid.as_bytes()) + .send_taker_fee(&DEX_FEE_ADDR_RAW_PUBKEY, fee_amount, self.uuid.as_bytes(), expire_at) .compat() .await; let transaction = match fee_tx { diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index 25dcad017d..6c288f5967 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -13,7 +13,7 @@ use crate::{mm2::lp_stats::{add_node_to_version_stat, remove_node_from_version_s mm2::rpc::lp_commands::{get_public_key, get_public_key_hash, get_shared_db_id, trezor_connection_status}}; use coins::eth::EthCoin; use coins::my_tx_history_v2::my_tx_history_v2_rpc; -use coins::rpc_command::tendermint::{ibc_chains, ibc_transfer_channels, ibc_withdraw}; +use coins::rpc_command::tendermint::{ibc_chains, ibc_transfer_channels}; use coins::rpc_command::{account_balance::account_balance, get_current_mtp::get_current_mtp_rpc, get_enabled_coins::get_enabled_coins, @@ -51,6 +51,7 @@ use common::log::{error, warn}; use common::HttpStatusCode; use futures::Future as Future03; use http::Response; +use mm2_core::data_asker::send_asked_data_rpc; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_rpc::mm_protocol::{MmRpcBuilder, MmRpcRequest, MmRpcVersion}; @@ -207,10 +208,10 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, update_version_stat_collection).await, "verify_message" => handle_mmrpc(ctx, request, verify_message).await, "withdraw" => handle_mmrpc(ctx, request, withdraw).await, - "ibc_withdraw" => handle_mmrpc(ctx, request, ibc_withdraw).await, "ibc_chains" => handle_mmrpc(ctx, request, ibc_chains).await, "ibc_transfer_channels" => handle_mmrpc(ctx, request, ibc_transfer_channels).await, "withdraw_nft" => handle_mmrpc(ctx, request, withdraw_nft).await, + "send_asked_data" => handle_mmrpc(ctx, request, send_asked_data_rpc).await, "z_coin_tx_history" => handle_mmrpc(ctx, request, coins::my_tx_history_v2::z_coin_tx_history_rpc).await, #[cfg(not(target_arch = "wasm32"))] native_only_methods => match native_only_methods { diff --git a/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs b/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs index c198371f61..d17ef9cbf5 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs @@ -2475,9 +2475,11 @@ fn test_maker_order_should_not_kick_start_and_appear_in_orderbook_if_balance_is_ )) .wait() .unwrap(); - coin.send_raw_tx(&hex::encode(&withdraw.tx_hex.0)).wait().unwrap(); + coin.send_raw_tx(&hex::encode(&withdraw.tx.tx_hex().unwrap().0)) + .wait() + .unwrap(); let confirm_payment_input = ConfirmPaymentInput { - payment_tx: withdraw.tx_hex.0, + payment_tx: withdraw.tx.tx_hex().unwrap().0.to_owned(), confirmations: 1, requires_nota: false, wait_until: wait_until_sec(10), diff --git a/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs b/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs index 857141e2d9..edd8b8fa78 100644 --- a/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs @@ -1102,7 +1102,7 @@ fn test_get_max_taker_vol_and_trade_with_dynamic_trade_fee(coin: QtumCoin, priv_ let dex_fee = dex_fee_amount("QTUM", "MYCOIN", &expected_max_taker_vol, &qtum_min_tx_amount); let _taker_fee_tx = coin - .send_taker_fee(&DEX_FEE_ADDR_RAW_PUBKEY, dex_fee, &[]) + .send_taker_fee(&DEX_FEE_ADDR_RAW_PUBKEY, dex_fee, &[], timelock) .wait() .expect("!send_taker_fee"); let taker_payment_args = SendPaymentArgs { @@ -1740,7 +1740,12 @@ fn test_send_taker_fee_qtum() { let amount = BigDecimal::from_str("0.01").unwrap(); let tx = coin - .send_taker_fee(&DEX_FEE_ADDR_RAW_PUBKEY, DexFee::Standard(amount.clone().into()), &[]) + .send_taker_fee( + &DEX_FEE_ADDR_RAW_PUBKEY, + DexFee::Standard(amount.clone().into()), + &[], + 0, + ) .wait() .expect("!send_taker_fee"); assert!(matches!(tx, TransactionEnum::UtxoTx(_)), "Expected UtxoTx"); @@ -1767,7 +1772,12 @@ fn test_send_taker_fee_qrc20() { let amount = BigDecimal::from_str("0.01").unwrap(); let tx = coin - .send_taker_fee(&DEX_FEE_ADDR_RAW_PUBKEY, DexFee::Standard(amount.clone().into()), &[]) + .send_taker_fee( + &DEX_FEE_ADDR_RAW_PUBKEY, + DexFee::Standard(amount.clone().into()), + &[], + 0, + ) .wait() .expect("!send_taker_fee"); assert!(matches!(tx, TransactionEnum::UtxoTx(_)), "Expected UtxoTx"); diff --git a/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs b/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs index e3a84a749d..08f3da6173 100644 --- a/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs @@ -1203,7 +1203,12 @@ fn test_watcher_validate_taker_fee_utxo() { let fee_amount = dex_fee_amount_from_taker_coin(&taker_coin, maker_coin.ticker(), &taker_amount); let taker_fee = taker_coin - .send_taker_fee(&DEX_FEE_ADDR_RAW_PUBKEY, fee_amount, Uuid::new_v4().as_bytes()) + .send_taker_fee( + &DEX_FEE_ADDR_RAW_PUBKEY, + fee_amount, + Uuid::new_v4().as_bytes(), + lock_duration, + ) .wait() .unwrap(); @@ -1324,7 +1329,12 @@ fn test_watcher_validate_taker_fee_eth() { let taker_amount = MmNumber::from((1, 1)); let fee_amount = dex_fee_amount_from_taker_coin(&taker_coin, "ETH", &taker_amount); let taker_fee = taker_coin - .send_taker_fee(&DEX_FEE_ADDR_RAW_PUBKEY, fee_amount, Uuid::new_v4().as_bytes()) + .send_taker_fee( + &DEX_FEE_ADDR_RAW_PUBKEY, + fee_amount, + Uuid::new_v4().as_bytes(), + lock_duration, + ) .wait() .unwrap(); @@ -1426,7 +1436,12 @@ fn test_watcher_validate_taker_fee_erc20() { let taker_amount = MmNumber::from((1, 1)); let fee_amount = dex_fee_amount_from_taker_coin(&taker_coin, "ETH", &taker_amount); let taker_fee = taker_coin - .send_taker_fee(&DEX_FEE_ADDR_RAW_PUBKEY, fee_amount, Uuid::new_v4().as_bytes()) + .send_taker_fee( + &DEX_FEE_ADDR_RAW_PUBKEY, + fee_amount, + Uuid::new_v4().as_bytes(), + lock_duration, + ) .wait() .unwrap(); diff --git a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs index 78833da08f..a40da2336e 100644 --- a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs +++ b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs @@ -6413,7 +6413,10 @@ mod trezor_tests { None, )) .expect("withdraw must end successfully"); - log!("tx_hex={}", serde_json::to_string(&tx_details.tx_hex).unwrap()); + log!( + "tx_hex={}", + serde_json::to_string(&tx_details.tx.tx_hex().unwrap()).unwrap() + ); } /// Helper to init trezor and wait for completion @@ -6626,7 +6629,10 @@ mod trezor_tests { }), )) .expect("withdraw must end successfully"); - log!("tx_hex={}", serde_json::to_string(&tx_details.tx_hex).unwrap()); + log!( + "tx_hex={}", + serde_json::to_string(&tx_details.tx.tx_hex().unwrap()).unwrap() + ); // create a non-default address expected as "m/44'/1'/0'/0/1" (must be topped up already) let new_addr_params: GetNewAddressParams = serde_json::from_value(json!({ @@ -6653,7 +6659,10 @@ mod trezor_tests { }), )) .expect("withdraw must end successfully"); - log!("tx_hex={}", serde_json::to_string(&tx_details.tx_hex).unwrap()); + log!( + "tx_hex={}", + serde_json::to_string(&tx_details.tx.tx_hex().unwrap()).unwrap() + ); // if you need to send the tx: /* let send_tx_res = block_on(send_raw_transaction(ctx, json!({ diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index 3c65716e2e..3e9014016e 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -2604,7 +2604,7 @@ pub async fn ibc_withdraw( let request = mm .rpc(&json!({ "userpass": mm.userpass, - "method": "ibc_withdraw", + "method": "withdraw", "mmrpc": "2.0", "params": { "ibc_source_channel": source_channel, @@ -2616,7 +2616,7 @@ pub async fn ibc_withdraw( })) .await .unwrap(); - assert_eq!(request.0, StatusCode::OK, "'ibc_withdraw' failed: {}", request.1); + assert_eq!(request.0, StatusCode::OK, "'withdraw' failed: {}", request.1); let json: Json = json::from_str(&request.1).unwrap(); json::from_value(json["result"].clone()).unwrap() From 29c48bb3a57cde0976ce41d5f47ffed3fa110542 Mon Sep 17 00:00:00 2001 From: dimxy Date: Tue, 14 May 2024 18:20:26 +0500 Subject: [PATCH 06/20] fix(swap): use tmp file for swap and order files (#2118) This avoids concurrent reading/writing by enabling .tmp file for swap and order files. This fix should not create a problem for two mm2 instances running on the same node as both would use different directories for their data, derived from different listening IP-addresses. --- mm2src/mm2_main/src/lp_ordermatch/my_orders_storage.rs | 2 +- mm2src/mm2_main/src/lp_swap/saved_swap.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mm2src/mm2_main/src/lp_ordermatch/my_orders_storage.rs b/mm2src/mm2_main/src/lp_ordermatch/my_orders_storage.rs index 38a144952f..677af4be71 100644 --- a/mm2src/mm2_main/src/lp_ordermatch/my_orders_storage.rs +++ b/mm2src/mm2_main/src/lp_ordermatch/my_orders_storage.rs @@ -212,7 +212,7 @@ mod native_impl { my_taker_order_file_path, my_taker_orders_dir}; use mm2_io::fs::{read_dir_json, read_json, remove_file_async, write_json, FsJsonError}; - const USE_TMP_FILE: bool = false; + const USE_TMP_FILE: bool = true; impl From for MyOrdersError { fn from(fs: FsJsonError) -> Self { diff --git a/mm2src/mm2_main/src/lp_swap/saved_swap.rs b/mm2src/mm2_main/src/lp_swap/saved_swap.rs index d5333bae17..1531ceb28b 100644 --- a/mm2src/mm2_main/src/lp_swap/saved_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/saved_swap.rs @@ -189,7 +189,7 @@ mod native_impl { use crate::mm2::lp_swap::{my_swap_file_path, my_swaps_dir}; use mm2_io::fs::{read_dir_json, read_json, write_json, FsJsonError}; - const USE_TMP_FILE: bool = false; + const USE_TMP_FILE: bool = true; impl From for SavedSwapError { fn from(fs: FsJsonError) -> Self { From df6ab98b3b2ee84918bfc0bf1bc03d1e1f219985 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Wed, 15 May 2024 21:42:05 +0300 Subject: [PATCH 07/20] fix(p2pk): show and spend P2PK balance (#2053) UTXO P2PK balance is shown as part of the P2PKH/Legacy address balance, it can be also spent in withdraws and swaps. --- mm2src/coins/lightning/ln_events.rs | 11 - mm2src/coins/lp_coins.rs | 2 +- mm2src/coins/qrc20.rs | 5 - mm2src/coins/qrc20/qrc20_tests.rs | 26 +- mm2src/coins/utxo.rs | 67 ++- mm2src/coins/utxo/bchd_grpc.rs | 9 + mm2src/coins/utxo/qtum.rs | 17 +- mm2src/coins/utxo/qtum_delegation.rs | 13 +- mm2src/coins/utxo/rpc_clients.rs | 158 +++--- mm2src/coins/utxo/slp.rs | 21 +- .../utxo/utxo_builder/utxo_coin_builder.rs | 6 +- mm2src/coins/utxo/utxo_common.rs | 129 ++--- mm2src/coins/utxo/utxo_common_tests.rs | 14 +- mm2src/coins/utxo/utxo_tests.rs | 264 ++++++++-- mm2src/coins/utxo/utxo_withdraw.rs | 19 +- mm2src/coins/utxo_signer/src/lib.rs | 15 +- mm2src/coins/utxo_signer/src/sign_params.rs | 15 +- mm2src/coins/utxo_signer/src/with_key_pair.rs | 67 ++- mm2src/coins/z_coin/z_htlc.rs | 3 +- mm2src/crypto/src/privkey.rs | 2 +- mm2src/mm2_bitcoin/keys/src/address.rs | 103 ++-- .../keys/src/address/address_builder.rs | 67 ++- mm2src/mm2_bitcoin/keys/src/lib.rs | 2 +- mm2src/mm2_bitcoin/keys/src/public.rs | 2 +- mm2src/mm2_bitcoin/script/src/sign.rs | 8 +- mm2src/mm2_main/src/lp_native_dex.rs | 2 +- mm2src/mm2_main/src/lp_network.rs | 2 +- mm2src/mm2_main/src/lp_ordermatch.rs | 2 +- mm2src/mm2_main/src/lp_swap.rs | 2 +- mm2src/mm2_main/src/mm2.rs | 2 +- mm2src/mm2_main/src/rpc.rs | 2 +- .../src/rpc/lp_commands/lp_commands_legacy.rs | 2 +- .../tests/docker_tests/docker_tests_common.rs | 9 +- .../tests/docker_tests/docker_tests_inner.rs | 4 + .../tests/mm2_tests/mm2_tests_inner.rs | 492 ++++-------------- 35 files changed, 706 insertions(+), 858 deletions(-) diff --git a/mm2src/coins/lightning/ln_events.rs b/mm2src/coins/lightning/ln_events.rs index 3a761cc2b3..d6c78f9ad9 100644 --- a/mm2src/coins/lightning/ln_events.rs +++ b/mm2src/coins/lightning/ln_events.rs @@ -2,7 +2,6 @@ use super::*; use crate::lightning::ln_db::{DBChannelDetails, HTLCStatus, LightningDB, PaymentType}; use crate::lightning::ln_errors::{SaveChannelClosingError, SaveChannelClosingResult}; use crate::lightning::ln_sql::SqliteLightningDB; -use crate::utxo::UtxoCommonOps; use bitcoin::blockdata::script::Script; use bitcoin::blockdata::transaction::Transaction; use bitcoin::consensus::encode::serialize_hex; @@ -209,25 +208,15 @@ async fn sign_funding_transaction( }; unsigned.outputs[0].script_pubkey = output_script_pubkey.to_bytes().into(); - let my_address = coin - .as_ref() - .derivation_method - .single_addr_or_err() - .await - .map_err(|e| SignFundingTransactionError::Internal(e.to_string()))?; let key_pair = coin .as_ref() .priv_key_policy .activated_key_or_err() .map_err(|e| SignFundingTransactionError::Internal(e.to_string()))?; - let prev_script = coin - .script_for_address(&my_address) - .map_err(|e| SignFundingTransactionError::Internal(e.to_string()))?; let signed = sign_tx( unsigned, key_pair, - prev_script, SignatureVersion::WitnessV0, coin.as_ref().conf.fork_id, ) diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 6f49f6ab17..4457fec6e6 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -1,5 +1,5 @@ /****************************************************************************** - * Copyright © 2023 Pampex LTD and TillyHK LTD * + * Copyright © 2023 Pampex LTD and TillyHK LTD * * * * See the CONTRIBUTOR-LICENSE-AGREEMENT, COPYING, LICENSE-COPYRIGHT-NOTICE * * and DEVELOPER-CERTIFICATE-OF-ORIGIN files in the LEGAL directory in * diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index 8c2367d14f..c26a9f38ac 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -539,16 +539,11 @@ impl Qrc20Coin { .build() .await?; - let my_address = self.utxo.derivation_method.single_addr_or_err().await?; let key_pair = self.utxo.priv_key_policy.activated_key_or_err()?; - let prev_script = self - .script_for_address(&my_address) - .map_err(|e| Qrc20GenTxError::InvalidAddress(e.to_string()))?; let signed = sign_tx( unsigned, key_pair, - prev_script, self.utxo.conf.signature_version, self.utxo.conf.fork_id, )?; diff --git a/mm2src/coins/qrc20/qrc20_tests.rs b/mm2src/coins/qrc20/qrc20_tests.rs index 7c3a0aba27..b3cccc5dc9 100644 --- a/mm2src/coins/qrc20/qrc20_tests.rs +++ b/mm2src/coins/qrc20/qrc20_tests.rs @@ -74,14 +74,15 @@ fn test_withdraw_to_p2sh_address_should_fail() { let p2sh_address = AddressBuilder::new( UtxoAddressFormat::Standard, - block_on(coin.as_ref().derivation_method.unwrap_single_addr()) - .hash() - .clone(), *block_on(coin.as_ref().derivation_method.unwrap_single_addr()).checksum_type(), coin.as_ref().conf.address_prefixes.clone(), coin.as_ref().conf.bech32_hrp.clone(), ) - .as_sh() + .as_sh( + block_on(coin.as_ref().derivation_method.unwrap_single_addr()) + .hash() + .clone(), + ) .build() .expect("valid address props"); @@ -103,6 +104,13 @@ fn test_withdraw_to_p2sh_address_should_fail() { #[cfg(not(target_arch = "wasm32"))] #[test] fn test_withdraw_impl_fee_details() { + // priv_key of qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG + let priv_key = [ + 3, 98, 177, 3, 108, 39, 234, 144, 131, 178, 103, 103, 127, 80, 230, 166, 53, 68, 147, 215, 42, 216, 144, 72, + 172, 110, 180, 13, 123, 179, 10, 49, + ]; + let (_ctx, coin) = qrc20_coin_for_test(priv_key, None); + Qrc20Coin::get_unspent_ordered_list.mock_safe(|coin, _| { let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); let unspents = vec![UnspentInfo { @@ -112,17 +120,13 @@ fn test_withdraw_impl_fee_details() { }, value: 1000000000, height: Default::default(), + script: coin + .script_for_address(&block_on(coin.as_ref().derivation_method.unwrap_single_addr())) + .unwrap(), }]; MockResult::Return(Box::pin(futures::future::ok((unspents, cache)))) }); - // priv_key of qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG - let priv_key = [ - 3, 98, 177, 3, 108, 39, 234, 144, 131, 178, 103, 103, 127, 80, 230, 166, 53, 68, 147, 215, 42, 216, 144, 72, - 172, 110, 180, 13, 123, 179, 10, 49, - ]; - let (_ctx, coin) = qrc20_coin_for_test(priv_key, None); - let withdraw_req = WithdrawRequest { amount: 10.into(), from: None, diff --git a/mm2src/coins/utxo.rs b/mm2src/coins/utxo.rs index 6cf25fcf7f..7d13e63398 100644 --- a/mm2src/coins/utxo.rs +++ b/mm2src/coins/utxo.rs @@ -1,5 +1,5 @@ /****************************************************************************** - * Copyright © 2023 Pampex LTD and TillyHK LTD * + * Copyright © 2023 Pampex LTD and TillyHK LTD * * * * See the CONTRIBUTOR-LICENSE-AGREEMENT, COPYING, LICENSE-COPYRIGHT-NOTICE * * and DEVELOPER-CERTIFICATE-OF-ORIGIN files in the LEGAL directory in * @@ -309,21 +309,20 @@ pub struct CachedUnspentInfo { pub value: u64, } -impl From for CachedUnspentInfo { - fn from(unspent: UnspentInfo) -> CachedUnspentInfo { +impl CachedUnspentInfo { + fn from_unspent_info(unspent: &UnspentInfo) -> CachedUnspentInfo { CachedUnspentInfo { outpoint: unspent.outpoint, value: unspent.value, } } -} -impl From for UnspentInfo { - fn from(cached: CachedUnspentInfo) -> UnspentInfo { + fn to_unspent_info(&self, script: Script) -> UnspentInfo { UnspentInfo { - outpoint: cached.outpoint, - value: cached.value, + outpoint: self.outpoint, + value: self.value, height: None, + script, } } } @@ -350,22 +349,17 @@ impl RecentlySpentOutPoints { } pub fn add_spent(&mut self, inputs: Vec, spend_tx_hash: H256, outputs: Vec) { - let inputs: HashSet<_> = inputs.into_iter().map(From::from).collect(); + let inputs: HashSet<_> = inputs.iter().map(CachedUnspentInfo::from_unspent_info).collect(); let to_replace: HashSet<_> = outputs - .iter() + .into_iter() .enumerate() - .filter_map(|(index, output)| { - if output.script_pubkey == self.for_script_pubkey { - Some(CachedUnspentInfo { - outpoint: OutPoint { - hash: spend_tx_hash, - index: index as u32, - }, - value: output.value, - }) - } else { - None - } + .filter(|(_, output)| output.script_pubkey == self.for_script_pubkey) + .map(|(index, output)| CachedUnspentInfo { + outpoint: OutPoint { + hash: spend_tx_hash, + index: index as u32, + }, + value: output.value, }) .collect(); @@ -400,13 +394,14 @@ impl RecentlySpentOutPoints { pub fn replace_spent_outputs_with_cache(&self, mut outputs: HashSet) -> HashSet { let mut replacement_unspents = HashSet::new(); outputs.retain(|unspent| { - let outs = self.input_to_output_map.get(&unspent.clone().into()); + let outs = self + .input_to_output_map + .get(&CachedUnspentInfo::from_unspent_info(unspent)); + match outs { Some(outs) => { - for out in outs.iter() { - if !replacement_unspents.contains(out) { - replacement_unspents.insert(out.clone()); - } + for out in outs { + replacement_unspents.insert(out.clone()); } false }, @@ -416,7 +411,11 @@ impl RecentlySpentOutPoints { if replacement_unspents.is_empty() { return outputs; } - outputs.extend(replacement_unspents.into_iter().map(From::from)); + outputs.extend( + replacement_unspents + .iter() + .map(|cached| cached.to_unspent_info(self.for_script_pubkey.clone().into())), + ); self.replace_spent_outputs_with_cache(outputs) } } @@ -1795,6 +1794,7 @@ where outpoint: input.previous_output, value: input.amount, height: None, + script: input.prev_script.clone(), }) .collect(); @@ -1803,12 +1803,9 @@ where _ => coin.as_ref().conf.signature_version, }; - let prev_script = utxo_common::output_script_checked(coin.as_ref(), &my_address) - .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; let signed = try_tx_s!(sign_tx( unsigned, key_pair, - prev_script, signature_version, coin.as_ref().conf.fork_id )); @@ -1830,6 +1827,9 @@ pub fn output_script(address: &Address) -> Result { } } +/// Builds transaction output script for a legacy P2PK address +pub fn output_script_p2pk(pubkey: &Public) -> Script { Builder::build_p2pk(pubkey) } + pub fn address_by_conf_and_pubkey_str( coin: &str, conf: &Json, @@ -1854,16 +1854,15 @@ pub fn address_by_conf_and_pubkey_str( let conf_builder = UtxoConfBuilder::new(conf, ¶ms, coin); let utxo_conf = try_s!(conf_builder.build()); let pubkey_bytes = try_s!(hex::decode(pubkey)); - let hash = dhash160(&pubkey_bytes); + let pubkey = try_s!(Public::from_slice(&pubkey_bytes)); let address = AddressBuilder::new( addr_format, - hash.into(), utxo_conf.checksum_type, utxo_conf.address_prefixes, utxo_conf.bech32_hrp, ) - .as_pkh() + .as_pkh_from_pk(pubkey) .build()?; address.display_address() } diff --git a/mm2src/coins/utxo/bchd_grpc.rs b/mm2src/coins/utxo/bchd_grpc.rs index 6017f3b9c0..da240508c6 100644 --- a/mm2src/coins/utxo/bchd_grpc.rs +++ b/mm2src/coins/utxo/bchd_grpc.rs @@ -260,6 +260,7 @@ mod bchd_grpc_tests { }, value: 0, height: None, + script: Vec::new().into(), }, slp_amount: 1000, }, @@ -271,6 +272,7 @@ mod bchd_grpc_tests { }, value: 0, height: None, + script: Vec::new().into(), }, slp_amount: 8999, }, @@ -294,6 +296,7 @@ mod bchd_grpc_tests { }, value: 0, height: None, + script: Vec::new().into(), }, slp_amount: 1000, }, @@ -305,6 +308,7 @@ mod bchd_grpc_tests { }, value: 0, height: None, + script: Vec::new().into(), }, slp_amount: 8999, }, @@ -316,6 +320,7 @@ mod bchd_grpc_tests { }, value: 0, height: None, + script: Vec::new().into(), }, slp_amount: 8999, }, @@ -341,6 +346,7 @@ mod bchd_grpc_tests { }, value: 0, height: None, + script: Vec::new().into(), }, slp_amount: 999, }; @@ -353,6 +359,7 @@ mod bchd_grpc_tests { }, value: 0, height: None, + script: Vec::new().into(), }, slp_amount: 8999, }]; @@ -386,6 +393,7 @@ mod bchd_grpc_tests { }, value: 0, height: None, + script: Vec::new().into(), }, slp_amount: 1000, }, @@ -397,6 +405,7 @@ mod bchd_grpc_tests { }, value: 0, height: None, + script: Vec::new().into(), }, slp_amount: 8999, }, diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index e46ee28330..200f13dfb7 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -146,12 +146,11 @@ pub trait QtumBasedCoin: UtxoCommonOps + MarketCoinOps { let utxo = self.as_ref(); AddressBuilder::new( self.addr_format().clone(), - AddressHashEnum::AddressHash(address.0.into()), utxo.conf.checksum_type, utxo.conf.address_prefixes.clone(), utxo.conf.bech32_hrp.clone(), ) - .as_pkh() + .as_pkh(AddressHashEnum::AddressHash(address.0.into())) .build() .expect("valid address props") } @@ -161,20 +160,6 @@ pub trait QtumBasedCoin: UtxoCommonOps + MarketCoinOps { contract_addr_from_utxo_addr(my_address).mm_err(Qrc20AddressError::from) } - fn utxo_address_from_contract_addr(&self, address: H160) -> Address { - let utxo = self.as_ref(); - AddressBuilder::new( - self.addr_format().clone(), - AddressHashEnum::AddressHash(address.0.into()), - utxo.conf.checksum_type, - utxo.conf.address_prefixes.clone(), - utxo.conf.bech32_hrp.clone(), - ) - .as_pkh() - .build() - .expect("valid address props") - } - fn contract_address_from_raw_pubkey(&self, pubkey: &[u8]) -> Result { let utxo = self.as_ref(); let qtum_address = try_s!(utxo_common::address_from_raw_pubkey( diff --git a/mm2src/coins/utxo/qtum_delegation.rs b/mm2src/coins/utxo/qtum_delegation.rs index b7cfff7c1c..ad9aec86e1 100644 --- a/mm2src/coins/utxo/qtum_delegation.rs +++ b/mm2src/coins/utxo/qtum_delegation.rs @@ -186,7 +186,7 @@ impl QtumCoin { .map(|padded_staker_address_hex| padded_staker_address_hex.trim_start_matches('0')) }) { let hash = H160::from_str(raw).map_to_mm(|e| StakingInfosError::Internal(e.to_string()))?; - let address = self.utxo_address_from_contract_addr(hash); + let address = self.utxo_addr_from_contract_addr(hash); Ok(Some(address.to_string())) } else { Ok(None) @@ -290,16 +290,7 @@ impl QtumCoin { DelegationError::from_generate_tx_error(gen_tx_error, self.ticker().to_string(), utxo.decimals) })?; - let prev_script = self - .script_for_address(&my_address) - .map_err(|e| DelegationError::InternalError(e.to_string()))?; - let signed = sign_tx( - unsigned, - key_pair, - prev_script, - utxo.conf.signature_version, - utxo.conf.fork_id, - )?; + let signed = sign_tx(unsigned, key_pair, utxo.conf.signature_version, utxo.conf.fork_id)?; let miner_fee = data.fee_amount + data.unused_change; let generated_tx = GenerateQrc20TxResult { diff --git a/mm2src/coins/utxo/rpc_clients.rs b/mm2src/coins/utxo/rpc_clients.rs index b67946e40a..e8f86bc8e0 100644 --- a/mm2src/coins/utxo/rpc_clients.rs +++ b/mm2src/coins/utxo/rpc_clients.rs @@ -2,15 +2,15 @@ #![cfg_attr(target_arch = "wasm32", allow(dead_code))] use crate::utxo::utxo_block_header_storage::BlockHeaderStorage; -use crate::utxo::{output_script, sat_from_big_decimal, GetBlockHeaderError, GetConfirmedTxError, GetTxError, - GetTxHeightError, ScripthashNotification}; +use crate::utxo::{output_script, output_script_p2pk, sat_from_big_decimal, GetBlockHeaderError, GetConfirmedTxError, + GetTxError, GetTxHeightError, NumConversResult, ScripthashNotification}; use crate::{big_decimal_from_sat_unsigned, MyAddressError, NumConversError, RpcTransportEventHandler, RpcTransportEventHandlerShared}; use async_trait::async_trait; use chain::{BlockHeader, BlockHeaderBits, BlockHeaderNonce, OutPoint, Transaction as UtxoTx, TransactionInput, TxHashAlgo}; use common::custom_futures::{select_ok_sequential, timeout::FutureTimerExt}; -use common::custom_iter::{CollectInto, TryIntoGroupMap}; +use common::custom_iter::TryIntoGroupMap; use common::executor::{abortable_queue, abortable_queue::AbortableQueue, AbortableSystem, SpawnFuture, Timer}; use common::jsonrpc_client::{JsonRpcBatchClient, JsonRpcBatchResponse, JsonRpcClient, JsonRpcError, JsonRpcErrorType, JsonRpcId, JsonRpcMultiClient, JsonRpcRemoteAddr, JsonRpcRequest, JsonRpcRequestEnum, @@ -37,6 +37,7 @@ use mm2_number::{BigDecimal, BigInt, MmNumber}; use mm2_rpc::data::legacy::ElectrumProtocol; #[cfg(test)] use mocktopus::macros::*; use rpc::v1::types::{Bytes as BytesJson, Transaction as RpcTransaction, H256 as H256Json}; +use script::Script; use serde_json::{self as json, Value as Json}; use serialization::{deserialize, serialize, serialize_with_flags, CoinVariant, CompactInteger, Reader, SERIALIZE_TRANSACTION_WITNESS}; @@ -256,19 +257,34 @@ pub struct UnspentInfo { /// The block height transaction mined in. /// Note None if the transaction is not mined yet. pub height: Option, + /// The script pubkey of the UTXO + pub script: Script, } -impl From for UnspentInfo { - fn from(electrum: ElectrumUnspent) -> UnspentInfo { +impl UnspentInfo { + fn from_electrum(unspent: ElectrumUnspent, script: Script) -> UnspentInfo { UnspentInfo { outpoint: OutPoint { - hash: electrum.tx_hash.reversed().into(), - index: electrum.tx_pos, + hash: unspent.tx_hash.reversed().into(), + index: unspent.tx_pos, }, - value: electrum.value, - height: electrum.height, + value: unspent.value, + height: unspent.height, + script, } } + + fn from_native(unspent: NativeUnspent, decimals: u8, height: Option) -> NumConversResult { + Ok(UnspentInfo { + outpoint: OutPoint { + hash: unspent.txid.reversed().into(), + index: unspent.vout, + }, + value: sat_from_big_decimal(&unspent.amount.to_decimal(), decimals)?, + height, + script: unspent.script_pub_key.0.into(), + }) + } } #[derive(Debug, PartialEq)] @@ -758,20 +774,10 @@ impl UtxoRpcClientOps for NativeClient { .list_unspent_impl(0, std::i32::MAX, vec![address.to_string()]) .map_to_mm_fut(UtxoRpcError::from) .and_then(move |unspents| { - let unspents: UtxoRpcResult> = unspents - .into_iter() - .map(|unspent| { - Ok(UnspentInfo { - outpoint: OutPoint { - hash: unspent.txid.reversed().into(), - index: unspent.vout, - }, - value: sat_from_big_decimal(&unspent.amount.to_decimal(), decimals)?, - height: None, - }) - }) - .collect(); unspents + .into_iter() + .map(|unspent| Ok(UnspentInfo::from_native(unspent, decimals, None)?)) + .collect::>() }); Box::new(fut) } @@ -799,14 +805,7 @@ impl UtxoRpcClientOps for NativeClient { UtxoRpcError::InvalidResponse(format!("Unexpected address '{}'", unspent.address)) })? .clone(); - let unspent_info = UnspentInfo { - outpoint: OutPoint { - hash: unspent.txid.reversed().into(), - index: unspent.vout, - }, - value: sat_from_big_decimal(&unspent.amount.to_decimal(), decimals)?, - height: None, - }; + let unspent_info = UnspentInfo::from_native(unspent, decimals, None)?; Ok((orig_address, unspent_info)) }) // Collect `(Address, UnspentInfo)` items into `HashMap>` grouped by the addresses. @@ -2237,48 +2236,67 @@ impl ElectrumClient { #[cfg_attr(test, mockable)] impl UtxoRpcClientOps for ElectrumClient { fn list_unspent(&self, address: &Address, _decimals: u8) -> UtxoRpcFut> { - let script = try_f!(output_script(address)); - let script_hash = electrum_script_hash(&script); - Box::new( - self.scripthash_list_unspent(&hex::encode(script_hash)) - .map_to_mm_fut(UtxoRpcError::from) - .map(move |unspents| { + let mut output_scripts = vec![try_f!(output_script(address))]; + + // If the plain pubkey is available, fetch the UTXOs found in P2PK outputs as well (if any). + if let Some(pubkey) = address.pubkey() { + let p2pk_output_script = output_script_p2pk(pubkey); + output_scripts.push(p2pk_output_script); + } + + let this = self.clone(); + let fut = async move { + let hashes = output_scripts + .iter() + .map(|s| hex::encode(electrum_script_hash(s))) + .collect(); + let unspents = this.scripthash_list_unspent_batch(hashes).compat().await?; + + let unspents = unspents + .into_iter() + .zip(output_scripts) + .flat_map(|(unspents, output_script)| { unspents - .iter() - .map(|unspent| UnspentInfo { - outpoint: OutPoint { - hash: unspent.tx_hash.reversed().into(), - index: unspent.tx_pos, - }, - value: unspent.value, - height: unspent.height, - }) - .collect() - }), - ) + .into_iter() + .map(move |unspent| UnspentInfo::from_electrum(unspent, output_script.clone())) + }) + .collect(); + Ok(unspents) + }; + + Box::new(fut.boxed().compat()) } fn list_unspent_group(&self, addresses: Vec
, _decimals: u8) -> UtxoRpcFut { - let script_hashes = try_f!(addresses + let output_scripts = try_f!(addresses .iter() - .map(|addr| { - let script = output_script(addr)?; - let script_hash = electrum_script_hash(&script); - Ok(hex::encode(script_hash)) - }) + .map(output_script) .collect::, keys::Error>>()); let this = self.clone(); let fut = async move { - let unspents = this.scripthash_list_unspent_batch(script_hashes).compat().await?; + let hashes = output_scripts + .iter() + .map(|s| hex::encode(electrum_script_hash(s))) + .collect(); + let unspents = this.scripthash_list_unspent_batch(hashes).compat().await?; + + let unspents: Vec> = unspents + .into_iter() + .zip(output_scripts) + .map(|(unspents, output_script)| { + unspents + .into_iter() + .map(|unspent| UnspentInfo::from_electrum(unspent, output_script.clone())) + .collect() + }) + .collect(); let unspent_map = addresses .into_iter() // `scripthash_list_unspent_batch` returns `ScriptHashUnspents` elements in the same order in which they were requested. // So we can zip `addresses` and `unspents` into one iterator. .zip(unspents) - // Map `(Address, Vec)` pairs into `(Address, Vec)`. - .map(|(address, electrum_unspents)| (address, electrum_unspents.collect_into())) .collect(); Ok(unspent_map) }; @@ -2346,12 +2364,26 @@ impl UtxoRpcClientOps for ElectrumClient { rpc_req!(self, "blockchain.scripthash.get_balance").into(), JsonRpcErrorType::Internal(err.to_string()) ))); - let hash = electrum_script_hash(&output_script); - let hash_str = hex::encode(hash); - Box::new( - self.scripthash_get_balance(&hash_str) - .map(move |electrum_balance| electrum_balance.to_big_decimal(decimals)), - ) + let mut hashes = vec![hex::encode(electrum_script_hash(&output_script))]; + + // If the plain pubkey is available, fetch the balance found in P2PK output as well (if any). + if let Some(pubkey) = address.pubkey() { + let p2pk_output_script = output_script_p2pk(pubkey); + hashes.push(hex::encode(electrum_script_hash(&p2pk_output_script))); + } + + let this = self.clone(); + let fut = async move { + Ok(this + .scripthash_get_balances(hashes) + .compat() + .await? + .into_iter() + .fold(BigDecimal::from(0), |sum, electrum_balance| { + sum + electrum_balance.to_big_decimal(decimals) + })) + }; + Box::new(fut.boxed().compat()) } fn display_balances(&self, addresses: Vec
, decimals: u8) -> UtxoRpcFut> { diff --git a/mm2src/coins/utxo/slp.rs b/mm2src/coins/utxo/slp.rs index eb5f3c1a70..eaf25f7994 100644 --- a/mm2src/coins/utxo/slp.rs +++ b/mm2src/coins/utxo/slp.rs @@ -455,10 +455,11 @@ impl SlpToken { bch_unspent: UnspentInfo { outpoint: OutPoint { hash: tx.hash(), - index: 1, + index: SLP_SWAP_VOUT as u32, }, value: 0, height: None, + script: tx.outputs[SLP_SWAP_VOUT].script_pubkey.clone().into(), }, slp_amount: slp_satoshis, }; @@ -555,8 +556,9 @@ impl SlpToken { hash: tx.hash(), index: SLP_SWAP_VOUT as u32, }, - value: tx.outputs[1].value, + value: tx.outputs[SLP_SWAP_VOUT].value, height: None, + script: tx.outputs[SLP_SWAP_VOUT].script_pubkey.clone().into(), }, slp_amount, }; @@ -606,8 +608,9 @@ impl SlpToken { hash: tx.hash(), index: SLP_SWAP_VOUT as u32, }, - value: tx.outputs[1].value, + value: tx.outputs[SLP_SWAP_VOUT].value, height: None, + script: tx.outputs[SLP_SWAP_VOUT].script_pubkey.clone().into(), }, slp_amount, }; @@ -677,7 +680,6 @@ impl SlpToken { &unsigned, i, my_key_pair, - my_script_pubkey.clone(), self.platform_coin.as_ref().conf.signature_version, self.platform_coin.as_ref().conf.fork_id, ) @@ -1622,12 +1624,6 @@ impl MmCoin for SlpToken { )); } - let my_address = coin - .platform_coin - .as_ref() - .derivation_method - .single_addr_or_err() - .await?; let key_pair = coin.platform_coin.as_ref().priv_key_policy.activated_key_or_err()?; let address = CashAddress::decode(&req.to).map_to_mm(WithdrawError::InvalidAddress)?; @@ -1694,14 +1690,9 @@ impl MmCoin for SlpToken { WithdrawError::from_generate_tx_error(gen_tx_error, coin.platform_ticker().into(), platform_decimals) })?; - let prev_script = coin - .platform_coin - .script_for_address(&my_address) - .map_err(|e| WithdrawError::InvalidAddress(e.to_string()))?; let signed = sign_tx( unsigned, key_pair, - prev_script, coin.platform_conf().signature_version, coin.platform_conf().fork_id, )?; diff --git a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs index edf34ebb65..0b07d1596c 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs @@ -171,12 +171,11 @@ pub trait UtxoFieldsWithIguanaSecretBuilder: UtxoCoinBuilderCommonOps { let addr_format = self.address_format()?; let my_address = AddressBuilder::new( addr_format, - AddressHashEnum::AddressHash(key_pair.public().address_hash()), conf.checksum_type, conf.address_prefixes.clone(), conf.bech32_hrp.clone(), ) - .as_pkh() + .as_pkh_from_pk(*key_pair.public()) .build() .map_to_mm(UtxoCoinBuildError::Internal)?; let derivation_method = DerivationMethod::SingleAddress(my_address); @@ -276,12 +275,11 @@ where let addr_format = builder.address_format()?; let my_address = AddressBuilder::new( addr_format, - AddressHashEnum::AddressHash(key_pair.public().address_hash()), conf.checksum_type, conf.address_prefixes.clone(), conf.bech32_hrp.clone(), ) - .as_pkh() + .as_pkh_from_pk(*key_pair.public()) .build() .map_to_mm(UtxoCoinBuildError::Internal)?; diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index 615d046b28..60031278d3 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -310,19 +310,18 @@ pub fn addresses_from_script(coin: &T, script: &Script) -> Res let (addr_format, build_option) = match dst.kind { AddressScriptType::P2PKH => ( coin.addr_format_for_standard_scripts(), - AddressBuilderOption::BuildAsPubkeyHash, + AddressBuilderOption::PubkeyHash(dst.hash), ), AddressScriptType::P2SH => ( coin.addr_format_for_standard_scripts(), - AddressBuilderOption::BuildAsScriptHash, + AddressBuilderOption::ScriptHash(dst.hash), ), - AddressScriptType::P2WPKH => (UtxoAddressFormat::Segwit, AddressBuilderOption::BuildAsPubkeyHash), - AddressScriptType::P2WSH => (UtxoAddressFormat::Segwit, AddressBuilderOption::BuildAsScriptHash), + AddressScriptType::P2WPKH => (UtxoAddressFormat::Segwit, AddressBuilderOption::PubkeyHash(dst.hash)), + AddressScriptType::P2WSH => (UtxoAddressFormat::Segwit, AddressBuilderOption::ScriptHash(dst.hash)), }; AddressBuilder::new( addr_format, - dst.hash, conf.checksum_type, conf.address_prefixes.clone(), conf.bech32_hrp.clone(), @@ -514,9 +513,9 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { .inputs .extend(inputs.into_iter().map(|input| UnsignedTransactionInput { previous_output: input.outpoint, + prev_script: input.script, sequence: SEQUENCE_FINAL, amount: input.value, - witness: Vec::new(), })); self } @@ -589,9 +588,9 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { } if let Some(min_relay) = self.min_relay_fee { if self.tx_fee < min_relay { - outputs_plus_fee -= self.tx_fee; - outputs_plus_fee += min_relay; - self.tx_fee = min_relay; + let fee_diff = min_relay - self.tx_fee; + outputs_plus_fee += fee_diff; + self.tx_fee += fee_diff; } } self.sum_inputs >= outputs_plus_fee @@ -681,17 +680,17 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { }; for utxo in self.available_inputs.clone() { + if self.update_fee_and_check_completeness(from.addr_format(), &actual_tx_fee) { + break; + } + self.tx.inputs.push(UnsignedTransactionInput { previous_output: utxo.outpoint, + prev_script: utxo.script, sequence: SEQUENCE_FINAL, amount: utxo.value, - witness: vec![], }); self.sum_inputs += utxo.value; - - if self.update_fee_and_check_completeness(from.addr_format(), &actual_tx_fee) { - break; - } } match self.fee_policy { @@ -769,12 +768,13 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { .find(|input| input.previous_output == utxo.outpoint) { input.amount = utxo.value; + input.prev_script = utxo.script; } else { self.tx.inputs.push(UnsignedTransactionInput { previous_output: utxo.outpoint, + prev_script: utxo.script, sequence: SEQUENCE_FINAL, amount: utxo.value, - witness: vec![], }); } } @@ -913,8 +913,8 @@ async fn p2sh_spending_tx_preimage( hash: prev_tx.hash(), index: DEFAULT_SWAP_VOUT as u32, }, + prev_script: Vec::new().into(), amount, - witness: Vec::new(), }], outputs, expiry_height: 0, @@ -1212,12 +1212,11 @@ pub async fn sign_and_send_taker_funding_spend( ); let payment_address = AddressBuilder::new( UtxoAddressFormat::Standard, - AddressHashEnum::AddressHash(dhash160(&payment_redeem_script)), coin.as_ref().conf.checksum_type, coin.as_ref().conf.address_prefixes.clone(), coin.as_ref().conf.bech32_hrp.clone(), ) - .as_sh() + .as_sh(dhash160(&payment_redeem_script).into()) .build() .map_err(TransactionErr::Plain)?; let payment_address_str = payment_address.to_string(); @@ -2443,12 +2442,11 @@ pub fn check_if_my_payment_sent( UtxoRpcClientEnum::Native(client) => { let target_addr = AddressBuilder::new( coin.addr_format_for_standard_scripts(), - hash.into(), coin.as_ref().conf.checksum_type, coin.as_ref().conf.address_prefixes.clone(), coin.as_ref().conf.bech32_hrp.clone(), ) - .as_sh() + .as_sh(hash.into()) .build()?; let target_addr = target_addr.to_string(); let is_imported = try_s!(client.is_address_imported(&target_addr).await); @@ -2687,25 +2685,23 @@ pub fn send_raw_tx_bytes( } /// Helper to load unspent outputs from cache or rpc -/// also returns first previous scriptpubkey async fn get_unspents_for_inputs( coin: &UtxoCoinFields, inputs: &Vec, -) -> Result<(Option