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()