diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index a69fa8e391..5d00407873 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -3258,7 +3258,7 @@ impl MmCoin for EthCoin { }) } - fn get_receiver_trade_fee(&self, stage: FeeApproxStage) -> TradePreimageFut { + fn get_receiver_trade_fee(&self, _send_amount: BigDecimal, stage: FeeApproxStage) -> TradePreimageFut { let coin = self.clone(); let fut = async move { let gas_price = coin.get_gas_price().compat().await?; diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index f8588032d8..05556fe4cb 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -932,7 +932,7 @@ fn get_receiver_trade_preimage() { }; let actual = coin - .get_receiver_trade_fee(FeeApproxStage::WithoutApprox) + .get_receiver_trade_fee(Default::default(), FeeApproxStage::WithoutApprox) .wait() .expect("!get_sender_trade_fee"); assert_eq!(actual, expected_fee); diff --git a/mm2src/coins/lightning.rs b/mm2src/coins/lightning.rs index 8b6d9a5d95..92835e33d4 100644 --- a/mm2src/coins/lightning.rs +++ b/mm2src/coins/lightning.rs @@ -1056,7 +1056,7 @@ impl MmCoin 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 get_receiver_trade_fee(&self, _stage: FeeApproxStage) -> TradePreimageFut { + fn get_receiver_trade_fee(&self, _send_amount: BigDecimal, _stage: FeeApproxStage) -> TradePreimageFut { Box::new(futures01::future::ok(TradeFee { coin: self.ticker().to_owned(), amount: Default::default(), diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index c2466f7ecd..6654909419 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -217,8 +217,8 @@ use rpc_command::{init_account_balance::{AccountBalanceTaskManager, AccountBalan init_withdraw::{WithdrawTaskManager, WithdrawTaskManagerShared}}; pub mod tendermint; -use tendermint::{CosmosTransaction, TendermintCoin, TendermintFeeDetails, TendermintProtocolInfo, TendermintToken, - TendermintTokenProtocolInfo}; +use tendermint::{CosmosTransaction, CustomTendermintMsgType, TendermintCoin, TendermintFeeDetails, + TendermintProtocolInfo, TendermintToken, TendermintTokenProtocolInfo}; #[doc(hidden)] #[allow(unused_variables)] @@ -1036,16 +1036,18 @@ impl KmdRewardsDetails { } } -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[derive(Default, Clone, Debug, Deserialize, PartialEq, Serialize)] pub enum TransactionType { StakingDelegation, RemoveDelegation, + #[default] StandardTransfer, TokenTransfer(BytesJson), -} - -impl Default for TransactionType { - fn default() -> Self { TransactionType::StandardTransfer } + FeeForTokenTx, + CustomTendermintMsg { + msg_type: CustomTendermintMsgType, + token_id: Option, + }, } /// Transaction details @@ -1908,7 +1910,7 @@ pub trait MmCoin: SwapOps + WatcherOps + MarketCoinOps + Send + Sync + 'static { ) -> TradePreimageResult; /// Get fee to be paid by receiver per whole swap and check if the wallet has sufficient balance to pay the fee. - fn get_receiver_trade_fee(&self, stage: FeeApproxStage) -> TradePreimageFut; + fn get_receiver_trade_fee(&self, send_amount: BigDecimal, stage: FeeApproxStage) -> TradePreimageFut; /// Get transaction fee the Taker has to pay to send a `TakerFee` transaction and check if the wallet has sufficient balance to pay the fee. async fn get_fee_to_send_taker_fee( diff --git a/mm2src/coins/my_tx_history_v2.rs b/mm2src/coins/my_tx_history_v2.rs index 1667e08fd8..01f659caca 100644 --- a/mm2src/coins/my_tx_history_v2.rs +++ b/mm2src/coins/my_tx_history_v2.rs @@ -1,8 +1,10 @@ use crate::hd_wallet::{AddressDerivingError, InvalidBip44ChainError}; +use crate::tendermint::{TENDERMINT_ASSET_PROTOCOL_TYPE, TENDERMINT_COIN_PROTOCOL_TYPE}; use crate::tx_history_storage::{CreateTxHistoryStorageError, FilteringAddresses, GetTxHistoryFilters, TxHistoryStorageBuilder, WalletId}; -use crate::{lp_coinfind_or_err, BlockHeightAndTime, CoinFindError, HDAccountAddressId, HistorySyncState, MmCoin, - MmCoinEnum, Transaction, TransactionDetails, TransactionType, TxFeeDetails, UtxoRpcError}; +use crate::utxo::utxo_common::big_decimal_from_sat_unsigned; +use crate::{coin_conf, lp_coinfind_or_err, BlockHeightAndTime, CoinFindError, HDAccountAddressId, HistorySyncState, + MmCoin, MmCoinEnum, Transaction, TransactionDetails, TransactionType, TxFeeDetails, UtxoRpcError}; use async_trait::async_trait; use bitcrypto::sha256; use common::{calc_total_pages, ten, HttpStatusCode, PagingOptionsEnum, StatusCode}; @@ -13,6 +15,7 @@ use keys::{Address, CashAddress}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::BigDecimal; +use num_traits::ToPrimitive; use rpc::v1::types::{Bytes as BytesJson, ToTxHash}; use std::collections::HashSet; @@ -69,6 +72,9 @@ pub trait TxHistoryStorage: Send + Sync + 'static { internal_id: &BytesJson, ) -> Result, MmError>; + /// Gets the highest block_height from the selected wallet's history + async fn get_highest_block_height(&self, wallet_id: &WalletId) -> Result, MmError>; + /// Returns whether the history contains unconfirmed transactions. async fn history_contains_unconfirmed_txes( &self, @@ -215,8 +221,18 @@ impl<'a, Addr: Clone + DisplayAddress + Eq + std::hash::Hash, Tx: Transaction> T bytes_for_hash.extend_from_slice(&token_id.0); sha256(&bytes_for_hash).to_vec().into() }, + TransactionType::CustomTendermintMsg { token_id, .. } => { + if let Some(token_id) = token_id { + let mut bytes_for_hash = tx_hash.0.clone(); + bytes_for_hash.extend_from_slice(&token_id.0); + sha256(&bytes_for_hash).to_vec().into() + } else { + tx_hash.clone() + } + }, TransactionType::StakingDelegation | TransactionType::RemoveDelegation + | TransactionType::FeeForTokenTx | TransactionType::StandardTransfer => tx_hash.clone(), }; @@ -375,6 +391,8 @@ pub async fn my_tx_history_v2_rpc( MmCoinEnum::SlpToken(slp_token) => my_tx_history_v2_impl(ctx, &slp_token, request).await, MmCoinEnum::UtxoCoin(utxo) => my_tx_history_v2_impl(ctx, &utxo, request).await, MmCoinEnum::QtumCoin(qtum) => my_tx_history_v2_impl(ctx, &qtum, request).await, + MmCoinEnum::Tendermint(tendermint) => my_tx_history_v2_impl(ctx, &tendermint, request).await, + MmCoinEnum::TendermintToken(tendermint_token) => my_tx_history_v2_impl(ctx, &tendermint_token, request).await, other => MmError::err(MyTxHistoryErrorV2::NotSupportedFor(other.ticker().to_owned())), } } @@ -406,6 +424,10 @@ where .get_history(&wallet_id, filters, request.paging_options.clone(), request.limit) .await?; + let coin_conf = coin_conf(&ctx, coin.ticker()); + let protocol_type = coin_conf["protocol"]["type"].as_str().unwrap_or_default(); + let decimals = coin.decimals(); + let transactions = history .transactions .into_iter() @@ -414,6 +436,59 @@ where if details.coin != request.coin { details.coin = request.coin.clone(); } + + // TODO + // !! temporary solution !! + // for tendermint, tx_history_v2 implementation doesn't include amount parsing logic. + // therefore, re-mapping is required + match protocol_type { + TENDERMINT_COIN_PROTOCOL_TYPE | TENDERMINT_ASSET_PROTOCOL_TYPE => { + // TODO + // see this https://github.com/KomodoPlatform/atomicDEX-API/pull/1526#discussion_r1037001780 + if let Some(TxFeeDetails::Utxo(fee)) = &mut details.fee_details { + let mapped_fee = crate::tendermint::TendermintFeeDetails { + // We make sure this is filled in `tendermint_tx_history_v2` + coin: fee.coin.as_ref().expect("can't be empty").to_owned(), + amount: fee.amount.clone(), + gas_limit: crate::tendermint::GAS_LIMIT_DEFAULT, + // ignored anyway + uamount: 0, + }; + details.fee_details = Some(TxFeeDetails::Tendermint(mapped_fee)); + } + + match &details.transaction_type { + // Amount mappings are by-passed when `TransactionType` is `FeeForTokenTx` + TransactionType::FeeForTokenTx => {}, + _ => { + // In order to use error result instead of panicking, we should do an extra iteration above this map. + // Because all the values are inserted by u64 convertion in tx_history_v2 implementation, using `panic` + // shouldn't harm. + + let u_total_amount = details.total_amount.to_u64().unwrap_or_else(|| { + panic!("Parsing '{}' into u64 should not fail", details.total_amount) + }); + details.total_amount = big_decimal_from_sat_unsigned(u_total_amount, decimals); + + let u_spent_by_me = details.spent_by_me.to_u64().unwrap_or_else(|| { + panic!("Parsing '{}' into u64 should not fail", details.spent_by_me) + }); + details.spent_by_me = big_decimal_from_sat_unsigned(u_spent_by_me, decimals); + + let u_received_by_me = details.received_by_me.to_u64().unwrap_or_else(|| { + panic!("Parsing '{}' into u64 should not fail", details.received_by_me) + }); + details.received_by_me = big_decimal_from_sat_unsigned(u_received_by_me, decimals); + + // Because this can be negative values, no need to read and parse + // this since it's always 0 from tx_history_v2 implementation. + details.my_balance_change = &details.received_by_me - &details.spent_by_me; + }, + } + }, + _ => {}, + }; + let confirmations = if details.block_height == 0 || details.block_height > current_block { 0 } else { diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index c6c6bbe2a5..7d25031a2a 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -1316,7 +1316,7 @@ impl MmCoin for Qrc20Coin { }) } - fn get_receiver_trade_fee(&self, stage: FeeApproxStage) -> TradePreimageFut { + fn get_receiver_trade_fee(&self, _send_amount: BigDecimal, stage: FeeApproxStage) -> TradePreimageFut { let selfi = self.clone(); let fut = async move { // pass the dummy params diff --git a/mm2src/coins/qrc20/qrc20_tests.rs b/mm2src/coins/qrc20/qrc20_tests.rs index bbe24b857c..8b677e706b 100644 --- a/mm2src/coins/qrc20/qrc20_tests.rs +++ b/mm2src/coins/qrc20/qrc20_tests.rs @@ -876,7 +876,7 @@ fn test_receiver_trade_preimage() { check_tx_fee(&coin, ActualTxFee::FixedPerKb(EXPECTED_TX_FEE as u64)); let actual = coin - .get_receiver_trade_fee(FeeApproxStage::WithoutApprox) + .get_receiver_trade_fee(Default::default(), FeeApproxStage::WithoutApprox) .wait() .expect("!get_receiver_trade_fee"); // only one contract call should be included into the expected trade fee diff --git a/mm2src/coins/solana.rs b/mm2src/coins/solana.rs index e0a329e4fc..1a86277733 100644 --- a/mm2src/coins/solana.rs +++ b/mm2src/coins/solana.rs @@ -672,18 +672,20 @@ impl MmCoin for SolanaCoin { async fn get_sender_trade_fee( &self, - value: TradePreimageValue, - stage: FeeApproxStage, + _value: TradePreimageValue, + _stage: FeeApproxStage, ) -> TradePreimageResult { unimplemented!() } - fn get_receiver_trade_fee(&self, _stage: FeeApproxStage) -> TradePreimageFut { unimplemented!() } + fn get_receiver_trade_fee(&self, _send_amount: BigDecimal, _stage: FeeApproxStage) -> TradePreimageFut { + unimplemented!() + } async fn get_fee_to_send_taker_fee( &self, - dex_fee_amount: BigDecimal, - stage: FeeApproxStage, + _dex_fee_amount: BigDecimal, + _stage: FeeApproxStage, ) -> TradePreimageResult { unimplemented!() } diff --git a/mm2src/coins/solana/spl.rs b/mm2src/coins/solana/spl.rs index 194811c0e2..13faf86ca3 100644 --- a/mm2src/coins/solana/spl.rs +++ b/mm2src/coins/solana/spl.rs @@ -474,7 +474,9 @@ impl MmCoin for SplToken { unimplemented!() } - fn get_receiver_trade_fee(&self, _stage: FeeApproxStage) -> TradePreimageFut { unimplemented!() } + fn get_receiver_trade_fee(&self, _send_amount: BigDecimal, _stage: FeeApproxStage) -> TradePreimageFut { + unimplemented!() + } async fn get_fee_to_send_taker_fee( &self, diff --git a/mm2src/coins/tendermint/iris/htlc.rs b/mm2src/coins/tendermint/iris/htlc.rs index 4da2d0f96f..226c1794fc 100644 --- a/mm2src/coins/tendermint/iris/htlc.rs +++ b/mm2src/coins/tendermint/iris/htlc.rs @@ -21,6 +21,8 @@ // check this page https://www.irisnet.org/docs/get-started/testnet.html#faucet use super::htlc_proto::{ClaimHtlcProtoRep, CreateHtlcProtoRep}; + +use crate::tendermint::type_urls::{CLAIM_HTLC_TYPE_URL, CREATE_HTLC_TYPE_URL}; use cosmrs::{tx::{Msg, MsgProto}, AccountId, Coin, ErrorReport}; use std::convert::TryFrom; @@ -30,9 +32,6 @@ pub const HTLC_STATE_OPEN: i32 = 0; pub const HTLC_STATE_COMPLETED: i32 = 1; pub const HTLC_STATE_REFUNDED: i32 = 2; -const CREATE_HTLC_TYPE_URL: &str = "/irismod.htlc.MsgCreateHTLC"; -const CLAIM_HTLC_TYPE_URL: &str = "/irismod.htlc.MsgClaimHTLC"; - #[allow(dead_code)] pub(crate) struct IrisHtlc { /// Generated HTLC's ID. diff --git a/mm2src/coins/tendermint/iris/htlc_proto.rs b/mm2src/coins/tendermint/iris/htlc_proto.rs index 8555c0915e..7bf9b5281b 100644 --- a/mm2src/coins/tendermint/iris/htlc_proto.rs +++ b/mm2src/coins/tendermint/iris/htlc_proto.rs @@ -38,14 +38,14 @@ pub(crate) struct QueryHtlcRequestProto { #[derive(prost::Enumeration, Debug)] #[repr(i32)] -pub(crate) enum HtlcState { +pub enum HtlcState { Open = 0, Completed = 1, Refunded = 2, } #[derive(prost::Message)] -pub(crate) struct HtlcProto { +pub struct HtlcProto { #[prost(string, tag = "1")] pub(crate) id: prost::alloc::string::String, #[prost(string, tag = "2")] diff --git a/mm2src/coins/tendermint/iris/mod.rs b/mm2src/coins/tendermint/iris/mod.rs new file mode 100644 index 0000000000..00c493c504 --- /dev/null +++ b/mm2src/coins/tendermint/iris/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod htlc; +pub(crate) mod htlc_proto; diff --git a/mm2src/coins/tendermint/mod.rs b/mm2src/coins/tendermint/mod.rs index eb6a1af909..6e904c4567 100644 --- a/mm2src/coins/tendermint/mod.rs +++ b/mm2src/coins/tendermint/mod.rs @@ -2,11 +2,29 @@ // Useful resources // https://docs.cosmos.network/ -#[path = "iris/htlc.rs"] mod htlc; -#[path = "iris/htlc_proto.rs"] mod htlc_proto; +mod iris; +mod rpc; mod tendermint_coin; -#[cfg(not(target_arch = "wasm32"))] mod tendermint_native_rpc; mod tendermint_token; -#[cfg(target_arch = "wasm32")] mod tendermint_wasm_rpc; +pub mod tendermint_tx_history_v2; + pub use tendermint_coin::*; pub use tendermint_token::*; + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub enum CustomTendermintMsgType { + /// Create HTLC as sender + SendHtlcAmount, + /// Claim HTLC as reciever + ClaimHtlcAmount, + /// Claim HTLC for reciever + SignClaimHtlc, +} + +pub(crate) const TENDERMINT_COIN_PROTOCOL_TYPE: &str = "TENDERMINT"; +pub(crate) const TENDERMINT_ASSET_PROTOCOL_TYPE: &str = "TENDERMINTTOKEN"; + +pub(crate) mod type_urls { + pub(crate) const CREATE_HTLC_TYPE_URL: &str = "/irismod.htlc.MsgCreateHTLC"; + pub(crate) const CLAIM_HTLC_TYPE_URL: &str = "/irismod.htlc.MsgClaimHTLC"; +} diff --git a/mm2src/coins/tendermint/rpc/mod.rs b/mm2src/coins/tendermint/rpc/mod.rs new file mode 100644 index 0000000000..bd34834ce9 --- /dev/null +++ b/mm2src/coins/tendermint/rpc/mod.rs @@ -0,0 +1,23 @@ +#[cfg(not(target_arch = "wasm32"))] mod tendermint_native_rpc; +#[cfg(not(target_arch = "wasm32"))] +pub use tendermint_native_rpc::*; + +#[cfg(target_arch = "wasm32")] mod tendermint_wasm_rpc; +#[cfg(target_arch = "wasm32")] pub use tendermint_wasm_rpc::*; + +pub(crate) const TX_SUCCESS_CODE: u32 = 0; + +#[repr(u8)] +pub enum TendermintResultOrder { + Ascending = 1, + Descending, +} + +impl From for Order { + fn from(order: TendermintResultOrder) -> Self { + match order { + TendermintResultOrder::Ascending => Self::Ascending, + TendermintResultOrder::Descending => Self::Descending, + } + } +} diff --git a/mm2src/coins/tendermint/tendermint_native_rpc.rs b/mm2src/coins/tendermint/rpc/tendermint_native_rpc.rs similarity index 99% rename from mm2src/coins/tendermint/tendermint_native_rpc.rs rename to mm2src/coins/tendermint/rpc/tendermint_native_rpc.rs index 88466e7f89..dde181b3e3 100644 --- a/mm2src/coins/tendermint/tendermint_native_rpc.rs +++ b/mm2src/coins/tendermint/rpc/tendermint_native_rpc.rs @@ -15,8 +15,7 @@ use tendermint_rpc::endpoint::*; pub use tendermint_rpc::endpoint::{abci_query::Request as AbciRequest, health::Request as HealthRequest, tx_search::Request as TxSearchRequest}; use tendermint_rpc::Paging; -pub use tendermint_rpc::{query::Query as TendermintQuery, Error, Order as TendermintResultOrder, Scheme, - SimpleRequest, Url}; +pub use tendermint_rpc::{query::Query as TendermintQuery, Error, Order, Scheme, SimpleRequest, Url}; use tokio::time; /// Provides lightweight access to the Tendermint RPC. It gives access to all @@ -80,7 +79,7 @@ pub trait Client { query: TendermintQuery, page: u32, per_page: u8, - order: TendermintResultOrder, + order: Order, ) -> Result { self.perform(block_search::Request::new(query, page, per_page, order)) .await @@ -229,7 +228,7 @@ pub trait Client { prove: bool, page: u32, per_page: u8, - order: TendermintResultOrder, + order: Order, ) -> Result { self.perform(tx_search::Request::new(query, prove, page, per_page, order)) .await diff --git a/mm2src/coins/tendermint/tendermint_wasm_rpc.rs b/mm2src/coins/tendermint/rpc/tendermint_wasm_rpc.rs similarity index 92% rename from mm2src/coins/tendermint/tendermint_wasm_rpc.rs rename to mm2src/coins/tendermint/rpc/tendermint_wasm_rpc.rs index 8c072abe18..d0fc1a1f96 100644 --- a/mm2src/coins/tendermint/tendermint_wasm_rpc.rs +++ b/mm2src/coins/tendermint/rpc/tendermint_wasm_rpc.rs @@ -16,16 +16,16 @@ pub use tendermint_rpc::endpoint::{abci_query::{AbciQuery, Request as AbciReques use tendermint_rpc::error::Error as TendermintRpcError; pub use tendermint_rpc::query::Query as TendermintQuery; use tendermint_rpc::request::SimpleRequest; -pub use tendermint_rpc::Order as TendermintResultOrder; +pub use tendermint_rpc::Order; use tendermint_rpc::Response; #[derive(Debug, Clone)] -pub(super) struct HttpClient { +pub struct HttpClient { uri: String, } #[derive(Debug, Display)] -pub(super) enum HttpClientInitError { +pub(crate) enum HttpClientInitError { InvalidUri(InvalidUri), } @@ -34,7 +34,7 @@ impl From for HttpClientInitError { } #[derive(Debug, Display)] -pub(super) enum PerformError { +pub enum PerformError { TendermintRpc(TendermintRpcError), Slurp(SlurpError), #[display(fmt = "Request failed with status code {}, response {}", status_code, response)] @@ -53,12 +53,12 @@ impl From for PerformError { } impl HttpClient { - pub(super) fn new(url: &str) -> Result { + pub(crate) fn new(url: &str) -> Result { Uri::from_str(url)?; Ok(HttpClient { uri: url.to_owned() }) } - pub(super) async fn perform(&self, request: R) -> Result + pub(crate) async fn perform(&self, request: R) -> Result where R: SimpleRequest, { diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 79948267ef..d0ee6b9a19 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -1,10 +1,8 @@ -use super::htlc::{IrisHtlc, MsgCreateHtlc, HTLC_STATE_COMPLETED, HTLC_STATE_OPEN, HTLC_STATE_REFUNDED}; -#[cfg(not(target_arch = "wasm32"))] -use super::tendermint_native_rpc::*; -#[cfg(target_arch = "wasm32")] use super::tendermint_wasm_rpc::*; +use super::iris::htlc::{IrisHtlc, MsgClaimHtlc, MsgCreateHtlc, HTLC_STATE_COMPLETED, HTLC_STATE_OPEN, + HTLC_STATE_REFUNDED}; +use super::iris::htlc_proto::{CreateHtlcProtoRep, QueryHtlcRequestProto, QueryHtlcResponseProto}; +use super::rpc::*; use crate::coin_errors::{MyAddressError, ValidatePaymentError}; -use crate::tendermint::htlc::MsgClaimHtlc; -use crate::tendermint::htlc_proto::{CreateHtlcProtoRep, QueryHtlcRequestProto, QueryHtlcResponseProto}; use crate::utxo::sat_from_big_decimal; use crate::utxo::utxo_common::big_decimal_from_sat; use crate::{big_decimal_from_sat_unsigned, BalanceError, BalanceFut, BigDecimal, CheckIfMyPaymentSentArgs, @@ -13,19 +11,19 @@ use crate::{big_decimal_from_sat_unsigned, BalanceError, BalanceFut, BigDecimal, PrivKeyPolicyNotAllowed, RawTransactionError, RawTransactionFut, RawTransactionRequest, RawTransactionRes, SearchForSwapTxSpendInput, SendMakerPaymentArgs, SendMakerRefundsPaymentArgs, SendMakerSpendsTakerPaymentArgs, SendTakerPaymentArgs, SendTakerRefundsPaymentArgs, - SendTakerSpendsMakerPaymentArgs, SignatureError, SignatureResult, SwapOps, TradeFee, TradePreimageFut, - TradePreimageResult, TradePreimageValue, TransactionDetails, TransactionEnum, TransactionErr, - TransactionFut, TransactionType, TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, - ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, - ValidatePaymentFut, ValidatePaymentInput, VerificationError, VerificationResult, WatcherOps, - WatcherValidatePaymentInput, WithdrawError, WithdrawFut, WithdrawRequest}; + SendTakerSpendsMakerPaymentArgs, SignatureError, SignatureResult, SwapOps, TradeFee, TradePreimageError, + TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionDetails, TransactionEnum, + TransactionErr, TransactionFut, TransactionType, TxFeeDetails, TxMarshalingErr, + UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, + ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, VerificationError, VerificationResult, + WatcherOps, WatcherValidatePaymentInput, WithdrawError, WithdrawFut, WithdrawRequest}; use async_std::prelude::FutureExt as AsyncStdFutureExt; use async_trait::async_trait; use bitcrypto::{dhash160, sha256}; use common::executor::Timer; use common::executor::{abortable_queue::AbortableQueue, AbortableSystem}; use common::log::warn; -use common::{get_utc_timestamp, log, Future01CompatExt}; +use common::{get_utc_timestamp, log, now_ms, Future01CompatExt, DEX_FEE_ADDR_PUBKEY}; use cosmrs::bank::MsgSend; use cosmrs::crypto::secp256k1::SigningKey; use cosmrs::proto::cosmos::auth::v1beta1::{BaseAccount, QueryAccountRequest, QueryAccountResponse}; @@ -52,13 +50,14 @@ use mm2_err_handle::prelude::*; use mm2_number::MmNumber; use parking_lot::Mutex as PaMutex; use prost::{DecodeError, Message}; +use rand::{thread_rng, Rng}; 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::ops::Deref; use std::str::FromStr; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use std::time::Duration; use uuid::Uuid; @@ -88,10 +87,25 @@ pub(crate) const TX_DEFAULT_MEMO: &str = ""; const MAX_TIME_LOCK: i64 = 34560; const MIN_TIME_LOCK: i64 = 50; +#[async_trait] +pub trait TendermintCommons { + fn platform_denom(&self) -> String; + + fn set_history_sync_state(&self, new_state: HistorySyncState); + + async fn get_block_timestamp(&self, block: i64) -> MmResult, TendermintCoinRpcError>; + + async fn all_balances(&self) -> MmResult; + + async fn rpc_client(&self) -> MmResult; +} + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct TendermintFeeDetails { pub coin: String, pub amount: BigDecimal, + #[serde(skip)] + pub uamount: u64, pub gas_limit: u64, } @@ -106,12 +120,12 @@ pub struct TendermintProtocolInfo { #[derive(Clone)] pub struct ActivatedTokenInfo { - decimals: u8, - denom: Denom, + pub(crate) decimals: u8, + pub(crate) denom: Denom, } pub struct TendermintConf { - avg_block_time: u8, + avg_blocktime: u8, /// Derivation path of the coin. /// This derivation path consists of `purpose` and `coin_type` only /// where the full `BIP44` address has the following structure: @@ -121,15 +135,20 @@ pub struct TendermintConf { impl TendermintConf { pub fn try_from_json(ticker: &str, conf: &Json) -> MmResult { - let avg_block_time = conf["avg_block_time"].as_i64().unwrap_or(0); + let avg_blocktime = conf.get("avg_blocktime").or_mm_err(|| TendermintInitError { + ticker: ticker.to_string(), + kind: TendermintInitErrorKind::AvgBlockTimeMissing, + })?; - // `avg_block_time` can not be less than 1 OR bigger than 255(u8::MAX) - if avg_block_time < 1 || avg_block_time > std::u8::MAX as i64 { - return MmError::err(TendermintInitError { - ticker: ticker.to_string(), - kind: TendermintInitErrorKind::AvgBlockTimeMissingOrInvalid, - }); - } + let avg_blocktime = avg_blocktime.as_i64().or_mm_err(|| TendermintInitError { + ticker: ticker.to_string(), + kind: TendermintInitErrorKind::AvgBlockTimeInvalid, + })?; + + let avg_blocktime = u8::try_from(avg_blocktime).map_to_mm(|_| TendermintInitError { + ticker: ticker.to_string(), + kind: TendermintInitErrorKind::AvgBlockTimeInvalid, + })?; let derivation_path = json::from_value(conf["derivation_path"].clone()).map_to_mm(|e| TendermintInitError { ticker: ticker.to_string(), @@ -137,7 +156,7 @@ impl TendermintConf { })?; Ok(TendermintConf { - avg_block_time: avg_block_time as u8, + avg_blocktime, derivation_path, }) } @@ -145,24 +164,23 @@ impl TendermintConf { pub struct TendermintCoinImpl { ticker: String, - /// TODO - /// Test Vec instead of HttpClient and pick - /// better one in terms of performance & resource consumption on runtime. rpc_clients: Vec, + /// As seconds + avg_blocktime: u8, /// My address - avg_block_time: u8, pub account_id: AccountId, pub(super) account_prefix: String, priv_key: Vec, - decimals: u8, + pub(crate) decimals: u8, pub(super) denom: Denom, chain_id: ChainId, gas_price: Option, pub(super) sequence_lock: AsyncMutex<()>, - tokens_info: PaMutex>, + pub(crate) tokens_info: PaMutex>, /// This spawner is used to spawn coin's related futures that should be aborted on coin deactivation /// or on [`MmArc::stop`]. pub(super) abortable_system: AbortableQueue, + pub(crate) history_sync_state: Mutex, } #[derive(Clone)] @@ -195,8 +213,10 @@ pub enum TendermintInitErrorKind { ErrorDeserializingDerivationPath(String), PrivKeyPolicyNotAllowed(PrivKeyPolicyNotAllowed), RpcError(String), - #[display(fmt = "avg_block_time missing or invalid. Please provide it with min 1 or max 255 value.")] - AvgBlockTimeMissingOrInvalid, + #[display(fmt = "avg_blocktime is missing in coin configuration")] + AvgBlockTimeMissing, + #[display(fmt = "avg_blocktime must be in-between '0' and '255'.")] + AvgBlockTimeInvalid, } #[derive(Display, Debug)] @@ -234,6 +254,10 @@ impl From for ValidatePaymentError { } } +impl From for TradePreimageError { + fn from(err: TendermintCoinRpcError) -> Self { TradePreimageError::Transport(err.to_string()) } +} + #[cfg(not(target_arch = "wasm32"))] impl From for TendermintCoinRpcError { fn from(err: tendermint_rpc::Error) -> Self { TendermintCoinRpcError::PerformError(err.to_string()) } @@ -293,6 +317,7 @@ pub fn account_id_from_pubkey_hex(prefix: &str, pubkey: &str) -> MmResult, @@ -320,6 +345,62 @@ impl From for SearchForSwapTxSpendErr { fn from(e: DecodeError) -> Self { SearchForSwapTxSpendErr::Proto(e) } } +#[async_trait] +impl TendermintCommons for TendermintCoin { + fn platform_denom(&self) -> String { self.denom.to_string() } + + fn set_history_sync_state(&self, new_state: HistorySyncState) { + *self.history_sync_state.lock().unwrap() = new_state; + } + + async fn get_block_timestamp(&self, block: i64) -> MmResult, TendermintCoinRpcError> { + let block_response = self.get_block_by_height(block).await?; + let block_header = some_or_return_ok_none!(some_or_return_ok_none!(block_response.block).header); + let timestamp = some_or_return_ok_none!(block_header.time); + + Ok(u64::try_from(timestamp.seconds).ok()) + } + + async fn all_balances(&self) -> MmResult { + let platform_balance_denom = self.balance_for_denom(self.denom.to_string()).await?; + let platform_balance = big_decimal_from_sat_unsigned(platform_balance_denom, self.decimals); + let ibc_assets_info = self.tokens_info.lock().clone(); + + let mut result = AllBalancesResult { + platform_balance, + tokens_balances: HashMap::new(), + }; + for (ticker, info) in ibc_assets_info { + let balance_denom = self.balance_for_denom(info.denom.to_string()).await?; + let balance_decimal = big_decimal_from_sat_unsigned(balance_denom, info.decimals); + result.tokens_balances.insert(ticker, balance_decimal); + } + + Ok(result) + } + + // TODO + // Save one working client to the coin context, only try others once it doesn't + // work anymore. + // Also, try couple times more on health check errors. + async fn rpc_client(&self) -> MmResult { + for rpc_client in self.rpc_clients.iter() { + match rpc_client.perform(HealthRequest).timeout(Duration::from_secs(3)).await { + Ok(Ok(_)) => return Ok(rpc_client.clone()), + Ok(Err(e)) => log::warn!( + "Recieved error from Tendermint rpc node during health check. Error: {:?}", + e + ), + Err(_) => log::warn!("Tendermint rpc node: {:?} got timeout during health check", rpc_client), + }; + } + + MmError::err(TendermintCoinRpcError::PerformError( + "All the current rpc nodes are unavailable.".to_string(), + )) + } +} + impl TendermintCoin { pub async fn init( ctx: &MmArc, @@ -327,6 +408,7 @@ impl TendermintCoin { conf: TendermintConf, protocol_info: TendermintProtocolInfo, rpc_urls: Vec, + tx_history: bool, priv_key_policy: PrivKeyBuildPolicy, ) -> MmResult { if rpc_urls.is_empty() { @@ -368,6 +450,12 @@ impl TendermintCoin { kind: TendermintInitErrorKind::InvalidDenom(e.to_string()), })?; + let history_sync_state = if tx_history { + HistorySyncState::NotStarted + } else { + HistorySyncState::NotEnabled + }; + // Create an abortable system linked to the `MmCtx` so if the context is stopped via `MmArc::stop`, // all spawned futures related to `TendermintCoin` will be aborted as well. let abortable_system = ctx @@ -388,34 +476,14 @@ impl TendermintCoin { denom, chain_id, gas_price: protocol_info.gas_price, - avg_block_time: conf.avg_block_time, + avg_blocktime: conf.avg_blocktime, sequence_lock: AsyncMutex::new(()), tokens_info: PaMutex::new(HashMap::new()), abortable_system, + history_sync_state: Mutex::new(history_sync_state), }))) } - // TODO - // Save one working client to the coin context, only try others once it doesn't - // work anymore. - // Also, try couple times more on health check errors. - async fn rpc_client(&self) -> MmResult { - for rpc_client in self.rpc_clients.iter() { - match rpc_client.perform(HealthRequest).timeout(Duration::from_secs(3)).await { - Ok(Ok(_)) => return Ok(rpc_client.clone()), - Ok(Err(e)) => log::warn!( - "Recieved error from Tendermint rpc node during health check. Error: {:?}", - e - ), - Err(_) => log::warn!("Tendermint rpc node: {:?} got timeout during health check", rpc_client), - }; - } - - MmError::err(TendermintCoinRpcError::PerformError( - "All the current rpc nodes are unavailable.".to_string(), - )) - } - #[inline(always)] fn gas_price(&self) -> f64 { self.gas_price.unwrap_or(DEFAULT_GAS_PRICE) } @@ -521,13 +589,13 @@ impl TendermintCoin { ABCI_REQUEST_PROVE, ); - let response = self.rpc_client().await?.perform(request).await?; - let response = SimulateResponse::decode(response.response.value.as_slice())?; + let raw_response = self.rpc_client().await?.perform(request).await?; + let response = SimulateResponse::decode(raw_response.response.value.as_slice())?; let gas = response.gas_info.as_ref().ok_or_else(|| { TendermintCoinRpcError::InvalidResponse(format!( "Could not read gas_info. Invalid Response: {:?}", - response + raw_response )) })?; @@ -552,13 +620,13 @@ impl TendermintCoin { ABCI_REQUEST_PROVE, ); - let response = self.rpc_client().await?.perform(request).await?; - let response = SimulateResponse::decode(response.response.value.as_slice())?; + let raw_response = self.rpc_client().await?.perform(request).await?; + let response = SimulateResponse::decode(raw_response.response.value.as_slice())?; let gas = response.gas_info.as_ref().ok_or_else(|| { TendermintCoinRpcError::InvalidResponse(format!( "Could not read gas_info. Invalid Response: {:?}", - response + raw_response )) })?; @@ -608,24 +676,6 @@ impl TendermintCoin { .map_to_mm(|e| TendermintCoinRpcError::InvalidResponse(format!("balance is not u64, err {}", e))) } - pub async fn all_balances(&self) -> MmResult { - let platform_balance_denom = self.balance_for_denom(self.denom.to_string()).await?; - let platform_balance = big_decimal_from_sat_unsigned(platform_balance_denom, self.decimals); - let ibc_assets_info = self.tokens_info.lock().clone(); - - let mut result = AllBalancesResult { - platform_balance, - tokens_balances: HashMap::new(), - }; - for (ticker, info) in ibc_assets_info { - let balance_denom = self.balance_for_denom(info.denom.to_string()).await?; - let balance_decimal = big_decimal_from_sat_unsigned(balance_denom, info.decimals); - result.tokens_balances.insert(ticker, balance_decimal); - } - - Ok(result) - } - fn gen_create_htlc_tx( &self, denom: Denom, @@ -695,7 +745,7 @@ impl TendermintCoin { } fn estimate_blocks_from_duration(&self, duration: u64) -> i64 { - let estimated_time_lock = (duration / self.avg_block_time as u64) as i64; + let estimated_time_lock = (duration / self.avg_blocktime as u64) as i64; estimated_time_lock.clamp(MIN_TIME_LOCK, MAX_TIME_LOCK) } @@ -738,7 +788,13 @@ impl TendermintCoin { let response = try_s!( // Search single tx rpc_client - .perform(TxSearchRequest::new(q, false, 1, 1, TendermintResultOrder::Descending)) + .perform(TxSearchRequest::new( + q, + false, + 1, + 1, + TendermintResultOrder::Descending.into() + )) .await ); @@ -1037,7 +1093,7 @@ impl TendermintCoin { .or_mm_err(|| ValidatePaymentError::InvalidRpcResponse(format!("No HTLC data for {}", htlc_id)))?; match htlc_data.state { - 0 => Ok(()), + HTLC_STATE_OPEN => Ok(()), unexpected_state => MmError::err(ValidatePaymentError::UnexpectedPaymentState(format!( "{}", unexpected_state @@ -1047,6 +1103,114 @@ impl TendermintCoin { Box::new(fut.boxed().compat()) } + pub(super) async fn get_sender_trade_fee_for_denom( + &self, + ticker: String, + denom: Denom, + decimals: u8, + amount: BigDecimal, + ) -> TradePreimageResult { + const TIME_LOCK: u64 = 1750; + let sec: [u8; 32] = thread_rng().gen(); + 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())))?; + + let amount = sat_from_big_decimal(&amount, decimals)?; + + let create_htlc_tx = self + .gen_create_htlc_tx(denom, &to_address, amount.into(), sha256(&sec).as_slice(), TIME_LOCK) + .map_err(|e| { + MmError::new(TradePreimageError::InternalError(format!( + "Could not create HTLC. {:?}", + e.into_inner() + ))) + })?; + + let current_block = self.current_block().compat().await.map_err(|e| { + MmError::new(TradePreimageError::InternalError(format!( + "Could not get current_block. {}", + e + ))) + })?; + + let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; + let account_info = self.my_account_info().await?; + + let simulated_tx = self + .gen_simulated_tx( + account_info.clone(), + create_htlc_tx.msg_payload.clone(), + timeout_height, + TX_DEFAULT_MEMO.into(), + ) + .map_err(|e| { + MmError::new(TradePreimageError::InternalError(format!( + "Tx simulation failed. {:?}", + e + ))) + })?; + + let fee_uamount = self.calculate_fee_amount_as_u64(simulated_tx).await?; + let fee_amount = big_decimal_from_sat_unsigned(fee_uamount, self.decimals); + + Ok(TradeFee { + coin: ticker, + amount: fee_amount.into(), + paid_from_trading_vol: false, + }) + } + + pub(super) async fn get_fee_to_send_taker_fee_for_denom( + &self, + ticker: String, + denom: Denom, + decimals: u8, + dex_fee_amount: BigDecimal, + ) -> 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())))?; + let amount = sat_from_big_decimal(&dex_fee_amount, decimals)?; + + let current_block = self.current_block().compat().await.map_err(|e| { + MmError::new(TradePreimageError::InternalError(format!( + "Could not get current_block. {}", + e + ))) + })?; + + let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; + let account_info = self.my_account_info().await?; + + let msg_send = MsgSend { + from_address: self.account_id.clone(), + to_address: to_address.clone(), + amount: vec![Coin { + denom, + amount: amount.into(), + }], + } + .to_any() + .map_err(|e| MmError::new(TradePreimageError::InternalError(e.to_string())))?; + + let simulated_tx = self + .gen_simulated_tx(account_info.clone(), msg_send, timeout_height, TX_DEFAULT_MEMO.into()) + .map_err(|e| { + MmError::new(TradePreimageError::InternalError(format!( + "Tx simulation failed. {:?}", + e + ))) + })?; + + let fee_uamount = self.calculate_fee_amount_as_u64(simulated_tx).await?; + let fee_amount = big_decimal_from_sat_unsigned(fee_uamount, decimals); + + Ok(TradeFee { + coin: ticker, + amount: fee_amount.into(), + paid_from_trading_vol: false, + }) + } + async fn request_tx(&self, hash: String) -> MmResult { let path = AbciPath::from_str(ABCI_GET_TX_PATH).expect("valid path"); let request = GetTxRequest { hash }; @@ -1067,7 +1231,39 @@ impl TendermintCoin { .or_mm_err(|| TendermintCoinRpcError::InvalidResponse(format!("Tx {} does not exist", request.hash))) } - async fn query_htlc(&self, id: String) -> MmResult { + /// Returns status code of transaction. + /// If tx doesn't exists on chain, then returns `None`. + async fn get_tx_status_code_or_none( + &self, + hash: String, + ) -> MmResult, TendermintCoinRpcError> { + let path = AbciPath::from_str(ABCI_GET_TX_PATH).expect("valid path"); + let request = GetTxRequest { hash }; + let response = self + .rpc_client() + .await? + .abci_query( + Some(path), + request.encode_to_vec(), + ABCI_REQUEST_HEIGHT, + ABCI_REQUEST_PROVE, + ) + .await?; + + let tx = GetTxResponse::decode(response.value.as_slice())?; + + if let Some(tx_response) = tx.tx_response { + // non-zero values are error. + match tx_response.code { + TX_SUCCESS_CODE => Ok(Some(cosmrs::tendermint::abci::Code::Ok)), + err_code => Ok(Some(cosmrs::tendermint::abci::Code::Err(err_code))), + } + } else { + Ok(None) + } + } + + pub(crate) async fn query_htlc(&self, id: String) -> MmResult { let path = AbciPath::from_str(ABCI_QUERY_HTLC_PATH).expect("valid path"); let request = QueryHtlcRequestProto { id }; let response = self @@ -1117,7 +1313,7 @@ impl TendermintCoin { let request = GetTxsEventRequest { events: vec![events_string], pagination: None, - order_by: 0, + order_by: TendermintResultOrder::Ascending as i32, }; let encoded_request = request.encode_to_vec(); @@ -1314,6 +1510,7 @@ impl MmCoin for TendermintCoin { fee_details: Some(TxFeeDetails::Tendermint(TendermintFeeDetails { coin: coin.ticker.clone(), amount: fee_amount_dec, + uamount: fee_amount_u64, gas_limit: GAS_LIMIT_DEFAULT, })), coin: coin.ticker.to_string(), @@ -1366,70 +1563,54 @@ impl MmCoin for TendermintCoin { } fn process_history_loop(&self, ctx: MmArc) -> Box + Send> { - // TODO - warn!("process_history_loop is not implemented"); + warn!("process_history_loop is deprecated, tendermint uses tx_history_v2"); Box::new(futures01::future::err(())) } - fn history_sync_status(&self) -> HistorySyncState { HistorySyncState::NotEnabled } + fn history_sync_status(&self) -> HistorySyncState { self.history_sync_state.lock().unwrap().clone() } fn get_trade_fee(&self) -> Box + Send> { - // TODO Box::new(futures01::future::err("Not implemented".into())) } - // TODO - // !! This function includes dummy implementation for P.O.C work async fn get_sender_trade_fee( &self, value: TradePreimageValue, - stage: FeeApproxStage, + _stage: FeeApproxStage, ) -> TradePreimageResult { - Ok(TradeFee { - coin: self.ticker().to_string(), - amount: MmNumber::from(1_u64), - paid_from_trading_vol: false, - }) + let amount = match value { + TradePreimageValue::Exact(decimal) | TradePreimageValue::UpperBound(decimal) => decimal, + }; + self.get_sender_trade_fee_for_denom(self.ticker.clone(), self.denom.clone(), self.decimals, amount) + .await } - // TODO - // !! This function includes dummy implementation for P.O.C work - fn get_receiver_trade_fee(&self, stage: FeeApproxStage) -> TradePreimageFut { + fn get_receiver_trade_fee(&self, send_amount: BigDecimal, stage: FeeApproxStage) -> TradePreimageFut { let coin = self.clone(); let fut = async move { - Ok(TradeFee { - coin: coin.ticker().to_string(), - amount: MmNumber::from(1_u64), - paid_from_trading_vol: false, - }) + // We can't simulate Claim Htlc without having information about broadcasted htlc tx. + // Since create and claim htlc fees are almost same, we can simply simulate create htlc tx. + coin.get_sender_trade_fee_for_denom(coin.ticker.clone(), coin.denom.clone(), coin.decimals, send_amount) + .await }; - Box::new(fut.boxed().compat()) } - // TODO - // !! This function includes dummy implementation for P.O.C work async fn get_fee_to_send_taker_fee( &self, dex_fee_amount: BigDecimal, - stage: FeeApproxStage, + _stage: FeeApproxStage, ) -> TradePreimageResult { - Ok(TradeFee { - coin: self.ticker().to_string(), - amount: MmNumber::from(1_u64), - paid_from_trading_vol: false, - }) + self.get_fee_to_send_taker_fee_for_denom(self.ticker.clone(), self.denom.clone(), self.decimals, dex_fee_amount) + .await } - // TODO - // !! This function includes dummy implementation for P.O.C work fn required_confirmations(&self) -> u64 { 0 } fn requires_notarization(&self) -> bool { false } fn set_required_confirmations(&self, confirmations: u64) { - // TODO - warn!("set_required_confirmations has no effect for now") + warn!("set_required_confirmations is not supported for tendermint") } fn set_requires_notarization(&self, requires_nota: bool) { warn!("TendermintCoin doesn't support notarization") } @@ -1519,13 +1700,44 @@ impl MarketCoinOps for TendermintCoin { fn wait_for_confirmations( &self, - _tx: &[u8], + tx_bytes: &[u8], _confirmations: u64, _requires_nota: bool, - _wait_until: u64, - _check_every: u64, + wait_until: u64, + check_every: u64, ) -> Box + Send> { - let fut = async move { Ok(()) }; + // Sanity check + let _: TxRaw = try_fus!(Message::decode(tx_bytes)); + + let tx_hash = hex::encode_upper(sha256(tx_bytes)); + + let coin = self.clone(); + let fut = async move { + loop { + if now_ms() / 1000 > wait_until { + return ERR!( + "Waited too long until {} for payment {} to be received", + wait_until, + tx_hash.clone() + ); + } + + let tx_status_code = try_s!(coin.get_tx_status_code_or_none(tx_hash.clone()).await); + + if let Some(tx_status_code) = tx_status_code { + return match tx_status_code { + cosmrs::tendermint::abci::Code::Ok => Ok(()), + cosmrs::tendermint::abci::Code::Err(err_code) => Err(format!( + "Got error code: '{}' for tx: '{}'. Broadcasted tx isn't valid.", + err_code, tx_hash + )), + }; + }; + + Timer::sleep(check_every as f64).await; + } + }; + Box::new(fut.boxed().compat()) } @@ -1547,7 +1759,7 @@ impl MarketCoinOps for TendermintCoin { let request = GetTxsEventRequest { events: vec![events_string], pagination: None, - order_by: 0, + order_by: TendermintResultOrder::Ascending as i32, }; let encoded_request = request.encode_to_vec(); @@ -1603,8 +1815,6 @@ impl MarketCoinOps for TendermintCoin { fn min_tx_amount(&self) -> BigDecimal { big_decimal_from_sat(MIN_TX_SATOSHIS, self.decimals) } - // TODO - // !! This function includes dummy implementation for P.O.C work fn min_trading_vol(&self) -> MmNumber { MmNumber::from("0.00777") } } @@ -1643,8 +1853,7 @@ impl SwapOps for TendermintCoin { ) -> TransactionFut { let tx = try_tx_fus!(cosmrs::Tx::from_bytes(maker_spends_payment_args.other_payment_tx)); let msg = try_tx_fus!(tx.body.messages.first().ok_or("Tx body couldn't be read.")); - let htlc_proto: crate::tendermint::htlc_proto::CreateHtlcProtoRep = - try_tx_fus!(prost::Message::decode(msg.value.as_slice())); + let htlc_proto: CreateHtlcProtoRep = try_tx_fus!(prost::Message::decode(msg.value.as_slice())); let htlc = try_tx_fus!(MsgCreateHtlc::try_from(htlc_proto)); let mut amount = htlc.amount.clone(); @@ -1702,8 +1911,7 @@ impl SwapOps for TendermintCoin { ) -> TransactionFut { let tx = try_tx_fus!(cosmrs::Tx::from_bytes(taker_spends_payment_args.other_payment_tx)); let msg = try_tx_fus!(tx.body.messages.first().ok_or("Tx body couldn't be read.")); - let htlc_proto: crate::tendermint::htlc_proto::CreateHtlcProtoRep = - try_tx_fus!(prost::Message::decode(msg.value.as_slice())); + let htlc_proto: CreateHtlcProtoRep = try_tx_fus!(prost::Message::decode(msg.value.as_slice())); let htlc = try_tx_fus!(MsgCreateHtlc::try_from(htlc_proto)); let mut amount = htlc.amount.clone(); @@ -1817,7 +2025,7 @@ impl SwapOps for TendermintCoin { async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { let tx = try_s!(cosmrs::Tx::from_bytes(spend_tx)); let msg = try_s!(tx.body.messages.first().ok_or("Tx body couldn't be read.")); - let htlc_proto: crate::tendermint::htlc_proto::ClaimHtlcProtoRep = + let htlc_proto: super::iris::htlc_proto::ClaimHtlcProtoRep = try_s!(prost::Message::decode(msg.value.as_slice())); let htlc = try_s!(MsgClaimHtlc::try_from(htlc_proto)); @@ -1944,11 +2152,12 @@ pub(crate) fn secret_from_priv_key_policy( #[cfg(test)] pub mod tendermint_coin_tests { use super::*; - use crate::tendermint::htlc_proto::ClaimHtlcProtoRep; + use common::{block_on, DEX_FEE_ADDR_RAW_PUBKEY}; use cosmrs::proto::cosmos::tx::v1beta1::{GetTxRequest, GetTxResponse, GetTxsEventResponse}; use crypto::privkey::key_pair_from_seed; use rand::{thread_rng, Rng}; + use std::mem::discriminant; pub const IRIS_TESTNET_HTLC_PAIR1_SEED: &str = "iris test seed"; // pub const IRIS_TESTNET_HTLC_PAIR1_PUB_KEY: &str = &[ @@ -1966,6 +2175,26 @@ pub mod tendermint_coin_tests { pub const IRIS_TESTNET_RPC_URL: &str = "http://34.80.202.172:26657"; + const AVG_BLOCKTIME: u8 = 5; + + const SUCCEED_TX_HASH_SAMPLES: &[&str] = &[ + // https://nyancat.iobscan.io/#/tx?txHash=A010FC0AA33FC6D597A8635F9D127C0A7B892FAAC72489F4DADD90048CFE9279 + "A010FC0AA33FC6D597A8635F9D127C0A7B892FAAC72489F4DADD90048CFE9279", + // https://nyancat.iobscan.io/#/tx?txHash=54FD77054AE311C484CC2EADD4621428BB23D14A9BAAC128B0E7B47422F86EC8 + "54FD77054AE311C484CC2EADD4621428BB23D14A9BAAC128B0E7B47422F86EC8", + // https://nyancat.iobscan.io/#/tx?txHash=7C00FAE7F70C36A316A4736025B08A6EAA2A0CC7919A2C4FC4CD14D9FFD166F9 + "7C00FAE7F70C36A316A4736025B08A6EAA2A0CC7919A2C4FC4CD14D9FFD166F9", + ]; + + const FAILED_TX_HASH_SAMPLES: &[&str] = &[ + // https://nyancat.iobscan.io/#/tx?txHash=57EE62B2DF7E311C98C24AE2A53EB0FF2C16D289CECE0826CA1FF1108C91B3F9 + "57EE62B2DF7E311C98C24AE2A53EB0FF2C16D289CECE0826CA1FF1108C91B3F9", + // https://nyancat.iobscan.io/#/tx?txHash=F3181D69C580318DFD54282C656AC81113BC600BCFBAAA480E6D8A6469EE8786 + "F3181D69C580318DFD54282C656AC81113BC600BCFBAAA480E6D8A6469EE8786", + // https://nyancat.iobscan.io/#/tx?txHash=FE6F9F395DA94A14FCFC04E0E8C496197077D5F4968DA5528D9064C464ADF522 + "FE6F9F395DA94A14FCFC04E0E8C496197077D5F4968DA5528D9064C464ADF522", + ]; + fn get_iris_usdc_ibc_protocol() -> TendermintProtocolInfo { TendermintProtocolInfo { decimals: 6, @@ -2005,7 +2234,7 @@ pub mod tendermint_coin_tests { let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc(); let conf = TendermintConf { - avg_block_time: 5, + avg_blocktime: AVG_BLOCKTIME, derivation_path: None, }; @@ -2018,6 +2247,7 @@ pub mod tendermint_coin_tests { conf, protocol_conf, rpc_urls, + false, priv_key_policy, )) .unwrap(); @@ -2135,7 +2365,7 @@ pub mod tendermint_coin_tests { let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc(); let conf = TendermintConf { - avg_block_time: 5, + avg_blocktime: AVG_BLOCKTIME, derivation_path: None, }; @@ -2148,6 +2378,7 @@ pub mod tendermint_coin_tests { conf, protocol_conf, rpc_urls, + false, priv_key_policy, )) .unwrap(); @@ -2156,7 +2387,7 @@ pub mod tendermint_coin_tests { let request = GetTxsEventRequest { events: vec![events.into()], pagination: None, - order_by: 0, + order_by: TendermintResultOrder::Ascending as i32, }; let path = AbciPath::from_str(ABCI_GET_TXS_EVENT_PATH).unwrap(); let response = block_on(block_on(coin.rpc_client()).unwrap().abci_query( @@ -2175,7 +2406,8 @@ pub mod tendermint_coin_tests { let first_msg = tx.body.as_ref().unwrap().messages.first().unwrap(); println!("{:?}", first_msg); - let claim_htlc = ClaimHtlcProtoRep::decode(first_msg.value.as_slice()).unwrap(); + let claim_htlc = + crate::tendermint::iris::htlc_proto::ClaimHtlcProtoRep::decode(first_msg.value.as_slice()).unwrap(); let expected_secret = [1; 32]; let actual_secret = hex::decode(claim_htlc.secret).unwrap(); @@ -2191,7 +2423,7 @@ pub mod tendermint_coin_tests { let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc(); let conf = TendermintConf { - avg_block_time: 5, + avg_blocktime: AVG_BLOCKTIME, derivation_path: None, }; @@ -2204,6 +2436,7 @@ pub mod tendermint_coin_tests { conf, protocol_conf, rpc_urls, + false, priv_key_policy, )) .unwrap(); @@ -2254,7 +2487,7 @@ pub mod tendermint_coin_tests { let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc(); let conf = TendermintConf { - avg_block_time: 5, + avg_blocktime: AVG_BLOCKTIME, derivation_path: None, }; @@ -2267,6 +2500,7 @@ pub mod tendermint_coin_tests { conf, protocol_conf, rpc_urls, + false, priv_key_policy, )) .unwrap(); @@ -2422,7 +2656,7 @@ pub mod tendermint_coin_tests { let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc(); let conf = TendermintConf { - avg_block_time: 5, + avg_blocktime: AVG_BLOCKTIME, derivation_path: None, }; @@ -2435,6 +2669,7 @@ pub mod tendermint_coin_tests { conf, protocol_conf, rpc_urls, + false, priv_key_policy, )) .unwrap(); @@ -2503,7 +2738,7 @@ pub mod tendermint_coin_tests { let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc(); let conf = TendermintConf { - avg_block_time: 5, + avg_blocktime: AVG_BLOCKTIME, derivation_path: None, }; @@ -2516,6 +2751,7 @@ pub mod tendermint_coin_tests { conf, protocol_conf, rpc_urls, + false, priv_key_policy, )) .unwrap(); @@ -2575,7 +2811,7 @@ pub mod tendermint_coin_tests { let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc(); let conf = TendermintConf { - avg_block_time: 5, + avg_blocktime: AVG_BLOCKTIME, derivation_path: None, }; @@ -2588,6 +2824,7 @@ pub mod tendermint_coin_tests { conf, protocol_conf, rpc_urls, + false, priv_key_policy, )) .unwrap(); @@ -2635,4 +2872,108 @@ pub mod tendermint_coin_tests { unexpected => panic!("Unexpected search_for_swap_tx_spend_my result {:?}", unexpected), }; } + + #[test] + fn test_get_tx_status_code_or_none() { + let rpc_urls = vec![IRIS_TESTNET_RPC_URL.to_string()]; + let protocol_conf = get_iris_usdc_ibc_protocol(); + + let conf = TendermintConf { + avg_blocktime: AVG_BLOCKTIME, + derivation_path: None, + }; + + 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 = PrivKeyBuildPolicy::IguanaPrivKey(key_pair.private().secret); + + let coin = common::block_on(TendermintCoin::init( + &ctx, + "USDC-IBC".to_string(), + conf, + protocol_conf, + rpc_urls, + false, + priv_key_policy, + )) + .unwrap(); + + for succeed_tx_hash in SUCCEED_TX_HASH_SAMPLES { + let status_code = common::block_on(coin.get_tx_status_code_or_none(succeed_tx_hash.to_string())) + .unwrap() + .expect("tx exists"); + + assert_eq!(status_code, cosmrs::tendermint::abci::Code::Ok); + } + + for failed_tx_hash in FAILED_TX_HASH_SAMPLES { + let status_code = common::block_on(coin.get_tx_status_code_or_none(failed_tx_hash.to_string())) + .unwrap() + .expect("tx exists"); + + assert_eq!( + discriminant(&status_code), + discriminant(&cosmrs::tendermint::abci::Code::Err(61)) + ); + } + + // Doesn't exists + let tx_hash = "0000000000000000000000000000000000000000000000000000000000000000".to_string(); + let status_code = common::block_on(coin.get_tx_status_code_or_none(tx_hash)).unwrap(); + assert!(status_code.is_none()); + } + + #[test] + fn test_wait_for_confirmations() { + const CHECK_INTERVAL: u64 = 2; + + let rpc_urls = vec![IRIS_TESTNET_RPC_URL.to_string()]; + let protocol_conf = get_iris_usdc_ibc_protocol(); + + let conf = TendermintConf { + avg_blocktime: AVG_BLOCKTIME, + derivation_path: None, + }; + + 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 = PrivKeyBuildPolicy::IguanaPrivKey(key_pair.private().secret); + + let coin = common::block_on(TendermintCoin::init( + &ctx, + "USDC-IBC".to_string(), + conf, + protocol_conf, + rpc_urls, + false, + priv_key_policy, + )) + .unwrap(); + + let wait_until = || now_ms() + 45; + + for succeed_tx_hash in SUCCEED_TX_HASH_SAMPLES { + let tx_bytes = block_on(coin.request_tx(succeed_tx_hash.to_string())) + .unwrap() + .encode_to_vec(); + + block_on( + coin.wait_for_confirmations(&tx_bytes, 0, false, wait_until(), CHECK_INTERVAL) + .compat(), + ) + .unwrap(); + } + + for failed_tx_hash in FAILED_TX_HASH_SAMPLES { + let tx_bytes = block_on(coin.request_tx(failed_tx_hash.to_string())) + .unwrap() + .encode_to_vec(); + + block_on( + coin.wait_for_confirmations(&tx_bytes, 0, false, wait_until(), CHECK_INTERVAL) + .compat(), + ) + .unwrap_err(); + } + } } diff --git a/mm2src/coins/tendermint/tendermint_token.rs b/mm2src/coins/tendermint/tendermint_token.rs index d318fe19f4..fba157675d 100644 --- a/mm2src/coins/tendermint/tendermint_token.rs +++ b/mm2src/coins/tendermint/tendermint_token.rs @@ -36,7 +36,7 @@ use std::sync::Arc; pub struct TendermintTokenImpl { pub ticker: String, - platform_coin: TendermintCoin, + pub platform_coin: TendermintCoin, pub decimals: u8, pub denom: Denom, /// This spawner is used to spawn coin's related futures that should be aborted on coin deactivation @@ -503,6 +503,7 @@ impl MmCoin for TendermintToken { fee_details: Some(TxFeeDetails::Tendermint(TendermintFeeDetails { coin: platform.ticker().to_string(), amount: fee_amount_dec, + uamount: fee_amount_u64, gas_limit: GAS_LIMIT_DEFAULT, })), coin: token.ticker.clone(), @@ -527,48 +528,51 @@ impl MmCoin for TendermintToken { fn validate_address(&self, address: &str) -> ValidateAddressResult { self.platform_coin.validate_address(address) } fn process_history_loop(&self, ctx: MmArc) -> Box + Send> { - // TODO - warn!("process_history_loop is not implemented"); + warn!("process_history_loop is deprecated, tendermint uses tx_history_v2"); Box::new(futures01::future::err(())) } fn history_sync_status(&self) -> HistorySyncState { HistorySyncState::NotEnabled } fn get_trade_fee(&self) -> Box + Send> { - // TODO Box::new(futures01::future::err("Not implemented".into())) } async fn get_sender_trade_fee( &self, value: TradePreimageValue, - stage: FeeApproxStage, + _stage: FeeApproxStage, ) -> TradePreimageResult { - Ok(TradeFee { - coin: self.platform_coin.ticker().into(), - amount: "0.0002".into(), - paid_from_trading_vol: false, - }) + let amount = match value { + TradePreimageValue::Exact(decimal) | TradePreimageValue::UpperBound(decimal) => decimal, + }; + + self.platform_coin + .get_sender_trade_fee_for_denom(self.ticker.clone(), self.denom.clone(), self.decimals, amount) + .await } - fn get_receiver_trade_fee(&self, stage: FeeApproxStage) -> TradePreimageFut { - Box::new(futures01::future::ok(TradeFee { - coin: self.platform_coin.ticker().into(), - amount: "0.0002".into(), - paid_from_trading_vol: false, - })) + fn get_receiver_trade_fee(&self, send_amount: BigDecimal, _stage: FeeApproxStage) -> TradePreimageFut { + let token = self.clone(); + let fut = async move { + // We can't simulate Claim Htlc without having information about broadcasted htlc tx. + // Since create and claim htlc fees are almost same, we can simply simulate create htlc tx. + token + .platform_coin + .get_sender_trade_fee_for_denom(token.ticker.clone(), token.denom.clone(), token.decimals, send_amount) + .await + }; + Box::new(fut.boxed().compat()) } async fn get_fee_to_send_taker_fee( &self, dex_fee_amount: BigDecimal, - stage: FeeApproxStage, + _stage: FeeApproxStage, ) -> TradePreimageResult { - Ok(TradeFee { - coin: self.platform_coin.ticker().into(), - amount: "0.0002".into(), - paid_from_trading_vol: false, - }) + self.platform_coin + .get_fee_to_send_taker_fee_for_denom(self.ticker.clone(), self.denom.clone(), self.decimals, dex_fee_amount) + .await } fn required_confirmations(&self) -> u64 { self.platform_coin.required_confirmations() } @@ -576,8 +580,7 @@ impl MmCoin for TendermintToken { fn requires_notarization(&self) -> bool { self.platform_coin.requires_notarization() } fn set_required_confirmations(&self, confirmations: u64) { - // TODO - warn!("set_required_confirmations has no effect for now") + warn!("set_required_confirmations is not supported for tendermint") } fn set_requires_notarization(&self, requires_nota: bool) { diff --git a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs new file mode 100644 index 0000000000..14e624a9ba --- /dev/null +++ b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs @@ -0,0 +1,899 @@ +use super::{rpc::*, AllBalancesResult, TendermintCoin, TendermintCommons, TendermintToken}; + +use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHistoryTarget, TxHistoryStorage}; +use crate::tendermint::{CustomTendermintMsgType, 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 async_trait::async_trait; +use bitcrypto::sha256; +use common::executor::Timer; +use common::log; +use common::state_machine::prelude::*; +use cosmrs::tendermint::abci::Code as TxCode; +use cosmrs::tendermint::abci::Event; +use cosmrs::tx::Fee; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::MmResult; +use mm2_number::BigDecimal; +use primitives::hash::H256; +use rpc::v1::types::Bytes as BytesJson; +use std::cmp; + +macro_rules! try_or_return_stopped_as_err { + ($exp:expr, $reason: expr, $fmt:literal) => { + match $exp { + Ok(t) => t, + Err(e) => { + return Err(Stopped { + phantom: Default::default(), + stop_reason: $reason(format!("{}: {}", $fmt, e)), + }) + }, + } + }; +} + +macro_rules! try_or_continue { + ($exp:expr, $fmt:literal) => { + match $exp { + Ok(t) => t, + Err(e) => { + log::debug!("{}: {}", $fmt, e); + continue; + }, + } + }; +} + +macro_rules! some_or_continue { + ($exp:expr) => { + match $exp { + Some(t) => t, + None => { + continue; + }, + } + }; +} + +macro_rules! some_or_return { + ($exp:expr) => { + match $exp { + Some(t) => t, + None => { + return; + }, + } + }; +} + +trait CoinCapabilities: TendermintCommons + CoinWithTxHistoryV2 + MmCoin + MarketCoinOps {} +impl CoinCapabilities for TendermintCoin {} + +#[async_trait] +impl CoinWithTxHistoryV2 for TendermintCoin { + fn history_wallet_id(&self) -> WalletId { WalletId::new(self.ticker().into()) } + + async fn get_tx_history_filters( + &self, + _target: MyTxHistoryTarget, + ) -> MmResult { + Ok(GetTxHistoryFilters::for_address(self.account_id.to_string())) + } +} + +#[async_trait] +impl CoinWithTxHistoryV2 for TendermintToken { + fn history_wallet_id(&self) -> WalletId { WalletId::new(self.platform_ticker().into()) } + + async fn get_tx_history_filters( + &self, + _target: MyTxHistoryTarget, + ) -> MmResult { + let denom_hash = sha256(self.denom.to_string().as_bytes()); + let id = H256::from(denom_hash.as_slice()); + + Ok(GetTxHistoryFilters::for_address(self.platform_coin.account_id.to_string()).with_token_id(id.to_string())) + } +} + +struct TendermintTxHistoryCtx { + coin: Coin, + storage: Storage, + balances: AllBalancesResult, +} + +struct TendermintInit { + phantom: std::marker::PhantomData<(Coin, Storage)>, +} + +impl TendermintInit { + fn new() -> Self { + TendermintInit { + phantom: Default::default(), + } + } +} + +#[derive(Debug)] +enum StopReason { + StorageError(String), + RpcClient(String), +} + +struct Stopped { + phantom: std::marker::PhantomData<(Coin, Storage)>, + stop_reason: StopReason, +} + +impl Stopped { + fn storage_error(e: E) -> Self + where + E: std::fmt::Debug, + { + Stopped { + phantom: Default::default(), + stop_reason: StopReason::StorageError(format!("{:?}", e)), + } + } +} + +struct WaitForHistoryUpdateTrigger { + address: String, + last_height_state: u64, + phantom: std::marker::PhantomData<(Coin, Storage)>, +} + +impl WaitForHistoryUpdateTrigger { + fn new(address: String, last_height_state: u64) -> Self { + WaitForHistoryUpdateTrigger { + address, + last_height_state, + phantom: Default::default(), + } + } +} + +struct OnIoErrorCooldown { + address: String, + last_block_height: u64, + phantom: std::marker::PhantomData<(Coin, Storage)>, +} + +impl OnIoErrorCooldown { + fn new(address: String, last_block_height: u64) -> Self { + OnIoErrorCooldown { + address, + last_block_height, + phantom: Default::default(), + } + } +} + +impl TransitionFrom> for OnIoErrorCooldown {} + +#[async_trait] +impl State for OnIoErrorCooldown +where + Coin: CoinCapabilities, + Storage: TxHistoryStorage, +{ + type Ctx = TendermintTxHistoryCtx; + type Result = (); + + async fn on_changed(mut self: Box, _ctx: &mut Self::Ctx) -> StateResult { + Timer::sleep(30.).await; + + // retry history fetching process from last saved block + return Self::change_state(FetchingTransactionsData::new(self.address, self.last_block_height)); + } +} + +struct FetchingTransactionsData { + /// The list of addresses for those we have requested [`UpdatingUnconfirmedTxes::all_tx_ids_with_height`] TX hashses + /// at the `FetchingTxHashes` state. + address: String, + from_block_height: u64, + phantom: std::marker::PhantomData<(Coin, Storage)>, +} + +impl FetchingTransactionsData { + fn new(address: String, from_block_height: u64) -> Self { + FetchingTransactionsData { + address, + phantom: Default::default(), + from_block_height, + } + } +} + +impl TransitionFrom> for Stopped {} +impl TransitionFrom> for FetchingTransactionsData {} +impl TransitionFrom> for FetchingTransactionsData {} +impl TransitionFrom> for OnIoErrorCooldown {} +impl TransitionFrom> for Stopped {} +impl TransitionFrom> for Stopped {} + +impl TransitionFrom> + for FetchingTransactionsData +{ +} + +impl TransitionFrom> + for WaitForHistoryUpdateTrigger +{ +} + +#[async_trait] +impl State for WaitForHistoryUpdateTrigger +where + Coin: CoinCapabilities, + Storage: TxHistoryStorage, +{ + type Ctx = TendermintTxHistoryCtx; + type Result = (); + + async fn on_changed(self: Box, ctx: &mut Self::Ctx) -> StateResult { + loop { + Timer::sleep(30.).await; + + let ctx_balances = ctx.balances.clone(); + + let balances = match ctx.coin.all_balances().await { + Ok(balances) => balances, + Err(_) => { + return Self::change_state(OnIoErrorCooldown::new(self.address.clone(), self.last_height_state)); + }, + }; + + if balances != ctx_balances { + // Update balances + ctx.balances = balances; + + return Self::change_state(FetchingTransactionsData::new( + self.address.clone(), + self.last_height_state, + )); + } + } + } +} + +#[async_trait] +impl State for FetchingTransactionsData +where + Coin: CoinCapabilities, + Storage: TxHistoryStorage, +{ + type Ctx = TendermintTxHistoryCtx; + type Result = (); + + async fn on_changed(self: Box, ctx: &mut Self::Ctx) -> StateResult { + const TX_PAGE_SIZE: u8 = 50; + + const DEFAULT_TRANSFER_EVENT_COUNT: usize = 1; + const CREATE_HTLC_EVENT: &str = "create_htlc"; + const CLAIM_HTLC_EVENT: &str = "claim_htlc"; + const TRANSFER_EVENT: &str = "transfer"; + const ACCEPTED_EVENTS: &[&str] = &[CREATE_HTLC_EVENT, CLAIM_HTLC_EVENT, TRANSFER_EVENT]; + const RECIPIENT_TAG_KEY: &str = "recipient"; + const SENDER_TAG_KEY: &str = "sender"; + const RECEIVER_TAG_KEY: &str = "receiver"; + const AMOUNT_TAG_KEY: &str = "amount"; + + struct TxAmounts { + total: BigDecimal, + spent_by_me: BigDecimal, + received_by_me: BigDecimal, + } + + fn get_tx_amounts( + transfer_details: &TransferDetails, + is_self_transfer: bool, + sent_by_me: bool, + is_sign_claim_htlc: bool, + fee_details: Option<&TendermintFeeDetails>, + ) -> TxAmounts { + let amount = BigDecimal::from(transfer_details.amount); + + let total = if is_sign_claim_htlc && !is_self_transfer { + BigDecimal::default() + } else { + amount.clone() + }; + + let spent_by_me = if sent_by_me + && !is_self_transfer + && !matches!(transfer_details.transfer_event_type, TransferEventType::ClaimHtlc) + { + amount.clone() + } else { + BigDecimal::default() + }; + + let received_by_me = if !sent_by_me || is_self_transfer { + amount + } else { + BigDecimal::default() + }; + + let mut tx_amounts = TxAmounts { + total, + spent_by_me, + received_by_me, + }; + + if let Some(fee_details) = fee_details { + tx_amounts.total += BigDecimal::from(fee_details.uamount); + tx_amounts.spent_by_me += BigDecimal::from(fee_details.uamount); + } + + tx_amounts + } + + fn get_fee_details(fee: Fee, coin: &Coin) -> Result + where + Coin: CoinCapabilities, + { + let fee_coin = fee + .amount + .first() + .ok_or_else(|| "fee coin can't be empty".to_string())?; + let fee_uamount: u64 = fee_coin.amount.to_string().parse().map_err(|e| format!("{:?}", e))?; + + Ok(TendermintFeeDetails { + coin: coin.platform_ticker().to_string(), + amount: big_decimal_from_sat_unsigned(fee_uamount, coin.decimals()), + uamount: fee_uamount, + gas_limit: fee.gas_limit.value(), + }) + } + + #[derive(Default, Clone)] + enum TransferEventType { + #[default] + Standard, + CreateHtlc, + ClaimHtlc, + } + + #[derive(Clone)] + struct TransferDetails { + from: String, + to: String, + denom: String, + amount: u64, + transfer_event_type: TransferEventType, + } + + // updates sender and receiver addressses if tx is htlc, and if not leaves as it is. + fn read_real_htlc_addresses(transfer_details: &mut TransferDetails, msg_event: &&Event) { + match msg_event.type_str.as_str() { + CREATE_HTLC_EVENT => { + transfer_details.from = some_or_return!(msg_event + .attributes + .iter() + .find(|tag| tag.key.to_string() == SENDER_TAG_KEY)) + .value + .to_string(); + + transfer_details.to = some_or_return!(msg_event + .attributes + .iter() + .find(|tag| tag.key.to_string() == RECEIVER_TAG_KEY)) + .value + .to_string(); + + transfer_details.transfer_event_type = TransferEventType::CreateHtlc; + }, + CLAIM_HTLC_EVENT => { + transfer_details.from = some_or_return!(msg_event + .attributes + .iter() + .find(|tag| tag.key.to_string() == SENDER_TAG_KEY)) + .value + .to_string(); + + transfer_details.transfer_event_type = TransferEventType::ClaimHtlc; + }, + _ => {}, + } + } + + fn parse_transfer_values_from_events(tx_events: Vec<&Event>) -> Vec { + let mut transfer_details_list: Vec = vec![]; + + for (index, event) in tx_events.iter().enumerate() { + if event.type_str.as_str() == TRANSFER_EVENT { + let amount_with_denoms = some_or_continue!(event + .attributes + .iter() + .find(|tag| tag.key.to_string() == AMOUNT_TAG_KEY)) + .value + .to_string(); + let amount_with_denoms = amount_with_denoms.split(','); + + for amount_with_denom in amount_with_denoms { + let extracted_amount: String = + amount_with_denom.chars().take_while(|c| c.is_numeric()).collect(); + let denom = &amount_with_denom[extracted_amount.len()..]; + let amount = some_or_continue!(extracted_amount.parse().ok()); + + let from = some_or_continue!(event + .attributes + .iter() + .find(|tag| tag.key.to_string() == SENDER_TAG_KEY)) + .value + .to_string(); + + let to = some_or_continue!(event + .attributes + .iter() + .find(|tag| tag.key.to_string() == RECIPIENT_TAG_KEY)) + .value + .to_string(); + + let mut tx_details = TransferDetails { + from, + to, + denom: denom.to_owned(), + amount, + // Default is Standard, can be changed later in read_real_htlc_addresses + transfer_event_type: TransferEventType::default(), + }; + + if index != 0 { + // If previous message is htlc related, that means current transfer + // addresses will be wrong. + if let Some(prev_event) = tx_events.get(index - 1) { + if [CREATE_HTLC_EVENT, CLAIM_HTLC_EVENT].contains(&prev_event.type_str.as_str()) { + read_real_htlc_addresses(&mut tx_details, prev_event); + } + }; + } + + // sum the amounts coins and pairs are same + let mut duplicated_details = transfer_details_list.iter_mut().find(|details| { + details.from == tx_details.from + && details.to == tx_details.to + && details.denom == tx_details.denom + }); + + if let Some(duplicated_details) = &mut duplicated_details { + duplicated_details.amount += tx_details.amount; + } else { + transfer_details_list.push(tx_details); + } + } + } + } + + transfer_details_list + } + + fn get_transfer_details(tx_events: Vec, fee_amount_with_denom: String) -> Vec { + // Filter out irrelevant events + let mut events: Vec<&Event> = tx_events + .iter() + .filter(|event| ACCEPTED_EVENTS.contains(&event.type_str.as_str())) + .collect(); + + events.reverse(); + + if events.len() > DEFAULT_TRANSFER_EVENT_COUNT { + // Retain fee related events + events.retain(|event| { + if event.type_str == TRANSFER_EVENT { + let amount_with_denom = event + .attributes + .iter() + .find(|tag| tag.key.to_string() == AMOUNT_TAG_KEY) + .map(|t| t.value.to_string()); + + amount_with_denom != Some(fee_amount_with_denom.clone()) + } else { + true + } + }); + } + + parse_transfer_values_from_events(events) + } + + fn get_transaction_type( + transfer_event_type: &TransferEventType, + token_id: Option, + is_sign_claim_htlc: bool, + ) -> TransactionType { + match (transfer_event_type, token_id) { + (TransferEventType::CreateHtlc, token_id) => TransactionType::CustomTendermintMsg { + msg_type: CustomTendermintMsgType::SendHtlcAmount, + token_id, + }, + (TransferEventType::ClaimHtlc, token_id) => TransactionType::CustomTendermintMsg { + msg_type: if is_sign_claim_htlc { + CustomTendermintMsgType::SignClaimHtlc + } else { + CustomTendermintMsgType::ClaimHtlcAmount + }, + token_id, + }, + (_, Some(token_id)) => TransactionType::TokenTransfer(token_id), + _ => TransactionType::StandardTransfer, + } + } + + fn get_pair_addresses( + my_address: String, + tx_sent_by_me: bool, + transfer_details: &TransferDetails, + ) -> Option<(Vec, Vec)> { + match transfer_details.transfer_event_type { + TransferEventType::CreateHtlc => { + if tx_sent_by_me { + Some((vec![my_address], vec![])) + } else { + // This shouldn't happen if rpc node properly executes the tx search query. + None + } + }, + TransferEventType::ClaimHtlc => Some((vec![my_address], vec![])), + TransferEventType::Standard => { + Some((vec![transfer_details.from.clone()], vec![transfer_details.to.clone()])) + }, + } + } + + async fn fetch_and_insert_txs( + address: String, + coin: &Coin, + storage: &Storage, + query: String, + from_height: u64, + ) -> Result> + where + Coin: CoinCapabilities, + Storage: TxHistoryStorage, + { + let mut page = 1; + let mut iterate_more = true; + let mut highest_height = from_height; + + let client = try_or_return_stopped_as_err!( + coin.rpc_client().await, + StopReason::RpcClient, + "could not get rpc client" + ); + + while iterate_more { + let response = try_or_return_stopped_as_err!( + client + .perform(TxSearchRequest::new( + query.clone(), + false, + page, + TX_PAGE_SIZE, + TendermintResultOrder::Ascending.into(), + )) + .await, + StopReason::RpcClient, + "tx search rpc call failed" + ); + + let mut tx_details = vec![]; + for tx in response.txs { + if tx.tx_result.code != TxCode::Ok { + continue; + } + + let timestamp = try_or_return_stopped_as_err!( + coin.get_block_timestamp(i64::from(tx.height)).await, + StopReason::RpcClient, + "could not get block_timestamp over rpc node" + ); + let timestamp = some_or_continue!(timestamp); + + let tx_hash = tx.hash.to_string(); + + highest_height = cmp::max(highest_height, tx.height.into()); + + let deserialized_tx = try_or_continue!( + cosmrs::Tx::from_bytes(tx.tx.as_bytes()), + "Could not deserialize transaction" + ); + + let msg = try_or_continue!( + deserialized_tx.body.messages.first().ok_or("Tx body couldn't be read."), + "Tx body messages is empty" + ) + .value + .as_slice(); + + let fee_data = match deserialized_tx.auth_info.fee.amount.first() { + Some(data) => data, + None => { + log::debug!("Could not read transaction fee for tx '{}', skipping it", &tx_hash); + continue; + }, + }; + + let fee_amount_with_denom = format!("{}{}", fee_data.amount, fee_data.denom); + + let transfer_details_list = get_transfer_details(tx.tx_result.events, fee_amount_with_denom); + + if transfer_details_list.is_empty() { + log::debug!( + "Could not find transfer details in events for tx '{}', skipping it", + &tx_hash + ); + continue; + } + + let fee_details = try_or_continue!( + get_fee_details(deserialized_tx.auth_info.fee, coin), + "get_fee_details failed" + ); + + let mut fee_added = false; + for (index, transfer_details) in transfer_details_list.iter().enumerate() { + let mut internal_id_hash = index.to_le_bytes().to_vec(); + internal_id_hash.extend_from_slice(tx_hash.as_bytes()); + drop_mutability!(internal_id_hash); + + let internal_id = H256::from(internal_id_hash.as_slice()).reversed().to_vec().into(); + + if let Ok(Some(_)) = storage + .get_tx_from_history(&coin.history_wallet_id(), &internal_id) + .await + { + log::debug!("Tx '{}' already exists in tx_history. Skipping it.", &tx_hash); + continue; + } + + let tx_sent_by_me = address == transfer_details.from; + let is_platform_coin_tx = transfer_details.denom == coin.platform_denom(); + let is_self_tx = transfer_details.to == transfer_details.from && tx_sent_by_me; + let is_sign_claim_htlc = tx_sent_by_me + && matches!(transfer_details.transfer_event_type, TransferEventType::ClaimHtlc); + + let (from, to) = + some_or_continue!(get_pair_addresses(address.clone(), tx_sent_by_me, transfer_details)); + + let maybe_add_fees = if !fee_added + // if tx is platform coin tx and sent by me + && is_platform_coin_tx && tx_sent_by_me + { + fee_added = true; + Some(&fee_details) + } else { + None + }; + + let tx_amounts = get_tx_amounts( + transfer_details, + is_self_tx, + tx_sent_by_me, + is_sign_claim_htlc, + maybe_add_fees, + ); + + let token_id: Option = match !is_platform_coin_tx { + true => { + let denom_hash = sha256(transfer_details.denom.clone().as_bytes()); + Some(H256::from(denom_hash.as_slice()).to_vec().into()) + }, + false => None, + }; + + let transaction_type = get_transaction_type( + &transfer_details.transfer_event_type, + token_id.clone(), + is_sign_claim_htlc, + ); + + let details = TransactionDetails { + from, + to, + total_amount: tx_amounts.total, + spent_by_me: tx_amounts.spent_by_me, + 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(), + fee_details: Some(TxFeeDetails::Tendermint(fee_details.clone())), + block_height: tx.height.into(), + coin: transfer_details.denom.clone(), + internal_id, + timestamp, + kmd_rewards: None, + transaction_type, + }; + tx_details.push(details.clone()); + + // Display fees as extra transactions for asset txs sent by user + if tx_sent_by_me && !fee_added && !is_platform_coin_tx { + let fee_details = fee_details.clone(); + let mut fee_tx_details = details; + fee_tx_details.to = vec![]; + fee_tx_details.total_amount = fee_details.amount.clone(); + fee_tx_details.spent_by_me = fee_details.amount.clone(); + fee_tx_details.received_by_me = BigDecimal::default(); + fee_tx_details.my_balance_change = BigDecimal::default() - &fee_details.amount; + fee_tx_details.coin = coin.platform_ticker().to_string(); + // Non-reversed version of original internal id + fee_tx_details.internal_id = H256::from(internal_id_hash.as_slice()).to_vec().into(); + fee_tx_details.transaction_type = TransactionType::FeeForTokenTx; + + tx_details.push(fee_tx_details); + fee_added = true; + } + } + + log::debug!("Tx '{}' successfuly parsed.", tx.hash); + } + + try_or_return_stopped_as_err!( + storage + .add_transactions_to_history(&coin.history_wallet_id(), tx_details) + .await + .map_err(|e| format!("{:?}", e)), + StopReason::StorageError, + "add_transactions_to_history failed" + ); + + iterate_more = (TX_PAGE_SIZE as u32 * page) < response.total_count; + page += 1; + } + + Ok(highest_height) + } + + let q = format!( + "coin_spent.spender = '{}' AND tx.height > {}", + self.address.clone(), + self.from_block_height + ); + let highest_send_tx_height = match fetch_and_insert_txs( + self.address.clone(), + &ctx.coin, + &ctx.storage, + q, + self.from_block_height, + ) + .await + { + Ok(block) => block, + Err(stopped) => { + if let StopReason::RpcClient(e) = &stopped.stop_reason { + log::error!("Sent tx history process turned into cooldown mode due to rpc error: {e}"); + return Self::change_state(OnIoErrorCooldown::new(self.address.clone(), self.from_block_height)); + } + + return Self::change_state(stopped); + }, + }; + + let q = format!( + "coin_received.receiver = '{}' AND tx.height > {}", + self.address.clone(), + self.from_block_height + ); + let highest_recieved_tx_height = match fetch_and_insert_txs( + self.address.clone(), + &ctx.coin, + &ctx.storage, + q, + self.from_block_height, + ) + .await + { + Ok(block) => block, + Err(stopped) => { + if let StopReason::RpcClient(e) = &stopped.stop_reason { + log::error!("Received tx history process turned into cooldown mode due to rpc error: {e}"); + return Self::change_state(OnIoErrorCooldown::new(self.address.clone(), self.from_block_height)); + } + + return Self::change_state(stopped); + }, + }; + + let last_fetched_block = cmp::max(highest_send_tx_height, highest_recieved_tx_height); + + log::info!( + "Tx history fetching finished for {}. Last fetched block {}", + ctx.coin.platform_ticker(), + last_fetched_block + ); + + ctx.coin.set_history_sync_state(HistorySyncState::Finished); + Self::change_state(WaitForHistoryUpdateTrigger::new( + self.address.clone(), + last_fetched_block, + )) + } +} + +#[async_trait] +impl State for TendermintInit +where + Coin: CoinCapabilities, + Storage: TxHistoryStorage, +{ + type Ctx = TendermintTxHistoryCtx; + type Result = (); + + async fn on_changed(self: Box, ctx: &mut Self::Ctx) -> StateResult { + const INITIAL_SEARCH_HEIGHT: u64 = 0; + + ctx.coin.set_history_sync_state(HistorySyncState::NotStarted); + + if let Err(e) = ctx.storage.init(&ctx.coin.history_wallet_id()).await { + return Self::change_state(Stopped::storage_error(e)); + } + + let search_from = match ctx + .storage + .get_highest_block_height(&ctx.coin.history_wallet_id()) + .await + { + Ok(Some(height)) if height > 0 => height as u64 - 1, + _ => INITIAL_SEARCH_HEIGHT, + }; + + Self::change_state(FetchingTransactionsData::new( + ctx.coin.my_address().expect("my_address can't fail"), + search_from, + )) + } +} + +#[async_trait] +impl LastState for Stopped +where + Coin: CoinCapabilities, + Storage: TxHistoryStorage, +{ + type Ctx = TendermintTxHistoryCtx; + type Result = (); + + async fn on_changed(self: Box, ctx: &mut Self::Ctx) -> Self::Result { + log::info!( + "Stopping tx history fetching for {}. Reason: {:?}", + ctx.coin.ticker(), + self.stop_reason + ); + + let new_state_json = json!({ + "message": format!("{:?}", self.stop_reason), + }); + + ctx.coin.set_history_sync_state(HistorySyncState::Error(new_state_json)); + } +} + +pub async fn tendermint_history_loop( + coin: TendermintCoin, + storage: impl TxHistoryStorage, + _ctx: MmArc, + _current_balance: BigDecimal, +) { + let balances = match coin.all_balances().await { + Ok(balances) => balances, + Err(e) => { + log::error!("{}", e); + return; + }, + }; + + let ctx = TendermintTxHistoryCtx { + coin, + storage, + balances, + }; + + let state_machine: StateMachine<_, ()> = StateMachine::from_ctx(ctx); + state_machine.run(TendermintInit::new()).await; +} diff --git a/mm2src/coins/test_coin.rs b/mm2src/coins/test_coin.rs index e0ef3ba32f..4bc319a0fa 100644 --- a/mm2src/coins/test_coin.rs +++ b/mm2src/coins/test_coin.rs @@ -269,18 +269,20 @@ impl MmCoin for TestCoin { async fn get_sender_trade_fee( &self, - value: TradePreimageValue, - stage: FeeApproxStage, + _value: TradePreimageValue, + _stage: FeeApproxStage, ) -> TradePreimageResult { unimplemented!() } - fn get_receiver_trade_fee(&self, stage: FeeApproxStage) -> TradePreimageFut { unimplemented!() } + fn get_receiver_trade_fee(&self, _send_amount: BigDecimal, _stage: FeeApproxStage) -> TradePreimageFut { + unimplemented!() + } async fn get_fee_to_send_taker_fee( &self, - dex_fee_amount: BigDecimal, - stage: FeeApproxStage, + _dex_fee_amount: BigDecimal, + _stage: FeeApproxStage, ) -> TradePreimageResult { unimplemented!() } diff --git a/mm2src/coins/tx_history_storage/mod.rs b/mm2src/coins/tx_history_storage/mod.rs index 2c23290eb9..1f0ca4f8d8 100644 --- a/mm2src/coins/tx_history_storage/mod.rs +++ b/mm2src/coins/tx_history_storage/mod.rs @@ -23,7 +23,16 @@ mod tx_history_v2_tests; #[inline] pub fn token_id_from_tx_type(tx_type: &TransactionType) -> String { match tx_type { - TransactionType::TokenTransfer(token_id) => format!("{:02x}", token_id), + TransactionType::TokenTransfer(token_id) => { + format!("{:02x}", token_id) + }, + TransactionType::CustomTendermintMsg { token_id, .. } => { + if let Some(token_id) = token_id { + format!("{:02x}", token_id) + } else { + String::new() + } + }, _ => String::new(), } } @@ -106,7 +115,7 @@ impl WalletId { #[inline] pub fn new(ticker: String) -> WalletId { WalletId { - ticker, + ticker: ticker.replace('-', "_"), hd_wallet_rmd160: None, } } 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 76e5451566..b418bba556 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 @@ -155,6 +155,18 @@ fn select_tx_by_internal_id_sql(wallet_id: &WalletId) -> Result Result> { + let table_name = tx_history_table(wallet_id); + validate_table_name(&table_name)?; + + let sql = format!( + "SELECT block_height FROM {} ORDER BY block_height DESC LIMIT 1;", + table_name + ); + + Ok(sql) +} + fn update_tx_in_table_by_internal_id_sql(wallet_id: &WalletId) -> Result> { let table_name = tx_history_table(wallet_id); validate_table_name(&table_name)?; @@ -340,6 +352,8 @@ fn tx_details_from_row(row: &Row<'_>) -> Result { json::from_str(&json_string).map_err(|e| SqlError::FromSqlConversionFailure(0, Type::Text, Box::new(e))) } +fn block_height_from_row(row: &Row<'_>) -> Result { row.get(0) } + impl TxHistoryStorageError for SqlError {} impl ConfirmationStatus { @@ -512,6 +526,17 @@ impl TxHistoryStorage for SqliteTxHistoryStorage { .await } + async fn get_highest_block_height(&self, wallet_id: &WalletId) -> Result, MmError> { + let sql = select_highest_block_height_sql(wallet_id)?; + let selfi = self.clone(); + + async_blocking(move || { + let conn = selfi.0.lock().unwrap(); + query_single_row(&conn, &sql, NO_PARAMS, block_height_from_row).map_to_mm(SqlError::from) + }) + .await + } + async fn history_contains_unconfirmed_txes( &self, wallet_id: &WalletId, 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 996c7e3b7f..12255a6a10 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 @@ -118,6 +118,11 @@ impl TxHistoryStorage for IndexedDbTxHistoryStorage { json::from_value(details_json).map_to_mm(|e| WasmTxHistoryError::ErrorDeserializing(e.to_string())) } + async fn get_highest_block_height(&self, _wallet_id: &WalletId) -> Result, MmError> { + // TODO + Ok(None) + } + /// Since we need to filter the transactions by the given `for_addresses`, /// we can't use [`DbTable::count_by_multi_index`]. /// TODO consider one of the solutions described at [`IndexedDbTxHistoryStorage::get_history`]. diff --git a/mm2src/coins/utxo/bch.rs b/mm2src/coins/utxo/bch.rs index c60c6fa8a8..5d31e03bcd 100644 --- a/mm2src/coins/utxo/bch.rs +++ b/mm2src/coins/utxo/bch.rs @@ -1228,7 +1228,7 @@ impl MmCoin for BchCoin { utxo_common::get_sender_trade_fee(self, value, stage).await } - fn get_receiver_trade_fee(&self, _stage: FeeApproxStage) -> TradePreimageFut { + fn get_receiver_trade_fee(&self, _send_amount: BigDecimal, _stage: FeeApproxStage) -> TradePreimageFut { utxo_common::get_receiver_trade_fee(self.clone()) } diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index b7ae14a63d..613a2084f3 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -916,7 +916,7 @@ impl MmCoin for QtumCoin { utxo_common::get_sender_trade_fee(self, value, stage).await } - fn get_receiver_trade_fee(&self, _stage: FeeApproxStage) -> TradePreimageFut { + fn get_receiver_trade_fee(&self, _send_amount: BigDecimal, _stage: FeeApproxStage) -> TradePreimageFut { utxo_common::get_receiver_trade_fee(self.clone()) } diff --git a/mm2src/coins/utxo/qtum_delegation.rs b/mm2src/coins/utxo/qtum_delegation.rs index 5765e482b0..e2f1fcb126 100644 --- a/mm2src/coins/utxo/qtum_delegation.rs +++ b/mm2src/coins/utxo/qtum_delegation.rs @@ -156,10 +156,7 @@ impl QtumCoin { .map_to_mm(|e| StakingInfosError::Transport(e.to_string()))?; let am_i_staking = add_delegation_history.len() > remove_delegation_history.len(); if am_i_staking { - let last_tx_add = match add_delegation_history.last() { - Some(last_tx_add) => last_tx_add, - None => return Ok(None), - }; + let last_tx_add = some_or_return_ok_none!(add_delegation_history.last()); let res = &client .blockchain_transaction_get_receipt(&last_tx_add.tx_hash) .compat() diff --git a/mm2src/coins/utxo/slp.rs b/mm2src/coins/utxo/slp.rs index d1e24bd6d1..9b28afa0c5 100644 --- a/mm2src/coins/utxo/slp.rs +++ b/mm2src/coins/utxo/slp.rs @@ -1735,7 +1735,7 @@ impl MmCoin for SlpToken { }) } - fn get_receiver_trade_fee(&self, _stage: FeeApproxStage) -> TradePreimageFut { + fn get_receiver_trade_fee(&self, _send_amount: BigDecimal, _stage: FeeApproxStage) -> TradePreimageFut { let coin = self.clone(); let fut = async move { diff --git a/mm2src/coins/utxo/utxo_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index c4b7dab062..5ec4860db2 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -678,7 +678,7 @@ impl MmCoin for UtxoStandardCoin { utxo_common::get_sender_trade_fee(self, value, stage).await } - fn get_receiver_trade_fee(&self, _stage: FeeApproxStage) -> TradePreimageFut { + fn get_receiver_trade_fee(&self, _send_amount: BigDecimal, _stage: FeeApproxStage) -> TradePreimageFut { utxo_common::get_receiver_trade_fee(self.clone()) } diff --git a/mm2src/coins/z_coin.rs b/mm2src/coins/z_coin.rs index 5f528cb7d6..d7530ea9d2 100644 --- a/mm2src/coins/z_coin.rs +++ b/mm2src/coins/z_coin.rs @@ -1553,7 +1553,7 @@ impl MmCoin for ZCoin { }) } - fn get_receiver_trade_fee(&self, _stage: FeeApproxStage) -> TradePreimageFut { + fn get_receiver_trade_fee(&self, _send_amount: BigDecimal, _stage: FeeApproxStage) -> TradePreimageFut { utxo_common::get_receiver_trade_fee(self.clone()) } diff --git a/mm2src/coins_activation/src/bch_with_tokens_activation.rs b/mm2src/coins_activation/src/bch_with_tokens_activation.rs index 5a7e6435ff..c13c4345e7 100644 --- a/mm2src/coins_activation/src/bch_with_tokens_activation.rs +++ b/mm2src/coins_activation/src/bch_with_tokens_activation.rs @@ -14,7 +14,6 @@ use common::executor::{AbortSettings, SpawnAbortable}; use common::Future01CompatExt; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; -use mm2_metrics::MetricsArc; use mm2_number::BigDecimal; use serde_derive::{Deserialize, Serialize}; use serde_json::Value as Json; @@ -282,11 +281,11 @@ impl PlatformWithTokensActivationOps for BchCoin { fn start_history_background_fetching( &self, - metrics: MetricsArc, + ctx: MmArc, storage: impl TxHistoryStorage + Send + 'static, initial_balance: BigDecimal, ) { - let fut = bch_and_slp_history_loop(self.clone(), storage, metrics, initial_balance); + let fut = bch_and_slp_history_loop(self.clone(), storage, ctx.metrics.clone(), initial_balance); let settings = AbortSettings::info_on_abort(format!("bch_and_slp_history_loop stopped for {}", self.ticker())); self.spawner().spawn_with_settings(fut, settings); diff --git a/mm2src/coins_activation/src/erc20_token_activation.rs b/mm2src/coins_activation/src/erc20_token_activation.rs index 31fabbf25b..9a5dff22d9 100644 --- a/mm2src/coins_activation/src/erc20_token_activation.rs +++ b/mm2src/coins_activation/src/erc20_token_activation.rs @@ -66,7 +66,6 @@ impl TokenProtocolParams for Erc20Protocol { #[async_trait] impl TokenActivationOps for EthCoin { - type PlatformCoin = EthCoin; type ActivationParams = Erc20TokenActivationRequest; type ProtocolInfo = Erc20Protocol; type ActivationResult = Erc20InitResult; diff --git a/mm2src/coins_activation/src/eth_with_token_activation.rs b/mm2src/coins_activation/src/eth_with_token_activation.rs index 5056dfe9ef..d051464371 100644 --- a/mm2src/coins_activation/src/eth_with_token_activation.rs +++ b/mm2src/coins_activation/src/eth_with_token_activation.rs @@ -12,7 +12,6 @@ use coins::{eth::{v2_activation::{eth_coin_from_conf_and_request_v2, Erc20Protoc use common::Future01CompatExt; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; -use mm2_metrics::MetricsArc; use mm2_number::BigDecimal; use serde::{Deserialize, Serialize}; use serde_json::Value as Json; @@ -235,7 +234,7 @@ impl PlatformWithTokensActivationOps for EthCoin { fn start_history_background_fetching( &self, - _metrics: MetricsArc, + _ctx: MmArc, _storage: impl TxHistoryStorage + Send + 'static, _initial_balance: BigDecimal, ) { diff --git a/mm2src/coins_activation/src/platform_coin_with_tokens.rs b/mm2src/coins_activation/src/platform_coin_with_tokens.rs index e800da5d62..488c3417d5 100644 --- a/mm2src/coins_activation/src/platform_coin_with_tokens.rs +++ b/mm2src/coins_activation/src/platform_coin_with_tokens.rs @@ -8,7 +8,6 @@ use crypto::{CryptoCtx, CryptoCtxError}; use derive_more::Display; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; -use mm2_metrics::MetricsArc; use mm2_number::BigDecimal; use ser_error_derive::SerializeErrorType; use serde_derive::{Deserialize, Serialize}; @@ -22,7 +21,7 @@ pub struct TokenActivationRequest { } pub trait TokenOf: Into { - type PlatformCoin: PlatformWithTokensActivationOps + RegisterTokenInfo; + type PlatformCoin: TryPlatformCoinFromMmCoinEnum + PlatformWithTokensActivationOps + RegisterTokenInfo + Clone; } pub struct TokenActivationParams { @@ -87,7 +86,7 @@ impl From for InitTokensAsMmCoinsError { } } -pub trait RegisterTokenInfo> { +pub trait RegisterTokenInfo { fn register_token_info(&self, token: &T); } @@ -156,7 +155,7 @@ pub trait PlatformWithTokensActivationOps: Into { fn start_history_background_fetching( &self, - metrics: MetricsArc, + ctx: MmArc, storage: impl TxHistoryStorage, initial_balance: BigDecimal, ); @@ -322,7 +321,7 @@ where if req.request.tx_history() { platform_coin.start_history_background_fetching( - ctx.metrics.clone(), + ctx.clone(), TxHistoryStorageBuilder::new(&ctx).build()?, activation_result.get_platform_balance(), ); diff --git a/mm2src/coins_activation/src/slp_token_activation.rs b/mm2src/coins_activation/src/slp_token_activation.rs index b061f7a00a..85f017b0cc 100644 --- a/mm2src/coins_activation/src/slp_token_activation.rs +++ b/mm2src/coins_activation/src/slp_token_activation.rs @@ -73,7 +73,6 @@ pub struct SlpInitResult { #[async_trait] impl TokenActivationOps for SlpToken { - type PlatformCoin = BchCoin; type ActivationParams = SlpActivationRequest; type ProtocolInfo = SlpProtocolConf; type ActivationResult = SlpInitResult; diff --git a/mm2src/coins_activation/src/solana_with_tokens_activation.rs b/mm2src/coins_activation/src/solana_with_tokens_activation.rs index 0f4cc53478..684f92501f 100644 --- a/mm2src/coins_activation/src/solana_with_tokens_activation.rs +++ b/mm2src/coins_activation/src/solana_with_tokens_activation.rs @@ -15,7 +15,6 @@ use coins::{BalanceError, CoinBalance, CoinProtocol, MarketCoinOps, PrivKeyBuild use common::Future01CompatExt; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; -use mm2_metrics::MetricsArc; use mm2_number::BigDecimal; use serde_derive::{Deserialize, Serialize}; use serde_json::Value as Json; @@ -243,7 +242,7 @@ impl PlatformWithTokensActivationOps for SolanaCoin { fn start_history_background_fetching( &self, - _metrics: MetricsArc, + _ctx: MmArc, _storage: impl TxHistoryStorage + Send + 'static, _initial_balance: BigDecimal, ) { diff --git a/mm2src/coins_activation/src/spl_token_activation.rs b/mm2src/coins_activation/src/spl_token_activation.rs index 5329cd373c..69782e11ac 100644 --- a/mm2src/coins_activation/src/spl_token_activation.rs +++ b/mm2src/coins_activation/src/spl_token_activation.rs @@ -83,7 +83,6 @@ impl From for EnableTokenError { #[async_trait] impl TokenActivationOps for SplToken { - type PlatformCoin = SolanaCoin; type ActivationParams = SplActivationRequest; type ProtocolInfo = SplProtocolConf; type ActivationResult = SplInitResult; diff --git a/mm2src/coins_activation/src/tendermint_token_activation.rs b/mm2src/coins_activation/src/tendermint_token_activation.rs index 122585ef0f..58cc6b90c1 100644 --- a/mm2src/coins_activation/src/tendermint_token_activation.rs +++ b/mm2src/coins_activation/src/tendermint_token_activation.rs @@ -45,7 +45,6 @@ impl TokenProtocolParams for TendermintTokenProtocolInfo { #[async_trait] impl TokenActivationOps for TendermintToken { - type PlatformCoin = TendermintCoin; type ActivationParams = TendermintTokenActivationParams; type ProtocolInfo = TendermintTokenProtocolInfo; type ActivationResult = TendermintTokenInitResult; @@ -68,6 +67,7 @@ impl TokenActivationOps for TendermintToken { let my_address = token.my_address()?; let mut balances = HashMap::new(); balances.insert(my_address, balance); + let init_result = TendermintTokenInitResult { balances, platform_coin: token.platform_ticker().into(), diff --git a/mm2src/coins_activation/src/tendermint_with_assets_activation.rs b/mm2src/coins_activation/src/tendermint_with_assets_activation.rs index 5fb8e7e978..d9bb87afc2 100644 --- a/mm2src/coins_activation/src/tendermint_with_assets_activation.rs +++ b/mm2src/coins_activation/src/tendermint_with_assets_activation.rs @@ -5,14 +5,15 @@ use crate::platform_coin_with_tokens::{EnablePlatformCoinWithTokensError, GetPla use crate::prelude::*; use async_trait::async_trait; use coins::my_tx_history_v2::TxHistoryStorage; -use coins::tendermint::{TendermintCoin, TendermintConf, TendermintInitError, TendermintInitErrorKind, - TendermintProtocolInfo, TendermintToken, TendermintTokenActivationParams, - TendermintTokenInitError, TendermintTokenProtocolInfo}; -use coins::{CoinBalance, CoinProtocol, MarketCoinOps, PrivKeyBuildPolicy}; +use coins::tendermint::tendermint_tx_history_v2::tendermint_history_loop; +use coins::tendermint::{TendermintCoin, TendermintCommons, TendermintConf, TendermintInitError, + TendermintInitErrorKind, TendermintProtocolInfo, TendermintToken, + TendermintTokenActivationParams, TendermintTokenInitError, TendermintTokenProtocolInfo}; +use coins::{CoinBalance, CoinProtocol, MarketCoinOps, MmCoin, PrivKeyBuildPolicy}; +use common::executor::{AbortSettings, SpawnAbortable}; use common::Future01CompatExt; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; -use mm2_metrics::MetricsArc; use mm2_number::BigDecimal; use serde::{Deserialize, Serialize}; use serde_json::Value as Json; @@ -32,10 +33,12 @@ impl RegisterTokenInfo for TendermintCoin { pub struct TendermintActivationParams { rpc_urls: Vec, pub tokens_params: Vec>, + #[serde(default)] + tx_history: bool, } impl TxHistory for TendermintActivationParams { - fn tx_history(&self) -> bool { false } + fn tx_history(&self) -> bool { self.tx_history } } struct TendermintTokenInitializer { @@ -165,6 +168,7 @@ impl PlatformWithTokensActivationOps for TendermintCoin { conf, protocol_conf, activation_request.rpc_urls, + activation_request.tx_history, priv_key_policy, ) .await @@ -212,9 +216,13 @@ impl PlatformWithTokensActivationOps for TendermintCoin { fn start_history_background_fetching( &self, - _metrics: MetricsArc, - _storage: impl TxHistoryStorage, - _initial_balance: BigDecimal, + ctx: MmArc, + storage: impl TxHistoryStorage, + initial_balance: BigDecimal, ) { + let fut = tendermint_history_loop(self.clone(), storage, ctx, initial_balance); + + let settings = AbortSettings::info_on_abort(format!("tendermint_history_loop stopped for {}", self.ticker())); + self.spawner().spawn_with_settings(fut, settings); } } diff --git a/mm2src/coins_activation/src/token.rs b/mm2src/coins_activation/src/token.rs index 4bbfa7279f..fd90378b4c 100644 --- a/mm2src/coins_activation/src/token.rs +++ b/mm2src/coins_activation/src/token.rs @@ -1,5 +1,6 @@ -/// Contains token activation traits and their implementations for various coins -/// +// Contains token activation traits and their implementations for various coins + +use crate::platform_coin_with_tokens::{self, RegisterTokenInfo}; use crate::prelude::*; use async_trait::async_trait; use coins::utxo::rpc_clients::UtxoRpcError; @@ -17,8 +18,7 @@ pub trait TokenProtocolParams { } #[async_trait] -pub trait TokenActivationOps: Into { - type PlatformCoin: TryPlatformCoinFromMmCoinEnum; +pub trait TokenActivationOps: Into + platform_coin_with_tokens::TokenOf { type ActivationParams; type ProtocolInfo: TokenProtocolParams + TryFromCoinProtocol; type ActivationResult; @@ -102,7 +102,7 @@ pub async fn enable_token( req: EnableTokenRequest, ) -> Result> where - Token: TokenActivationOps, + Token: TokenActivationOps + Clone, EnableTokenError: From, (Token::ActivationError, EnableTokenError): NotEqual, { @@ -124,14 +124,16 @@ where })?; let (token, activation_result) = - Token::enable_token(req.ticker, platform_coin, req.activation_params, token_protocol).await?; + Token::enable_token(req.ticker, platform_coin.clone(), req.activation_params, token_protocol).await?; let coins_ctx = CoinsContext::from_ctx(&ctx).unwrap(); coins_ctx - .add_coin(token.into()) + .add_coin(token.clone().into()) .await .mm_err(|e| EnableTokenError::TokenIsAlreadyActivated(e.ticker))?; + platform_coin.register_token_info(&token); + Ok(activation_result) } diff --git a/mm2src/common/common.rs b/mm2src/common/common.rs index 755811d017..65045c2427 100644 --- a/mm2src/common/common.rs +++ b/mm2src/common/common.rs @@ -93,6 +93,19 @@ macro_rules! drop_mutability { }; } +/// Reads inner value of `Option`, returns `Ok(None)` otherwise. +#[macro_export] +macro_rules! some_or_return_ok_none { + ($val:expr) => { + match $val { + Some(t) => t, + None => { + return Ok(None); + }, + } + }; +} + #[macro_use] pub mod jsonrpc_client; #[macro_use] diff --git a/mm2src/gossipsub/src/protocol.rs b/mm2src/gossipsub/src/protocol.rs index ea7f549345..1173a89b22 100644 --- a/mm2src/gossipsub/src/protocol.rs +++ b/mm2src/gossipsub/src/protocol.rs @@ -24,6 +24,7 @@ use crate::topic::TopicHash; use byteorder::{BigEndian, ByteOrder}; use bytes::Bytes; use bytes::BytesMut; +use common::some_or_return_ok_none; use futures::future; use futures::prelude::*; use futures_codec::{Decoder, Encoder, Framed}; @@ -211,10 +212,7 @@ impl Decoder for GossipsubCodec { type Error = io::Error; fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { - let packet = match self.length_codec.decode(src)? { - Some(p) => p, - None => return Ok(None), - }; + let packet = some_or_return_ok_none!(self.length_codec.decode(src)?); let rpc = rpc_proto::Rpc::decode(&packet[..])?; diff --git a/mm2src/hw_common/src/transport/libusb.rs b/mm2src/hw_common/src/transport/libusb.rs index ac9351b99c..da33494dc5 100644 --- a/mm2src/hw_common/src/transport/libusb.rs +++ b/mm2src/hw_common/src/transport/libusb.rs @@ -2,8 +2,8 @@ //! We can spawn it once on [`UsbContext::new`], but we have to set the read/write chunk timeout to 0.5s or smaller. use super::{send_event_recv_response, InternalError}; -use common::block_on; use common::log::error; +use common::{block_on, some_or_return_ok_none}; use derive_more::Display; use futures::channel::{mpsc, oneshot}; use futures::StreamExt; @@ -102,28 +102,17 @@ impl UsbContext { let config_descriptor = device .config_descriptor(filters.config_id) .map_to_mm(UsbError::ErrorGettingDevices)?; - let interface = match config_descriptor + let interface = some_or_return_ok_none!(config_descriptor .interfaces() - .find(|i| i.number() == filters.interface_id) - { - Some(interface) => interface, - None => return Ok(None), - }; - let descriptor = match interface + .find(|i| i.number() == filters.interface_id)); + let descriptor = some_or_return_ok_none!(interface .descriptors() - .find(|d| d.setting_number() == filters.interface_descriptor) - { - Some(descriptor) => descriptor, - None => return Ok(None), - }; + .find(|d| d.setting_number() == filters.interface_descriptor)); if descriptor.class_code() != filters.interface_class_code { return Ok(None); } // Get the first endpoint. - let endpoint = match descriptor.endpoint_descriptors().next() { - Some(endpoint) => endpoint, - None => return Ok(None), - }; + let endpoint = some_or_return_ok_none!(descriptor.endpoint_descriptors().next()); Ok(Some(UsbDeviceInterfaceInfo { interface_number: filters.interface_id, endpoint_number: endpoint.number(), diff --git a/mm2src/mm2_gui_storage/src/account/storage/sqlite_storage.rs b/mm2src/mm2_gui_storage/src/account/storage/sqlite_storage.rs index 18d350d6c9..916854de63 100644 --- a/mm2src/mm2_gui_storage/src/account/storage/sqlite_storage.rs +++ b/mm2src/mm2_gui_storage/src/account/storage/sqlite_storage.rs @@ -3,6 +3,7 @@ use crate::account::{AccountId, AccountInfo, AccountType, AccountWithCoins, Acco EnabledAccountType, HwPubkey, MAX_ACCOUNT_DESCRIPTION_LENGTH, MAX_ACCOUNT_NAME_LENGTH, MAX_TICKER_LENGTH}; use async_trait::async_trait; +use common::some_or_return_ok_none; use db_common::foreign_columns; use db_common::sql_build::*; use db_common::sqlite::rusqlite::types::Type; @@ -244,10 +245,7 @@ impl SqliteAccountStorage { conn: &Connection, account_id: &AccountId, ) -> AccountStorageResult> { - let account_info = match Self::load_account(conn, account_id)? { - Some(acc) => acc, - None => return Ok(None), - }; + let account_info = some_or_return_ok_none!(Self::load_account(conn, account_id)?); let coins = Self::load_account_coins(conn, account_id)?; Ok(Some(AccountWithCoins { account_info, coins })) diff --git a/mm2src/mm2_main/src/lp_ordermatch.rs b/mm2src/mm2_main/src/lp_ordermatch.rs index c802dd93fa..8a7d78508f 100644 --- a/mm2src/mm2_main/src/lp_ordermatch.rs +++ b/mm2src/mm2_main/src/lp_ordermatch.rs @@ -838,10 +838,7 @@ fn process_sync_pubkey_orderbook_state( ) -> Result, String> { let ordermatch_ctx = OrdermatchContext::from_ctx(&ctx).unwrap(); let orderbook = ordermatch_ctx.orderbook.lock(); - let pubkey_state = match orderbook.pubkeys_state.get(&pubkey) { - Some(s) => s, - None => return Ok(None), - }; + let pubkey_state = some_or_return_ok_none!(orderbook.pubkeys_state.get(&pubkey)); let order_getter = |uuid: &Uuid| orderbook.order_set.get(uuid).cloned(); let pair_orders_diff: Result, _> = trie_roots @@ -4419,10 +4416,13 @@ struct CoinVolumeInfo { async fn get_max_volume(ctx: &MmArc, my_coin: &MmCoinEnum, other_coin: &MmCoinEnum) -> Result { let my_balance = try_s!(my_coin.my_spendable_balance().compat().await); - // first check if `rel_coin` balance is sufficient + + let volume = try_s!(calc_max_maker_vol(ctx, my_coin, &my_balance, FeeApproxStage::OrderIssue).await); + + // check if `rel_coin` balance is sufficient let other_coin_trade_fee = try_s!( other_coin - .get_receiver_trade_fee(FeeApproxStage::OrderIssue) + .get_receiver_trade_fee(volume.to_decimal(), FeeApproxStage::OrderIssue) .compat() .await ); @@ -4430,7 +4430,7 @@ async fn get_max_volume(ctx: &MmArc, my_coin: &MmCoinEnum, other_coin: &MmCoinEn // calculate max maker volume // note the `calc_max_maker_vol` returns [`CheckBalanceError::NotSufficientBalance`] error if the balance of `base_coin` is not sufficient Ok(CoinVolumeInfo { - volume: try_s!(calc_max_maker_vol(ctx, my_coin, &my_balance, FeeApproxStage::OrderIssue).await), + volume, balance: my_balance, }) } diff --git a/mm2src/mm2_main/src/lp_ordermatch/best_orders.rs b/mm2src/mm2_main/src/lp_ordermatch/best_orders.rs index 4305052699..35f59f8253 100644 --- a/mm2src/mm2_main/src/lp_ordermatch/best_orders.rs +++ b/mm2src/mm2_main/src/lp_ordermatch/best_orders.rs @@ -65,10 +65,7 @@ pub fn process_best_orders_p2p_request( BestOrdersAction::Buy => &orderbook.pairs_existing_for_base, BestOrdersAction::Sell => &orderbook.pairs_existing_for_rel, }; - let tickers = match search_pairs_in.get(&coin) { - Some(tickers) => tickers, - None => return Ok(None), - }; + let tickers = some_or_return_ok_none!(search_pairs_in.get(&coin)); let mut result = HashMap::new(); let pairs = tickers.iter().map(|ticker| match action { BestOrdersAction::Buy => (coin.clone(), ticker.clone()), @@ -154,10 +151,7 @@ pub fn process_best_orders_p2p_request_by_number( BestOrdersAction::Buy => &orderbook.pairs_existing_for_base, BestOrdersAction::Sell => &orderbook.pairs_existing_for_rel, }; - let tickers = match search_pairs_in.get(&coin) { - Some(tickers) => tickers, - None => return Ok(None), - }; + let tickers = some_or_return_ok_none!(search_pairs_in.get(&coin)); let mut result = HashMap::new(); let pairs = tickers.iter().map(|ticker| match action { BestOrdersAction::Buy => (coin.clone(), ticker.clone()), diff --git a/mm2src/mm2_main/src/lp_swap/maker_swap.rs b/mm2src/mm2_main/src/lp_swap/maker_swap.rs index 3c74596907..be706a7a67 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap.rs @@ -453,7 +453,9 @@ impl MakerSwap { )])) }, }; - let taker_payment_spend_trade_fee_fut = self.taker_coin.get_receiver_trade_fee(stage.clone()); + let taker_payment_spend_trade_fee_fut = self + .taker_coin + .get_receiver_trade_fee(self.maker_amount.clone(), stage.clone()); let taker_payment_spend_trade_fee = match taker_payment_spend_trade_fee_fut.compat().await { Ok(fee) => fee, Err(e) => { @@ -1977,7 +1979,7 @@ pub async fn check_balance_for_maker_swap( .await .mm_err(|e| CheckBalanceError::from_trade_preimage_error(e, my_coin.ticker()))?; let taker_payment_spend_trade_fee = other_coin - .get_receiver_trade_fee(stage) + .get_receiver_trade_fee(volume.to_decimal(), stage) .compat() .await .mm_err(|e| CheckBalanceError::from_trade_preimage_error(e, other_coin.ticker()))?; @@ -2029,7 +2031,7 @@ pub async fn maker_swap_trade_preimage( .await .mm_err(|e| TradePreimageRpcError::from_trade_preimage_error(e, base_coin_ticker))?; let rel_coin_fee = rel_coin - .get_receiver_trade_fee(FeeApproxStage::TradePreimage) + .get_receiver_trade_fee(volume.to_decimal(), FeeApproxStage::TradePreimage) .compat() .await .mm_err(|e| TradePreimageRpcError::from_trade_preimage_error(e, rel_coin_ticker))?; diff --git a/mm2src/mm2_main/src/lp_swap/swap_lock.rs b/mm2src/mm2_main/src/lp_swap/swap_lock.rs index b28f15dc97..f9f84a6cb7 100644 --- a/mm2src/mm2_main/src/lp_swap/swap_lock.rs +++ b/mm2src/mm2_main/src/lp_swap/swap_lock.rs @@ -57,10 +57,8 @@ mod native_lock { impl SwapLockOps for SwapLock { async fn lock(ctx: &MmArc, swap_uuid: Uuid, ttl_sec: f64) -> SwapLockResult> { let lock_path = my_swaps_dir(ctx).join(format!("{}.lock", swap_uuid)); - let file_lock = match FileLock::lock(lock_path, ttl_sec)? { - Some(lock) => lock, - None => return Ok(None), - }; + let file_lock = some_or_return_ok_none!(FileLock::lock(lock_path, ttl_sec)?); + Ok(Some(SwapLock { file_lock })) } diff --git a/mm2src/mm2_main/src/lp_swap/taker_swap.rs b/mm2src/mm2_main/src/lp_swap/taker_swap.rs index 224891081b..0c409da54d 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_swap.rs @@ -907,7 +907,9 @@ impl TakerSwap { )])) }, }; - let maker_payment_spend_trade_fee_fut = self.maker_coin.get_receiver_trade_fee(stage.clone()); + let maker_payment_spend_trade_fee_fut = self + .maker_coin + .get_receiver_trade_fee(self.taker_amount.to_decimal(), stage.clone()); let maker_payment_spend_trade_fee = match maker_payment_spend_trade_fee_fut.compat().await { Ok(fee) => fee, Err(e) => { @@ -2076,7 +2078,7 @@ pub async fn check_balance_for_taker_swap( .await .mm_err(|e| CheckBalanceError::from_trade_preimage_error(e, my_coin.ticker()))?; let maker_payment_spend_trade_fee = other_coin - .get_receiver_trade_fee(stage) + .get_receiver_trade_fee(volume.to_decimal(), stage) .compat() .await .mm_err(|e| CheckBalanceError::from_trade_preimage_error(e, other_coin.ticker()))?; @@ -2171,7 +2173,7 @@ pub async fn taker_swap_trade_preimage( .await .mm_err(|e| TradePreimageRpcError::from_trade_preimage_error(e, my_coin_ticker))?; let other_coin_trade_fee = other_coin - .get_receiver_trade_fee(stage.clone()) + .get_receiver_trade_fee(my_coin_volume.to_decimal(), stage.clone()) .compat() .await .mm_err(|e| TradePreimageRpcError::from_trade_preimage_error(e, other_coin_ticker))?; diff --git a/mm2src/mm2_main/tests/mm2_tests/iris_swap_poc.rs b/mm2src/mm2_main/tests/mm2_tests/iris_swap.rs similarity index 95% rename from mm2src/mm2_main/tests/mm2_tests/iris_swap_poc.rs rename to mm2src/mm2_main/tests/mm2_tests/iris_swap.rs index f0df923cf8..6375b31e8c 100644 --- a/mm2src/mm2_main/tests/mm2_tests/iris_swap_poc.rs +++ b/mm2src/mm2_main/tests/mm2_tests/iris_swap.rs @@ -17,9 +17,7 @@ const TBNB_URLS: &[&str] = &["https://data-seed-prebsc-1-s3.binance.org:8545/"]; const TBNB_SWAP_CONTRACT: &str = "0xB1Ad803ea4F57401639c123000C75F5B66E4D123"; #[test] -#[ignore] -// cargo test mm2::mm2_tests::iris_swap_poc::test -- --exact --ignored -fn test() { +fn start_swap_operation() { let pairs = [ ("USDC-IBC-IRIS", "IRIS-NIMDA"), ("IRIS-NIMDA", "RICK"), @@ -88,17 +86,25 @@ pub async fn trade_base_rel_iris( .unwrap(); dbg!( - enable_tendermint(&mm_bob, "IRIS-TEST", &["IRIS-NIMDA", "USDC-IBC-IRIS"], &[ - "http://34.80.202.172:26657" - ]) + enable_tendermint( + &mm_bob, + "IRIS-TEST", + &["IRIS-NIMDA", "USDC-IBC-IRIS"], + &["http://34.80.202.172:26657"], + false + ) .await ); dbg!(enable_electrum(&mm_bob, "RICK", false, RICK_ELECTRUM_ADDRS).await); dbg!( - enable_tendermint(&mm_alice, "IRIS-TEST", &["IRIS-NIMDA", "USDC-IBC-IRIS"], &[ - "http://34.80.202.172:26657" - ]) + enable_tendermint( + &mm_alice, + "IRIS-TEST", + &["IRIS-NIMDA", "USDC-IBC-IRIS"], + &["http://34.80.202.172:26657"], + false + ) .await ); dbg!(enable_electrum(&mm_alice, "RICK", false, RICK_ELECTRUM_ADDRS).await); diff --git a/mm2src/mm2_main/tests/mm2_tests/mod.rs b/mm2src/mm2_main/tests/mm2_tests/mod.rs index 3593203251..3f6749d567 100644 --- a/mm2src/mm2_main/tests/mm2_tests/mod.rs +++ b/mm2src/mm2_main/tests/mm2_tests/mod.rs @@ -1,6 +1,6 @@ mod bch_and_slp_tests; mod best_orders_tests; -mod iris_swap_poc; +mod iris_swap; mod lightning_tests; mod lp_bot_tests; mod mm2_tests_inner; diff --git a/mm2src/mm2_main/tests/mm2_tests/tendermint_ibc_asset_tests.rs b/mm2src/mm2_main/tests/mm2_tests/tendermint_ibc_asset_tests.rs index bb0043690c..584c7c0c64 100644 --- a/mm2src/mm2_main/tests/mm2_tests/tendermint_ibc_asset_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/tendermint_ibc_asset_tests.rs @@ -23,6 +23,7 @@ fn test_iris_with_usdc_activation_balance_orderbook() { IRIS_TICKER, &[USDC_IBC_TICKER], IRIS_TESTNET_RPCS, + false, )); let response: RpcV2Response = serde_json::from_value(activation_result).unwrap(); diff --git a/mm2src/mm2_main/tests/mm2_tests/tendermint_tests.rs b/mm2src/mm2_main/tests/mm2_tests/tendermint_tests.rs index ee7dc6f216..84c6be91bb 100644 --- a/mm2src/mm2_main/tests/mm2_tests/tendermint_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/tendermint_tests.rs @@ -1,8 +1,8 @@ use common::block_on; use mm2_number::BigDecimal; use mm2_test_helpers::for_tests::{atom_testnet_conf, enable_tendermint, enable_tendermint_token, - iris_nimda_testnet_conf, iris_testnet_conf, my_balance, send_raw_transaction, - withdraw_v1, MarketMakerIt, Mm2TestConf}; + get_tendermint_my_tx_history, iris_nimda_testnet_conf, iris_testnet_conf, + my_balance, send_raw_transaction, withdraw_v1, MarketMakerIt, Mm2TestConf}; use mm2_test_helpers::structs::{MyBalanceResponse, RpcV2Response, TendermintActivationResult, TransactionDetails}; use serde_json::{self as json, json}; @@ -11,6 +11,8 @@ const ATOM_TEST_WITHDRAW_SEED: &str = "atom test withdraw seed"; const ATOM_TICKER: &str = "ATOM"; const ATOM_TENDERMINT_RPC_URLS: &[&str] = &["https://cosmos-testnet-rpc.allthatnode.com:26657"]; +const IRIS_TESTNET_RPC_URLS: &[&str] = &["http://34.80.202.172:26657"]; + #[test] fn test_tendermint_activation_and_balance() { let coins = json!([atom_testnet_conf()]); @@ -19,7 +21,13 @@ fn test_tendermint_activation_and_balance() { let conf = Mm2TestConf::seednode(ATOM_TEST_BALANCE_SEED, &coins); let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); - let activation_result = block_on(enable_tendermint(&mm, ATOM_TICKER, &[], ATOM_TENDERMINT_RPC_URLS)); + let activation_result = block_on(enable_tendermint( + &mm, + ATOM_TICKER, + &[], + ATOM_TENDERMINT_RPC_URLS, + false, + )); let result: RpcV2Response = json::from_value(activation_result).unwrap(); assert_eq!(result.result.address, expected_address); @@ -42,7 +50,13 @@ fn test_tendermint_withdraw() { let conf = Mm2TestConf::seednode(ATOM_TEST_WITHDRAW_SEED, &coins); let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); - let activation_res = block_on(enable_tendermint(&mm, ATOM_TICKER, &[], ATOM_TENDERMINT_RPC_URLS)); + let activation_res = block_on(enable_tendermint( + &mm, + ATOM_TICKER, + &[], + ATOM_TENDERMINT_RPC_URLS, + false, + )); println!("Activation {}", json::to_string(&activation_res).unwrap()); // just call withdraw without sending to check response correctness @@ -111,9 +125,7 @@ fn test_tendermint_token_activation_and_withdraw() { let conf = Mm2TestConf::seednode(TEST_SEED, &coins); let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); - let activation_res = block_on(enable_tendermint(&mm, platform_coin, &[], &[ - "http://34.80.202.172:26657", - ])); + let activation_res = block_on(enable_tendermint(&mm, platform_coin, &[], IRIS_TESTNET_RPC_URLS, false)); println!("Activation with assets {}", json::to_string(&activation_res).unwrap()); let activation_res = block_on(enable_tendermint_token(&mm, token)); @@ -184,3 +196,78 @@ fn test_tendermint_token_activation_and_withdraw() { let send_raw_tx = block_on(send_raw_transaction(&mm, token, &tx_details.tx_hex)); println!("Send raw tx {}", json::to_string(&send_raw_tx).unwrap()); } + +#[test] +fn test_tendermint_tx_history() { + const TEST_SEED: &str = "Vdo8Xt8pTAetRlMq3kV0LzE393eVYbPSn5Mhtw4p"; + const TX_FINISHED_LOG: &str = "Tx history fetching finished for IRIS-TEST."; + const TX_HISTORY_PAGE_LIMIT: usize = 50; + const IRIS_TEST_EXPECTED_TX_COUNT: u64 = 15; + const IRIS_NIMDA_EXPECTED_TX_COUNT: u64 = 10; + + let iris_test_constant_history_txs = include_str!("../../../mm2_test_helpers/dummy_files/iris_test_history.json"); + let iris_test_constant_history_txs: Vec = + serde_json::from_str(iris_test_constant_history_txs).unwrap(); + + let iris_nimda_constant_history_txs = include_str!("../../../mm2_test_helpers/dummy_files/iris_nimda_history.json"); + let iris_nimda_constant_history_txs: Vec = + serde_json::from_str(iris_nimda_constant_history_txs).unwrap(); + + let coins = json!([iris_testnet_conf(), iris_nimda_testnet_conf()]); + let platform_coin = coins[0]["coin"].as_str().unwrap(); + let token = coins[1]["coin"].as_str().unwrap(); + + let conf = Mm2TestConf::seednode(TEST_SEED, &coins); + let mut mm = block_on(MarketMakerIt::start_async(conf.conf, conf.rpc_password, None)).unwrap(); + + block_on(enable_tendermint( + &mm, + platform_coin, + &[token], + IRIS_TESTNET_RPC_URLS, + true, + )); + + if let Err(_) = block_on(mm.wait_for_log(60., |log| log.contains(TX_FINISHED_LOG))) { + println!("{}", mm.log_as_utf8().unwrap()); + assert!(false, "Tx history didn't finish which is not expected"); + } + + // testing IRIS-TEST history + let iris_tx_history_response = block_on(get_tendermint_my_tx_history( + &mm, + platform_coin, + TX_HISTORY_PAGE_LIMIT, + 1, + )); + let total_txs = iris_tx_history_response["result"]["total"].as_u64().unwrap(); + assert_eq!(total_txs, IRIS_TEST_EXPECTED_TX_COUNT); + + let mut iris_txs_from_request = iris_tx_history_response["result"]["transactions"].clone(); + for i in 0..IRIS_TEST_EXPECTED_TX_COUNT { + iris_txs_from_request[i as usize] + .as_object_mut() + .unwrap() + .remove("confirmations"); + } + let iris_txs_from_request: Vec = serde_json::from_value(iris_txs_from_request).unwrap(); + assert_eq!(iris_test_constant_history_txs, iris_txs_from_request); + + // testing IRIS-NIMDA history + let nimda_tx_history_response = block_on(get_tendermint_my_tx_history(&mm, token, TX_HISTORY_PAGE_LIMIT, 1)); + let total_txs = nimda_tx_history_response["result"]["total"].as_u64().unwrap(); + assert_eq!(total_txs, IRIS_NIMDA_EXPECTED_TX_COUNT); + + let mut nimda_txs_from_request = nimda_tx_history_response["result"]["transactions"].clone(); + for i in 0..IRIS_NIMDA_EXPECTED_TX_COUNT { + nimda_txs_from_request[i as usize] + .as_object_mut() + .unwrap() + .remove("confirmations"); + } + let nimda_txs_from_request: Vec = serde_json::from_value(nimda_txs_from_request).unwrap(); + + assert_eq!(iris_nimda_constant_history_txs, nimda_txs_from_request); + + block_on(mm.stop()).unwrap(); +} diff --git a/mm2src/mm2_test_helpers/dummy_files/iris_nimda_history.json b/mm2src/mm2_test_helpers/dummy_files/iris_nimda_history.json new file mode 100644 index 0000000000..e5d82afc21 --- /dev/null +++ b/mm2src/mm2_test_helpers/dummy_files/iris_nimda_history.json @@ -0,0 +1,276 @@ +[ + { + "tx_hex": "0a2a696161317a36303967397a386566346a6c6b63666767333663326c6a67636d6e6135306c32796a726130122a696161316530727838376d646a37397a656a65777563346a6737716c39756432323836673275733866322a0c0a036e696d1205313030303032403838613735653161306166373662636161386266353566313437616266616262313362323964646333316465323161373431363835333966373135643032343240980c", + "tx_hash": "B06D02A1449B07E89D15E709C347E0EE72A944687942E5A10467EC096D067FE7", + "from": [ + "iaa1z609g9z8ef4jlkcfgg36c2ljgcmna50l2yjra0" + ], + "to": [], + "total_amount": "0.01", + "spent_by_me": "0.01", + "received_by_me": "0", + "my_balance_change": "-0.01", + "block_height": 6175147, + "timestamp": 1669934633, + "fee_details": { + "type": "Tendermint", + "coin": "IRIS-TEST", + "amount": "0.027385", + "gas_limit": 100000 + }, + "coin": "IRIS-NIMDA", + "internal_id": "3930374535314439384537304239343431413230443630420000000000000000", + "transaction_type": { + "CustomTendermintMsg": { + "msg_type": "SendHtlcAmount", + "token_id": "e13c266c61de5e79cf37a99b94d4d4462d6857fe8a5dbdde9e041ae01b14a7db" + } + } + }, + { + "tx_hex": "0a2a696161317a36303967397a386566346a6c6b63666767333663326c6a67636d6e6135306c32796a726130122a696161316530727838376d646a37397a656a65777563346a6737716c39756432323836673275733866322a0c0a036e696d1205313030303032406466313465616234366366623230663736613465336233393537656234326137666130646435366137386466623038656134323034666334383762663536323040980c", + "tx_hash": "26E7CA4A95C70316195E8C8A3249351710B1BFE89AE9B910463BC8280C0AA9CF", + "from": [ + "iaa1z609g9z8ef4jlkcfgg36c2ljgcmna50l2yjra0" + ], + "to": [], + "total_amount": "0.01", + "spent_by_me": "0.01", + "received_by_me": "0", + "my_balance_change": "-0.01", + "block_height": 6175115, + "timestamp": 1669934472, + "fee_details": { + "type": "Tendermint", + "coin": "IRIS-TEST", + "amount": "0.028433", + "gas_limit": 100000 + }, + "coin": "IRIS-NIMDA", + "internal_id": "4138433845353931363133303743353941344143374536320000000000000000", + "transaction_type": { + "CustomTendermintMsg": { + "msg_type": "SendHtlcAmount", + "token_id": "e13c266c61de5e79cf37a99b94d4d4462d6857fe8a5dbdde9e041ae01b14a7db" + } + } + }, + { + "tx_hex": "0a2a696161317a36303967397a386566346a6c6b63666767333663326c6a67636d6e6135306c32796a7261301240364636394643303235393632413032463736363741414142393932313245393434454244444436434143314235354431443336333139383341413433454145421a4030646432663130353032343463386235316363636239316535363734386336373032343634393263383465323835656234353365363166373630623730356432", + "tx_hash": "459F88CACCD57756DDFE2164AABBA2C3D463D0FBA5D85DFD79488416A9BD54FC", + "from": [ + "iaa1z609g9z8ef4jlkcfgg36c2ljgcmna50l2yjra0" + ], + "to": [], + "total_amount": "0.01", + "spent_by_me": "0", + "received_by_me": "0.01", + "my_balance_change": "0.01", + "block_height": 6175070, + "timestamp": 1669934246, + "fee_details": { + "type": "Tendermint", + "coin": "IRIS-TEST", + "amount": "0.026403", + "gas_limit": 100000 + }, + "coin": "IRIS-NIMDA", + "internal_id": "3436313245464444363537373544434341433838463935340000000000000000", + "transaction_type": { + "CustomTendermintMsg": { + "msg_type": "SignClaimHtlc", + "token_id": "e13c266c61de5e79cf37a99b94d4d4462d6857fe8a5dbdde9e041ae01b14a7db" + } + } + }, + { + "tx_hex": "0a2a696161317a36303967397a386566346a6c6b63666767333663326c6a67636d6e6135306c32796a726130122a696161316530727838376d646a37397a656a65777563346a6737716c39756432323836673275733866322a0c0a036e696d1205313030303032403738646661636564613462346362373336366362626638643436336237393061666566363566656538613764356464356264616633613365303766343330623640980c", + "tx_hash": "25D9A1F1C9383AC7D56E3E19F3D6B9ED3F52F7D0E9225ABBEA0258FF75E28FC2", + "from": [ + "iaa1z609g9z8ef4jlkcfgg36c2ljgcmna50l2yjra0" + ], + "to": [], + "total_amount": "0.01", + "spent_by_me": "0.01", + "received_by_me": "0", + "my_balance_change": "-0.01", + "block_height": 6148309, + "timestamp": 1669798875, + "fee_details": { + "type": "Tendermint", + "coin": "IRIS-TEST", + "amount": "0.028433", + "gas_limit": 100000 + }, + "coin": "IRIS-NIMDA", + "internal_id": "3931453345363544374341333833394331463141394435320000000000000000", + "transaction_type": { + "CustomTendermintMsg": { + "msg_type": "SendHtlcAmount", + "token_id": "e13c266c61de5e79cf37a99b94d4d4462d6857fe8a5dbdde9e041ae01b14a7db" + } + } + }, + { + "tx_hex": "0a2a696161317a36303967397a386566346a6c6b63666767333663326c6a67636d6e6135306c32796a726130122a696161316567307167617a37336a737676727676747a713478383233686d7a387161706c64643078347a1a0a0a036e696d1203313030", + "tx_hash": "D83D8143CAD8C225F3BC24C6DCA324DB2F63C97A21AE2C030328266E9415E50A", + "from": [ + "iaa1z609g9z8ef4jlkcfgg36c2ljgcmna50l2yjra0" + ], + "to": [ + "iaa1eg0qgaz73jsvvrvvtzq4x823hmz8qapldd0x4z" + ], + "total_amount": "0.0001", + "spent_by_me": "0.0001", + "received_by_me": "0", + "my_balance_change": "-0.0001", + "block_height": 6148307, + "timestamp": 1669798865, + "fee_details": { + "type": "Tendermint", + "coin": "IRIS-TEST", + "amount": "0.025763", + "gas_limit": 100000 + }, + "coin": "IRIS-NIMDA", + "internal_id": "3643343243423346353232433844414333343138443338440000000000000000", + "transaction_type": { + "TokenTransfer": "e13c266c61de5e79cf37a99b94d4d4462d6857fe8a5dbdde9e041ae01b14a7db" + } + }, + { + "tx_hex": "0a2a696161316530727838376d646a37397a656a65777563346a6737716c3975643232383667327573386632122a696161317a36303967397a386566346a6c6b63666767333663326c6a67636d6e6135306c32796a7261301a0e0a036e696d120731303030303030", + "tx_hash": "7A43C30967D30B12B749111F217A670C0B47B7C5F272BCD82C442C1554351CB7", + "from": [ + "iaa1e0rx87mdj79zejewuc4jg7ql9ud2286g2us8f2" + ], + "to": [ + "iaa1z609g9z8ef4jlkcfgg36c2ljgcmna50l2yjra0" + ], + "total_amount": "2", + "spent_by_me": "0", + "received_by_me": "2", + "my_balance_change": "2", + "block_height": 6148055, + "timestamp": 1669797598, + "fee_details": { + "type": "Tendermint", + "coin": "IRIS-TEST", + "amount": "0.022114", + "gas_limit": 100000 + }, + "coin": "IRIS-NIMDA", + "internal_id": "4631313139343742323142303344373639303343333441370000000000000000", + "transaction_type": { + "TokenTransfer": "e13c266c61de5e79cf37a99b94d4d4462d6857fe8a5dbdde9e041ae01b14a7db" + } + }, + { + "tx_hex": "0a2a696161316530727838376d646a37397a656a65777563346a6737716c3975643232383667327573386632122a696161317a36303967397a386566346a6c6b63666767333663326c6a67636d6e6135306c32796a7261301a0e0a036e696d1207313030303030301a100a05756e79616e120731303030303030", + "tx_hash": "3057F075161ECD149F4F5A629F852D5E7E57411DECE7AD35CC7D11E7EB06E0BF", + "from": [ + "iaa1e0rx87mdj79zejewuc4jg7ql9ud2286g2us8f2" + ], + "to": [ + "iaa1z609g9z8ef4jlkcfgg36c2ljgcmna50l2yjra0" + ], + "total_amount": "2", + "spent_by_me": "0", + "received_by_me": "2", + "my_balance_change": "2", + "block_height": 6148039, + "timestamp": 1669797517, + "fee_details": { + "type": "Tendermint", + "coin": "IRIS-TEST", + "amount": "0.026861", + "gas_limit": 100000 + }, + "coin": "IRIS-NIMDA", + "internal_id": "3236413546344639343144434531363135373046373530330000000000000000", + "transaction_type": { + "TokenTransfer": "e13c266c61de5e79cf37a99b94d4d4462d6857fe8a5dbdde9e041ae01b14a7db" + } + }, + { + "tx_hex": "0a2a696161316530727838376d646a37397a656a65777563346a6737716c3975643232383667327573386632122a696161317a36303967397a386566346a6c6b63666767333663326c6a67636d6e6135306c32796a7261301a0e0a036e696d1207313030303030301a100a05756e79616e120731303030303030", + "tx_hash": "14FA04603223A5753DD60ADCE2593FD4AFA0A386E008AF7130916152F11D5748", + "from": [ + "iaa1e0rx87mdj79zejewuc4jg7ql9ud2286g2us8f2" + ], + "to": [ + "iaa1z609g9z8ef4jlkcfgg36c2ljgcmna50l2yjra0" + ], + "total_amount": "1", + "spent_by_me": "0", + "received_by_me": "1", + "my_balance_change": "1", + "block_height": 6148028, + "timestamp": 1669797462, + "fee_details": { + "type": "Tendermint", + "coin": "IRIS-TEST", + "amount": "0.026861", + "gas_limit": 100000 + }, + "coin": "IRIS-NIMDA", + "internal_id": "4344413036444433353735413332323330363430414634310000000000000000", + "transaction_type": { + "TokenTransfer": "e13c266c61de5e79cf37a99b94d4d4462d6857fe8a5dbdde9e041ae01b14a7db" + } + }, + { + "tx_hex": "0a2a696161316530727838376d646a37397a656a65777563346a6737716c3975643232383667327573386632122a696161317a36303967397a386566346a6c6b63666767333663326c6a67636d6e6135306c32796a7261301a0e0a036e696d120731303030303030", + "tx_hash": "3DC1E9372E873C69E009B89C3DE232E9E12D0FC941BB98582B729A5D64DB6308", + "from": [ + "iaa1e0rx87mdj79zejewuc4jg7ql9ud2286g2us8f2" + ], + "to": [ + "iaa1z609g9z8ef4jlkcfgg36c2ljgcmna50l2yjra0" + ], + "total_amount": "1", + "spent_by_me": "0", + "received_by_me": "1", + "my_balance_change": "1", + "block_height": 6148010, + "timestamp": 1669797371, + "fee_details": { + "type": "Tendermint", + "coin": "IRIS-TEST", + "amount": "0.022114", + "gas_limit": 100000 + }, + "coin": "IRIS-NIMDA", + "internal_id": "4339384239303045393643333738453237333945314344330000000000000000", + "transaction_type": { + "TokenTransfer": "e13c266c61de5e79cf37a99b94d4d4462d6857fe8a5dbdde9e041ae01b14a7db" + } + }, + { + "tx_hex": "0a2a696161316530727838376d646a37397a656a65777563346a6737716c3975643232383667327573386632122a696161317a36303967397a386566346a6c6b63666767333663326c6a67636d6e6135306c32796a7261301a0e0a036e696d120731303030303030", + "tx_hash": "E75EF2C47E8627B18840606A8F3502D5643984334B2FB5D8C69FF3D1F2B48473", + "from": [ + "iaa1e0rx87mdj79zejewuc4jg7ql9ud2286g2us8f2" + ], + "to": [ + "iaa1z609g9z8ef4jlkcfgg36c2ljgcmna50l2yjra0" + ], + "total_amount": "1", + "spent_by_me": "0", + "received_by_me": "1", + "my_balance_change": "1", + "block_height": 6147988, + "timestamp": 1669797261, + "fee_details": { + "type": "Tendermint", + "coin": "IRIS-TEST", + "amount": "0.02316", + "gas_limit": 100000 + }, + "coin": "IRIS-NIMDA", + "internal_id": "4136303630343838314237323638453734433246453537450000000000000000", + "transaction_type": { + "TokenTransfer": "e13c266c61de5e79cf37a99b94d4d4462d6857fe8a5dbdde9e041ae01b14a7db" + } + } +] \ No newline at end of file diff --git a/mm2src/mm2_test_helpers/dummy_files/iris_test_history.json b/mm2src/mm2_test_helpers/dummy_files/iris_test_history.json new file mode 100644 index 0000000000..4e3cdb1fe6 --- /dev/null +++ b/mm2src/mm2_test_helpers/dummy_files/iris_test_history.json @@ -0,0 +1,385 @@ +[ + { + "tx_hex": "0a2a696161317a36303967397a386566346a6c6b63666767333663326c6a67636d6e6135306c32796a7261301240353136384331423544333836313243373835414530364141344130363036443537433444454343373939333834314338344544313932363138303941333335361a4064323032323032336639656565633338393561326637323063383463613363383638653632656232393139666262613732376262333464623434313739333232", + "tx_hash": "072AB56BE582BB1EF82193D6EDF855A0311E53D9240EBEEE5E7844898C54DADD", + "from": [ + "iaa1z609g9z8ef4jlkcfgg36c2ljgcmna50l2yjra0" + ], + "to": [], + "total_amount": "0.037215", + "spent_by_me": "0.027215", + "received_by_me": "0.01", + "my_balance_change": "-0.017215", + "block_height": 6175150, + "timestamp": 1669934648, + "fee_details": { + "type": "Tendermint", + "coin": "IRIS-TEST", + "amount": "0.027215", + "gas_limit": 100000 + }, + "coin": "IRIS-TEST", + "internal_id": "3644333931323846453142423238354542363542413237300000000000000000", + "transaction_type": { + "CustomTendermintMsg": { + "msg_type": "SignClaimHtlc", + "token_id": null + } + } + }, + { + "tx_hex": "0a2a696161317a36303967397a386566346a6c6b63666767333663326c6a67636d6e6135306c32796a726130122a696161316530727838376d646a37397a656a65777563346a6737716c39756432323836673275733866322a0c0a036e696d1205313030303032403838613735653161306166373662636161386266353566313437616266616262313362323964646333316465323161373431363835333966373135643032343240980c", + "tx_hash": "B06D02A1449B07E89D15E709C347E0EE72A944687942E5A10467EC096D067FE7", + "from": [ + "iaa1z609g9z8ef4jlkcfgg36c2ljgcmna50l2yjra0" + ], + "to": [], + "total_amount": "0.027385", + "spent_by_me": "0.027385", + "received_by_me": "0", + "my_balance_change": "-0.027385", + "block_height": 6175147, + "timestamp": 1669934633, + "fee_details": { + "type": "Tendermint", + "coin": "IRIS-TEST", + "amount": "0.027385", + "gas_limit": 100000 + }, + "coin": "IRIS-TEST", + "internal_id": "0000000000000000423036443032413134343942303745383944313545373039", + "transaction_type": "FeeForTokenTx" + }, + { + "tx_hex": "0a2a696161317a36303967397a386566346a6c6b63666767333663326c6a67636d6e6135306c32796a726130122a696161316530727838376d646a37397a656a65777563346a6737716c39756432323836673275733866322a0c0a036e696d1205313030303032406466313465616234366366623230663736613465336233393537656234326137666130646435366137386466623038656134323034666334383762663536323040980c", + "tx_hash": "26E7CA4A95C70316195E8C8A3249351710B1BFE89AE9B910463BC8280C0AA9CF", + "from": [ + "iaa1z609g9z8ef4jlkcfgg36c2ljgcmna50l2yjra0" + ], + "to": [], + "total_amount": "0.028433", + "spent_by_me": "0.028433", + "received_by_me": "0", + "my_balance_change": "-0.028433", + "block_height": 6175115, + "timestamp": 1669934472, + "fee_details": { + "type": "Tendermint", + "coin": "IRIS-TEST", + "amount": "0.028433", + "gas_limit": 100000 + }, + "coin": "IRIS-TEST", + "internal_id": "0000000000000000323645374341344139354337303331363139354538433841", + "transaction_type": "FeeForTokenTx" + }, + { + "tx_hex": "0a2a696161317a36303967397a386566346a6c6b63666767333663326c6a67636d6e6135306c32796a7261301240364636394643303235393632413032463736363741414142393932313245393434454244444436434143314235354431443336333139383341413433454145421a4030646432663130353032343463386235316363636239316535363734386336373032343634393263383465323835656234353365363166373630623730356432", + "tx_hash": "459F88CACCD57756DDFE2164AABBA2C3D463D0FBA5D85DFD79488416A9BD54FC", + "from": [ + "iaa1z609g9z8ef4jlkcfgg36c2ljgcmna50l2yjra0" + ], + "to": [], + "total_amount": "0.026403", + "spent_by_me": "0.026403", + "received_by_me": "0", + "my_balance_change": "-0.026403", + "block_height": 6175070, + "timestamp": 1669934246, + "fee_details": { + "type": "Tendermint", + "coin": "IRIS-TEST", + "amount": "0.026403", + "gas_limit": 100000 + }, + "coin": "IRIS-TEST", + "internal_id": "0000000000000000343539463838434143434435373735364444464532313634", + "transaction_type": "FeeForTokenTx" + }, + { + "tx_hex": "0a2a696161317a36303967397a386566346a6c6b63666767333663326c6a67636d6e6135306c32796a726130122a696161316530727838376d646a37397a656a65777563346a6737716c39756432323836673275733866322a0e0a05756e79616e1205313030303032403835306338333863393930363661323466373330363866623537336638356337353836323036613733366330353331323030396233323437313834643130656340980c", + "tx_hash": "8A4BD1D952AC833C4C11313580CAC61374C80D1641D6EBD459DE73AFA7DF47F2", + "from": [ + "iaa1z609g9z8ef4jlkcfgg36c2ljgcmna50l2yjra0" + ], + "to": [], + "total_amount": "0.03856", + "spent_by_me": "0.03856", + "received_by_me": "0", + "my_balance_change": "-0.03856", + "block_height": 6175068, + "timestamp": 1669934236, + "fee_details": { + "type": "Tendermint", + "coin": "IRIS-TEST", + "amount": "0.02856", + "gas_limit": 100000 + }, + "coin": "IRIS-TEST", + "internal_id": "3533313331314334433333384341323539443144423441380000000000000000", + "transaction_type": { + "CustomTendermintMsg": { + "msg_type": "SendHtlcAmount", + "token_id": null + } + } + }, + { + "tx_hex": "0a2a696161317a36303967397a386566346a6c6b63666767333663326c6a67636d6e6135306c32796a726130122a696161316567307167617a37336a737676727676747a713478383233686d7a387161706c64643078347a1a0c0a05756e79616e1203313030", + "tx_hash": "734623F6744A44ABA5D551233B409400CF69EC8858302E9AE868A8400E925D51", + "from": [ + "iaa1z609g9z8ef4jlkcfgg36c2ljgcmna50l2yjra0" + ], + "to": [ + "iaa1eg0qgaz73jsvvrvvtzq4x823hmz8qapldd0x4z" + ], + "total_amount": "0.022185", + "spent_by_me": "0.022185", + "received_by_me": "0", + "my_balance_change": "-0.022185", + "block_height": 6175065, + "timestamp": 1669934221, + "fee_details": { + "type": "Tendermint", + "coin": "IRIS-TEST", + "amount": "0.022085", + "gas_limit": 100000 + }, + "coin": "IRIS-TEST", + "internal_id": "3332313535443541424134344134343736463332363433370000000000000000", + "transaction_type": "StandardTransfer" + }, + { + "tx_hex": "0a2a696161316530727838376d646a37397a656a65777563346a6737716c39756432323836673275733866321240413231314538373937463739314534434439333241423539424239333545324138354644453843393638374143463644383345373933424545373438463936431a4037626266353433313865643235643530366131393565363432636433343736613835303438393833623030346334653534336663313938313866333130353237", + "tx_hash": "252D31A8AFEF8B4C8D9058E9EFAF4ED5A41DB7C032B3849A87375F1DE2B8B9C1", + "from": [ + "iaa1z609g9z8ef4jlkcfgg36c2ljgcmna50l2yjra0" + ], + "to": [], + "total_amount": "0.000001", + "spent_by_me": "0", + "received_by_me": "0.000001", + "my_balance_change": "0.000001", + "block_height": 6173346, + "timestamp": 1669925588, + "fee_details": { + "type": "Tendermint", + "coin": "IRIS-TEST", + "amount": "0.026446", + "gas_limit": 100000 + }, + "coin": "IRIS-TEST", + "internal_id": "3945383530394438433442384645464138413133443235320000000000000000", + "transaction_type": { + "CustomTendermintMsg": { + "msg_type": "ClaimHtlcAmount", + "token_id": null + } + } + }, + { + "tx_hex": "0a2a696161317a36303967397a386566346a6c6b63666767333663326c6a67636d6e6135306c32796a7261301240384636304534433336384335344539343233344145424135394331344531384146413138443437303530314434383944444641364539363635444135323738451a4064363034613132626531616239643966343531306435343832666232323762613530363238373531326466333561626337303730313763366439306165303837", + "tx_hash": "6BA67D4C7A0F48E5D85EE18EF5A649ED456B539D902828D56849B57DCFF5914B", + "from": [ + "iaa1z609g9z8ef4jlkcfgg36c2ljgcmna50l2yjra0" + ], + "to": [], + "total_amount": "0.026426", + "spent_by_me": "0.026426", + "received_by_me": "0", + "my_balance_change": "-0.026426", + "block_height": 6148319, + "timestamp": 1669798925, + "fee_details": { + "type": "Tendermint", + "coin": "IRIS-TEST", + "amount": "0.026426", + "gas_limit": 100000 + }, + "coin": "IRIS-TEST", + "internal_id": "4538314545353844354538344630413743344437364142360000000000000000", + "transaction_type": { + "CustomTendermintMsg": { + "msg_type": "SignClaimHtlc", + "token_id": null + } + } + }, + { + "tx_hex": "0a2a696161317a36303967397a386566346a6c6b63666767333663326c6a67636d6e6135306c32796a726130122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a762a0a0a05756e79616e12013132403834373331663565393739643965616263343838393234356131613165663932396430386437383832353163353133633834303632373535313032343765326540e807", + "tx_hash": "F1C633D16C1578A16450555A06349989A4B1E2223D7AAA2FE12396A5F855FD9B", + "from": [ + "iaa1z609g9z8ef4jlkcfgg36c2ljgcmna50l2yjra0" + ], + "to": [], + "total_amount": "0.028456", + "spent_by_me": "0.028456", + "received_by_me": "0", + "my_balance_change": "-0.028456", + "block_height": 6148318, + "timestamp": 1669798920, + "fee_details": { + "type": "Tendermint", + "coin": "IRIS-TEST", + "amount": "0.028455", + "gas_limit": 100000 + }, + "coin": "IRIS-TEST", + "internal_id": "4135353530353436314138373531433631443333364331460000000000000000", + "transaction_type": { + "CustomTendermintMsg": { + "msg_type": "SendHtlcAmount", + "token_id": null + } + } + }, + { + "tx_hex": "0a2a696161317a36303967397a386566346a6c6b63666767333663326c6a67636d6e6135306c32796a7261301240304544424136304632423642383739324133333739423232453445323144373243343145413530464436353331433243383445413135353937303834373432391a4034353138383334373164316536383461323334663362323063626638646137393338636330643038383562626237326266303837363465386431356536353138", + "tx_hash": "10D92256317083E49F06A9DD2184714DEEE9C55A505CFEF037BE8E87E9FB82ED", + "from": [ + "iaa1z609g9z8ef4jlkcfgg36c2ljgcmna50l2yjra0" + ], + "to": [], + "total_amount": "0.03648", + "spent_by_me": "0.02648", + "received_by_me": "0.01", + "my_balance_change": "-0.01648", + "block_height": 6148311, + "timestamp": 1669798885, + "fee_details": { + "type": "Tendermint", + "coin": "IRIS-TEST", + "amount": "0.02648", + "gas_limit": 100000 + }, + "coin": "IRIS-TEST", + "internal_id": "4444394136304639344533383037313336353232394430310000000000000000", + "transaction_type": { + "CustomTendermintMsg": { + "msg_type": "SignClaimHtlc", + "token_id": null + } + } + }, + { + "tx_hex": "0a2a696161317a36303967397a386566346a6c6b63666767333663326c6a67636d6e6135306c32796a726130122a696161316530727838376d646a37397a656a65777563346a6737716c39756432323836673275733866322a0c0a036e696d1205313030303032403738646661636564613462346362373336366362626638643436336237393061666566363566656538613764356464356264616633613365303766343330623640980c", + "tx_hash": "25D9A1F1C9383AC7D56E3E19F3D6B9ED3F52F7D0E9225ABBEA0258FF75E28FC2", + "from": [ + "iaa1z609g9z8ef4jlkcfgg36c2ljgcmna50l2yjra0" + ], + "to": [], + "total_amount": "0.028433", + "spent_by_me": "0.028433", + "received_by_me": "0", + "my_balance_change": "-0.028433", + "block_height": 6148309, + "timestamp": 1669798875, + "fee_details": { + "type": "Tendermint", + "coin": "IRIS-TEST", + "amount": "0.028433", + "gas_limit": 100000 + }, + "coin": "IRIS-TEST", + "internal_id": "0000000000000000323544394131463143393338334143374435364533453139", + "transaction_type": "FeeForTokenTx" + }, + { + "tx_hex": "0a2a696161317a36303967397a386566346a6c6b63666767333663326c6a67636d6e6135306c32796a726130122a696161316567307167617a37336a737676727676747a713478383233686d7a387161706c64643078347a1a0a0a036e696d1203313030", + "tx_hash": "D83D8143CAD8C225F3BC24C6DCA324DB2F63C97A21AE2C030328266E9415E50A", + "from": [ + "iaa1z609g9z8ef4jlkcfgg36c2ljgcmna50l2yjra0" + ], + "to": [], + "total_amount": "0.025763", + "spent_by_me": "0.025763", + "received_by_me": "0", + "my_balance_change": "-0.025763", + "block_height": 6148307, + "timestamp": 1669798865, + "fee_details": { + "type": "Tendermint", + "coin": "IRIS-TEST", + "amount": "0.025763", + "gas_limit": 100000 + }, + "coin": "IRIS-TEST", + "internal_id": "0000000000000000443833443831343343414438433232354633424332344336", + "transaction_type": "FeeForTokenTx" + }, + { + "tx_hex": "0a2a696161316530727838376d646a37397a656a65777563346a6737716c3975643232383667327573386632122a696161317a36303967397a386566346a6c6b63666767333663326c6a67636d6e6135306c32796a7261301a0e0a036e696d1207313030303030301a100a05756e79616e120731303030303030", + "tx_hash": "3057F075161ECD149F4F5A629F852D5E7E57411DECE7AD35CC7D11E7EB06E0BF", + "from": [ + "iaa1e0rx87mdj79zejewuc4jg7ql9ud2286g2us8f2" + ], + "to": [ + "iaa1z609g9z8ef4jlkcfgg36c2ljgcmna50l2yjra0" + ], + "total_amount": "2", + "spent_by_me": "0", + "received_by_me": "2", + "my_balance_change": "2", + "block_height": 6148039, + "timestamp": 1669797517, + "fee_details": { + "type": "Tendermint", + "coin": "IRIS-TEST", + "amount": "0.026861", + "gas_limit": 100000 + }, + "coin": "IRIS-TEST", + "internal_id": "3236413546344639343144434531363135373046373530330000000000000001", + "transaction_type": "StandardTransfer" + }, + { + "tx_hex": "0a2a696161316530727838376d646a37397a656a65777563346a6737716c3975643232383667327573386632122a696161317a36303967397a386566346a6c6b63666767333663326c6a67636d6e6135306c32796a7261301a0e0a036e696d1207313030303030301a100a05756e79616e120731303030303030", + "tx_hash": "14FA04603223A5753DD60ADCE2593FD4AFA0A386E008AF7130916152F11D5748", + "from": [ + "iaa1e0rx87mdj79zejewuc4jg7ql9ud2286g2us8f2" + ], + "to": [ + "iaa1z609g9z8ef4jlkcfgg36c2ljgcmna50l2yjra0" + ], + "total_amount": "1", + "spent_by_me": "0", + "received_by_me": "1", + "my_balance_change": "1", + "block_height": 6148028, + "timestamp": 1669797462, + "fee_details": { + "type": "Tendermint", + "coin": "IRIS-TEST", + "amount": "0.026861", + "gas_limit": 100000 + }, + "coin": "IRIS-TEST", + "internal_id": "4344413036444433353735413332323330363430414634310000000000000001", + "transaction_type": "StandardTransfer" + }, + { + "tx_hex": "0a2a696161317039703230667468306c7665647634736d7733327339377079386e74657230716e7774727538122a696161317a36303967397a386566346a6c6b63666767333663326c6a67636d6e6135306c32796a7261301a120a05756e79616e1209313030303030303030", + "tx_hash": "AEABA70DB11EAE7D4C47E9F892C99280EF64E0DA58E1B0286DC8A190166A1218", + "from": [ + "iaa1p9p20fth0lvedv4smw32s97py8nter0qnwtru8" + ], + "to": [ + "iaa1z609g9z8ef4jlkcfgg36c2ljgcmna50l2yjra0" + ], + "total_amount": "100", + "spent_by_me": "0", + "received_by_me": "100", + "my_balance_change": "100", + "block_height": 6147959, + "timestamp": 1669797114, + "fee_details": { + "type": "Tendermint", + "coin": "IRIS-TEST", + "amount": "0.2", + "gas_limit": 100000 + }, + "coin": "IRIS-TEST", + "internal_id": "3846394537344334443745414531314244303741424145410000000000000000", + "transaction_type": "StandardTransfer" + } +] \ No newline at end of file diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index 2a18804203..0ea3824fb5 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -386,7 +386,7 @@ pub fn morty_conf() -> Json { pub fn atom_testnet_conf() -> Json { json!({ "coin":"ATOM", - "avg_block_time": 5, + "avg_blocktime": 5, "protocol":{ "type":"TENDERMINT", "protocol_data": { @@ -512,7 +512,7 @@ pub fn eth_jst_testnet_conf() -> Json { pub fn iris_testnet_conf() -> Json { json!({ "coin": "IRIS-TEST", - "avg_block_time": 5, + "avg_blocktime": 5, "derivation_path": "m/44'/566'", "protocol":{ "type":"TENDERMINT", @@ -2180,7 +2180,13 @@ pub async fn my_balance(mm: &MarketMakerIt, coin: &str) -> Json { json::from_str(&request.1).unwrap() } -pub async fn enable_tendermint(mm: &MarketMakerIt, coin: &str, ibc_assets: &[&str], rpc_urls: &[&str]) -> Json { +pub async fn enable_tendermint( + mm: &MarketMakerIt, + coin: &str, + ibc_assets: &[&str], + rpc_urls: &[&str], + tx_history: bool, +) -> Json { let ibc_requests: Vec<_> = ibc_assets.iter().map(|ticker| json!({ "ticker": ticker })).collect(); let request = json!({ @@ -2191,6 +2197,7 @@ pub async fn enable_tendermint(mm: &MarketMakerIt, coin: &str, ibc_assets: &[&st "ticker": coin, "tokens_params": ibc_requests, "rpc_urls": rpc_urls, + "tx_history": tx_history } }); println!( @@ -2209,6 +2216,36 @@ pub async fn enable_tendermint(mm: &MarketMakerIt, coin: &str, ibc_assets: &[&st json::from_str(&request.1).unwrap() } +pub async fn get_tendermint_my_tx_history(mm: &MarketMakerIt, coin: &str, limit: usize, page_number: usize) -> Json { + let request = json!({ + "userpass": mm.userpass, + "method": "my_tx_history", + "mmrpc": "2.0", + "params": { + "coin": coin, + "limit": limit, + "paging_options": { + "PageNumber": page_number + }, + } + }); + println!( + "tendermint 'my_tx_history' request {}", + json::to_string(&request).unwrap() + ); + + let request = mm.rpc(&request).await.unwrap(); + assert_eq!( + request.0, + StatusCode::OK, + "tendermint 'my_tx_history' failed: {}", + request.1 + ); + + println!("tendermint 'my_tx_history' response {}", request.1); + json::from_str(&request.1).unwrap() +} + pub async fn enable_tendermint_token(mm: &MarketMakerIt, coin: &str) -> Json { let request = json!({ "userpass": mm.userpass, diff --git a/mm2src/mm2_test_helpers/src/structs.rs b/mm2src/mm2_test_helpers/src/structs.rs index 61544c4266..1a62f71c75 100644 --- a/mm2src/mm2_test_helpers/src/structs.rs +++ b/mm2src/mm2_test_helpers/src/structs.rs @@ -535,15 +535,27 @@ pub mod raw_transaction_error { } } +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub enum CustomTendermintMsgType { + SendHtlcAmount, + ClaimHtlcAmount, + SignClaimHtlc, +} + #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub enum TransactionType { StakingDelegation, RemoveDelegation, StandardTransfer, TokenTransfer(String), + FeeForTokenTx, + CustomTendermintMsg { + msg_type: CustomTendermintMsgType, + token_id: Option, + }, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct TransactionDetails { pub tx_hex: String,