diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index b75e994b96..7f51d75ebb 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -12,7 +12,7 @@ enable-sia = [ ] default = [] run-docker-tests = [] -for-tests = [] +for-tests = ["dep:mocktopus"] [lib] path = "lp_coins.rs" @@ -72,7 +72,7 @@ mm2_number = { path = "../mm2_number"} mm2_p2p = { path = "../mm2_p2p", default-features = false } mm2_rpc = { path = "../mm2_rpc" } mm2_state_machine = { path = "../mm2_state_machine" } -mocktopus = "0.8.0" +mocktopus = { version = "0.8.0", optional = true } num-traits = "0.2" parking_lot = { version = "0.12.0", features = ["nightly"] } primitives = { path = "../mm2_bitcoin/primitives" } @@ -162,6 +162,7 @@ winapi = "0.3" [dev-dependencies] mm2_test_helpers = { path = "../mm2_test_helpers" } +mocktopus = { version = "0.8.0" } mm2_p2p = { path = "../mm2_p2p", features = ["application"] } [target.'cfg(target_arch = "wasm32")'.dev-dependencies] diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 6509027c08..db1551b0a3 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -80,7 +80,6 @@ use instant::Instant; use mm2_core::mm_ctx::{MmArc, MmWeak}; use mm2_number::bigdecimal_custom::CheckedDivision; use mm2_number::{BigDecimal, BigUint, MmNumber}; -#[cfg(test)] use mocktopus::macros::*; use rand::seq::SliceRandom; use rlp::{DecoderError, Encodable, RlpStream}; use rpc::v1::types::Bytes as BytesJson; @@ -1284,14 +1283,8 @@ impl Deref for EthCoin { #[async_trait] impl SwapOps for EthCoin { - async fn send_taker_fee( - &self, - fee_addr: &[u8], - dex_fee: DexFee, - _uuid: &[u8], - _expire_at: u64, - ) -> TransactionResult { - let address = try_tx_s!(addr_from_raw_pubkey(fee_addr)); + async fn send_taker_fee(&self, dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionResult { + let address = try_tx_s!(addr_from_raw_pubkey(self.dex_pubkey())); self.send_to_address( address, try_tx_s!(wei_from_big_decimal(&dex_fee.fee_amount().into(), self.decimals)), @@ -1358,7 +1351,6 @@ impl SwapOps for EthCoin { validate_fee_impl(self.clone(), EthValidateFeeArgs { fee_tx_hash: &tx.tx_hash(), expected_sender: validate_fee_args.expected_sender, - fee_addr: validate_fee_args.fee_addr, amount: &validate_fee_args.dex_fee.fee_amount().into(), min_block_number: validate_fee_args.min_block_number, uuid: validate_fee_args.uuid, @@ -1697,7 +1689,6 @@ impl WatcherOps for EthCoin { validate_fee_impl(self.clone(), EthValidateFeeArgs { fee_tx_hash: &H256::from_slice(validate_fee_args.taker_fee_hash.as_slice()), expected_sender: &validate_fee_args.sender_pubkey, - fee_addr: &validate_fee_args.fee_addr, amount: &BigDecimal::from(0), min_block_number: validate_fee_args.min_block_number, uuid: &[], @@ -2307,7 +2298,6 @@ impl WatcherOps for EthCoin { #[async_trait] #[cfg_attr(test, mockable)] -#[async_trait] impl MarketCoinOps for EthCoin { fn ticker(&self) -> &str { &self.ticker[..] } @@ -2672,6 +2662,9 @@ impl MarketCoinOps for EthCoin { MmNumber::from(1) / MmNumber::from(10u64.pow(pow)) } + #[inline] + fn should_burn_dex_fee(&self) -> bool { false } + fn is_trezor(&self) -> bool { self.priv_key_policy.is_trezor() } } @@ -5981,8 +5974,7 @@ fn validate_fee_impl(coin: EthCoin, validate_fee_args: EthValidateFeeArgs<'_>) - let sender_addr = try_f!( addr_from_raw_pubkey(validate_fee_args.expected_sender).map_to_mm(ValidatePaymentError::InvalidParameter) ); - let fee_addr = - try_f!(addr_from_raw_pubkey(validate_fee_args.fee_addr).map_to_mm(ValidatePaymentError::InvalidParameter)); + let fee_addr = try_f!(addr_from_raw_pubkey(coin.dex_pubkey()).map_to_mm(ValidatePaymentError::InvalidParameter)); let amount = validate_fee_args.amount.clone(); let min_block_number = validate_fee_args.min_block_number; @@ -7344,6 +7336,12 @@ impl CommonSwapOpsV2 for EthCoin { fn derive_htlc_pubkey_v2_bytes(&self, swap_unique_data: &[u8]) -> Vec { self.derive_htlc_pubkey_v2(swap_unique_data).to_bytes() } + + #[inline(always)] + fn taker_pubkey_bytes(&self) -> Option> { + let dummy_unique_data = []; // not used for non-private coins + Some(self.derive_htlc_pubkey_v2(&dummy_unique_data).to_bytes()) + } } #[cfg(all(feature = "for-tests", not(target_arch = "wasm32")))] diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index f1bd022ba9..4fd3e47cb8 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -580,7 +580,6 @@ fn validate_dex_fee_invalid_sender_eth() { let validate_fee_args = ValidateFeeArgs { fee_tx: &tx, expected_sender: &DEX_FEE_ADDR_RAW_PUBKEY, - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &DexFee::Standard(amount.into()), min_block_number: 0, uuid: &[], @@ -616,7 +615,6 @@ fn validate_dex_fee_invalid_sender_erc() { let validate_fee_args = ValidateFeeArgs { fee_tx: &tx, expected_sender: &DEX_FEE_ADDR_RAW_PUBKEY, - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &DexFee::Standard(amount.into()), min_block_number: 0, uuid: &[], @@ -656,7 +654,6 @@ fn validate_dex_fee_eth_confirmed_before_min_block() { let validate_fee_args = ValidateFeeArgs { fee_tx: &tx, expected_sender: &compressed_public, - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &DexFee::Standard(amount.into()), min_block_number: 11784793, uuid: &[], @@ -695,7 +692,6 @@ fn validate_dex_fee_erc_confirmed_before_min_block() { let validate_fee_args = ValidateFeeArgs { fee_tx: &tx, expected_sender: &compressed_public, - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &DexFee::Standard(amount.into()), min_block_number: 11823975, uuid: &[], diff --git a/mm2src/coins/lightning.rs b/mm2src/coins/lightning.rs index a4b604975c..fa7c747794 100644 --- a/mm2src/coins/lightning.rs +++ b/mm2src/coins/lightning.rs @@ -611,13 +611,7 @@ impl LightningCoin { #[async_trait] impl SwapOps for LightningCoin { // Todo: This uses dummy data for now for the sake of swap P.O.C., this should be implemented probably after agreeing on how fees will work for lightning - async fn send_taker_fee( - &self, - _fee_addr: &[u8], - _dex_fee: DexFee, - _uuid: &[u8], - _expire_at: u64, - ) -> TransactionResult { + async fn send_taker_fee(&self, _dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionResult { Ok(TransactionEnum::LightningPayment(PaymentHash([1; 32]))) } @@ -1251,6 +1245,8 @@ impl MarketCoinOps for LightningCoin { // Todo: doesn't take routing fees into account too, There is no way to know the route to the other side of the swap when placing the order, need to find a workaround for this fn min_trading_vol(&self) -> MmNumber { self.min_tx_amount().into() } + fn should_burn_dex_fee(&self) -> bool { false } + fn is_trezor(&self) -> bool { self.platform.coin.is_trezor() } } diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index b63228528a..720e2cb0eb 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -32,6 +32,7 @@ #![feature(hash_raw_entry)] #![feature(stmt_expr_attributes)] #![feature(result_flattening)] +#![feature(local_key_cell_methods)] // for tests #[macro_use] extern crate common; #[macro_use] extern crate gstuff; @@ -47,7 +48,7 @@ use bip32::ExtendedPrivateKey; use common::custom_futures::timeout::TimeoutError; use common::executor::{abortable_queue::WeakSpawner, AbortedError, SpawnFuture}; use common::log::{warn, LogOnError}; -use common::{calc_total_pages, now_sec, ten, HttpStatusCode}; +use common::{calc_total_pages, now_sec, ten, HttpStatusCode, DEX_BURN_ADDR_RAW_PUBKEY, DEX_FEE_ADDR_RAW_PUBKEY}; use crypto::{derive_secp256k1_secret, Bip32Error, Bip44Chain, CryptoCtx, CryptoCtxError, DerivationPath, GlobalHDAccountArc, HDPathToCoin, HwRpcError, KeyPairPolicy, RpcDerivationPath, Secp256k1ExtendedPublicKey, Secp256k1Secret, WithHwRpcError}; @@ -64,9 +65,12 @@ use keys::{AddressFormat as UtxoAddressFormat, KeyPair, NetworkPrefix as CashAdd use mm2_core::mm_ctx::{from_ctx, MmArc}; use mm2_err_handle::prelude::*; use mm2_metrics::MetricsWeak; +use mm2_number::BigRational; use mm2_number::{bigdecimal::{BigDecimal, ParseBigDecimalError, Zero}, BigUint, MmNumber, ParseBigIntError}; use mm2_rpc::data::legacy::{EnabledCoin, GetEnabledResponse, Mm2RpcResult}; +#[cfg(any(test, feature = "for-tests"))] +use mocktopus::macros::*; use parking_lot::Mutex as PaMutex; use rpc::v1::types::{Bytes as BytesJson, H256 as H256Json}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -85,6 +89,10 @@ use std::time::Duration; use std::{fmt, iter}; use utxo_signer::with_key_pair::UtxoSignWithKeyPairError; use zcash_primitives::transaction::Transaction as ZTransaction; + +#[cfg(feature = "for-tests")] +pub static mut TEST_BURN_ADDR_RAW_PUBKEY: Option> = None; + cfg_native! { use crate::lightning::LightningCoin; use crate::lightning::ln_conf::PlatformCoinConfirmationTargets; @@ -247,7 +255,9 @@ use tendermint::{CosmosTransaction, TendermintCoin, TendermintFeeDetails, Tender #[doc(hidden)] #[allow(unused_variables)] +#[cfg(any(test, feature = "for-tests"))] pub mod test_coin; +#[cfg(any(test, feature = "for-tests"))] pub use test_coin::TestCoin; pub mod tx_history_storage; @@ -703,7 +713,6 @@ pub struct WatcherValidateTakerFeeInput { pub taker_fee_hash: Vec, pub sender_pubkey: Vec, pub min_block_number: u64, - pub fee_addr: Vec, pub lock_duration: u64, } @@ -990,7 +999,6 @@ pub struct CheckIfMyPaymentSentArgs<'a> { pub struct ValidateFeeArgs<'a> { pub fee_tx: &'a TransactionEnum, pub expected_sender: &'a [u8], - pub fee_addr: &'a [u8], pub dex_fee: &'a DexFee, pub min_block_number: u64, pub uuid: &'a [u8], @@ -999,7 +1007,6 @@ pub struct ValidateFeeArgs<'a> { pub struct EthValidateFeeArgs<'a> { pub fee_tx_hash: &'a H256, pub expected_sender: &'a [u8], - pub fee_addr: &'a [u8], pub amount: &'a BigDecimal, pub min_block_number: u64, pub uuid: &'a [u8], @@ -1070,8 +1077,9 @@ pub enum WatcherRewardError { /// Swap operations (mostly based on the Hash/Time locked transactions implemented by coin wallets). #[async_trait] +#[cfg_attr(any(test, feature = "for-tests"), mockable)] pub trait SwapOps { - async fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, uuid: &[u8], expire_at: u64) -> TransactionResult; + async fn send_taker_fee(&self, dex_fee: DexFee, uuid: &[u8], expire_at: u64) -> TransactionResult; async fn send_maker_payment(&self, maker_payment_args: SendPaymentArgs<'_>) -> TransactionResult; @@ -1183,6 +1191,20 @@ pub trait SwapOps { fn contract_supports_watchers(&self) -> bool { true } fn maker_locktime_multiplier(&self) -> f64 { 2.0 } + + fn dex_pubkey(&self) -> &[u8] { &DEX_FEE_ADDR_RAW_PUBKEY } + + fn burn_pubkey(&self) -> &[u8] { + #[cfg(feature = "for-tests")] + { + unsafe { + if let Some(ref test_pk) = TEST_BURN_ADDR_RAW_PUBKEY { + return test_pk.as_slice(); + } + } + } + &DEX_BURN_ADDR_RAW_PUBKEY + } } /// Operations on maker coin from taker swap side @@ -1358,8 +1380,6 @@ pub struct GenTakerPaymentSpendArgs<'a, Coin: ParseCoinAssocTypes + ?Sized> { pub maker_address: &'a Coin::Address, /// Taker's pubkey pub taker_pub: &'a Coin::Pubkey, - /// Pubkey of address, receiving DEX fees - pub dex_fee_pub: &'a [u8], /// DEX fee pub dex_fee: &'a DexFee, /// Additional reward for maker (premium) @@ -1383,8 +1403,6 @@ pub enum TxGenError { Rpc(String), /// Error during conversion of BigDecimal amount to coin's specific monetary units (satoshis, wei, etc.). NumConversion(String), - /// Address derivation error. - AddressDerivation(String), /// Problem with tx preimage signing. Signing(String), /// Legacy error produced by usage of try_s/try_fus and other similar macros. @@ -1395,6 +1413,8 @@ pub enum TxGenError { TxFeeTooHigh(String), /// Previous tx is not valid PrevTxIsNotValid(String), + /// Previous tx output value too low + PrevOutputTooLow(String), /// Other errors, can be used to return an error that can happen only in specific coin protocol implementation Other(String), } @@ -1979,6 +1999,9 @@ pub trait CommonSwapOpsV2: ParseCoinAssocTypes + Send + Sync + 'static { fn derive_htlc_pubkey_v2(&self, swap_unique_data: &[u8]) -> Self::Pubkey; fn derive_htlc_pubkey_v2_bytes(&self, swap_unique_data: &[u8]) -> Vec; + + /// Returns taker pubkey for non-private coins, for dex fee calculation + fn taker_pubkey_bytes(&self) -> Option>; } /// Operations that coins have independently from the MarketMaker. @@ -2049,8 +2072,15 @@ pub trait MarketCoinOps { /// Get the minimum amount to trade. fn min_trading_vol(&self) -> MmNumber; + /// Is privacy coin like zcash or pirate fn is_privacy(&self) -> bool { false } + /// Is KMD coin + fn is_kmd(&self) -> bool { false } + + /// Should burn part of dex fee coin + fn should_burn_dex_fee(&self) -> bool; + fn is_trezor(&self) -> bool; } @@ -3427,6 +3457,7 @@ pub enum MmCoinEnum { LightningCoin(LightningCoin), #[cfg(feature = "enable-sia")] SiaCoin(SiaCoin), + #[cfg(any(test, feature = "for-tests"))] Test(TestCoin), } @@ -3438,6 +3469,7 @@ impl From for MmCoinEnum { fn from(c: EthCoin) -> MmCoinEnum { MmCoinEnum::EthCoin(c) } } +#[cfg(any(test, feature = "for-tests"))] impl From for MmCoinEnum { fn from(c: TestCoin) -> MmCoinEnum { MmCoinEnum::Test(c) } } @@ -3498,6 +3530,7 @@ impl Deref for MmCoinEnum { MmCoinEnum::ZCoin(ref c) => c, #[cfg(feature = "enable-sia")] MmCoinEnum::SiaCoin(ref c) => c, + #[cfg(any(test, feature = "for-tests"))] MmCoinEnum::Test(ref c) => c, } } @@ -3511,7 +3544,7 @@ impl MmCoinEnum { MmCoinEnum::Qrc20Coin(ref c) => c.as_ref().rpc_client.is_native(), MmCoinEnum::Bch(ref c) => c.as_ref().rpc_client.is_native(), MmCoinEnum::SlpToken(ref c) => c.as_ref().rpc_client.is_native(), - #[cfg(all(not(target_arch = "wasm32"), feature = "zhtlc"))] + #[cfg(not(target_arch = "wasm32"))] MmCoinEnum::ZCoin(ref c) => c.as_ref().rpc_client.is_native(), _ => false, } @@ -3568,32 +3601,171 @@ impl MmCoinStruct { } } +/// Represents how to burn part of dex fee. +#[derive(Clone, Debug, PartialEq)] +pub enum DexFeeBurnDestination { + /// Burn by sending to utxo opreturn output + KmdOpReturn, + /// Send non-kmd coins to a dedicated account to exchange for kmd coins and burn them + PreBurnAccount, +} + /// Represents the different types of DEX fees. #[derive(Clone, Debug, PartialEq)] pub enum DexFee { + /// No dex fee is taken (if taker is dex pubkey) + NoFee, /// Standard dex fee which will be sent to the dex fee address Standard(MmNumber), - /// Dex fee with the burn amount. - /// - `fee_amount` goes to the dex fee address. - /// - `burn_amount` will be added as `OP_RETURN` output in the dex fee transaction. + /// Dex fee with the burn amount WithBurn { + /// Amount to go to the dex fee address fee_amount: MmNumber, + /// Amount to be burned burn_amount: MmNumber, + /// This indicates how to burn the burn_amount + burn_destination: DexFeeBurnDestination, }, } impl DexFee { - /// Creates a new `DexFee` with burn amounts. - pub fn with_burn(fee_amount: MmNumber, burn_amount: MmNumber) -> DexFee { - DexFee::WithBurn { - fee_amount, - burn_amount, + const DEX_FEE_SHARE: &str = "0.75"; + + /// Recreates a `DexFee` from separate fields (usually stored in db). + #[cfg(any(test, feature = "for-tests"))] + pub fn create_from_fields(fee_amount: MmNumber, burn_amount: MmNumber, ticker: &str) -> DexFee { + if fee_amount == MmNumber::default() && burn_amount == MmNumber::default() { + return DexFee::NoFee; + } + if burn_amount > MmNumber::default() { + let burn_destination = match ticker { + "KMD" => DexFeeBurnDestination::KmdOpReturn, + _ => DexFeeBurnDestination::PreBurnAccount, + }; + DexFee::WithBurn { + fee_amount, + burn_amount, + burn_destination, + } + } else { + DexFee::Standard(fee_amount) + } + } + + /// Calculates DEX fee with a threshold based on min tx amount of the taker coin. + /// taker_pubkey may be optional if it is not known yet but we need total dex fee amount + pub fn new_from_taker_coin( + taker_coin: &dyn MmCoin, + rel_ticker: &str, + trade_amount: &MmNumber, + taker_pubkey: Option<&[u8]>, + ) -> DexFee { + if let Some(taker_pubkey) = taker_pubkey { + if !taker_coin.is_privacy() && taker_coin.burn_pubkey() == taker_pubkey { + return DexFee::NoFee; // no dex fee if the taker is the burn pubkey + } + } + // calc dex fee + let rate = Self::dex_fee_rate(taker_coin.ticker(), rel_ticker); + let dex_fee = trade_amount * &rate; + let min_tx_amount = MmNumber::from(taker_coin.min_tx_amount()); + + let dex_fee = if taker_coin.is_kmd() { + // use a special dex fee option for kmd + let (fee_amount, burn_amount) = Self::calc_burn_amount_for_op_return(&dex_fee, &min_tx_amount); + // Note: allow zero burn opreturn for compatibility with old nodes + return DexFee::WithBurn { + fee_amount, + burn_amount, + burn_destination: DexFeeBurnDestination::KmdOpReturn, + }; + } else if taker_coin.should_burn_dex_fee() { + // send part of dex fee to the 'pre-burn' account + let (fee_amount, burn_amount) = Self::calc_burn_amount_for_burn_account(&dex_fee, &min_tx_amount); + // burn_amount can be set to zero if it is dust + if burn_amount > MmNumber::from(0) { + return DexFee::WithBurn { + fee_amount, + burn_amount, + burn_destination: DexFeeBurnDestination::PreBurnAccount, + }; + } + fee_amount + } else { + dex_fee + }; + DexFee::Standard(dex_fee) + } + + /// Returns dex fee discount if KMD is traded + pub fn dex_fee_rate(base: &str, rel: &str) -> MmNumber { + #[cfg(any(feature = "for-tests", test))] + let fee_discount_tickers: &[&str] = match std::env::var("MYCOIN_FEE_DISCOUNT") { + Ok(_) => &["KMD", "MYCOIN"], + Err(_) => &["KMD"], + }; + + #[cfg(not(any(feature = "for-tests", test)))] + let fee_discount_tickers: &[&str] = &["KMD"]; + + if fee_discount_tickers.contains(&base) || fee_discount_tickers.contains(&rel) { + // 1/777 - 10% + BigRational::new(9.into(), 7770.into()).into() + } else { + BigRational::new(1.into(), 777.into()).into() + } + } + + /// Drops the dex fee in KMD by 25%. This cut will be burned during the taker fee payment. + /// + /// Also the cut can be decreased if the new dex fee amount is less than the minimum transaction amount. + fn calc_burn_amount_for_op_return(dex_fee: &MmNumber, min_tx_amount: &MmNumber) -> (MmNumber, MmNumber) { + // Dex fee with 25% burn amount cut + let new_fee = dex_fee * &MmNumber::from(Self::DEX_FEE_SHARE); + if &new_fee >= min_tx_amount { + // Use the max burn value, which is 25%. + let burn_amount = dex_fee - &new_fee; + // we don't care if burn_amount < dust as any amount can be sent to op_return + (new_fee, burn_amount) + } else if dex_fee >= min_tx_amount { + // Burn only the exceeding amount because fee after 25% cut is less than `min_tx_amount`. + let burn_amount = dex_fee - min_tx_amount; + (min_tx_amount.clone(), burn_amount) + } else if dex_fee <= min_tx_amount { + (min_tx_amount.clone(), 0.into()) + } else { + (dex_fee.clone(), 0.into()) + } + } + + /// Drops the dex fee in non-KMD by 25%. This cut will be sent to an output designated as 'burn account' during the taker fee payment + /// (so it cannot be dust). + /// + /// The cut can be set to zero if any of resulting amounts is less than the minimum transaction amount. + fn calc_burn_amount_for_burn_account(dex_fee: &MmNumber, min_tx_amount: &MmNumber) -> (MmNumber, MmNumber) { + // Dex fee with 25% burn amount cut + let new_fee = dex_fee * &MmNumber::from(Self::DEX_FEE_SHARE); + let burn_amount = dex_fee - &new_fee; + if &new_fee >= min_tx_amount && &burn_amount >= min_tx_amount { + // Use the max burn value, which is 25%. Ensure burn_amount is not dust + return (new_fee, burn_amount); + } + // If the new dex fee is dust set it to min_tx_amount and check the updated burn_amount is not dust. + let burn_amount = dex_fee - min_tx_amount; + if &new_fee < min_tx_amount && &burn_amount >= min_tx_amount { + // actually currently burn_amount (25%) < new_fee (75%) so this never happens. Added for a case if 25/75 will ever change + (min_tx_amount.clone(), burn_amount) + } else if dex_fee <= min_tx_amount { + (min_tx_amount.clone(), 0.into()) + } else { + (dex_fee.clone(), 0.into()) } } /// Gets the fee amount associated with the dex fee. pub fn fee_amount(&self) -> MmNumber { match self { + DexFee::NoFee => 0.into(), DexFee::Standard(t) => t.clone(), DexFee::WithBurn { fee_amount, .. } => fee_amount.clone(), } @@ -3602,6 +3774,7 @@ impl DexFee { /// Gets the burn amount associated with the dex fee, if applicable. pub fn burn_amount(&self) -> Option { match self { + DexFee::NoFee => None, DexFee::Standard(_) => None, DexFee::WithBurn { burn_amount, .. } => Some(burn_amount.clone()), } @@ -3610,22 +3783,24 @@ impl DexFee { /// Calculates the total spend amount, considering both the fee and burn amounts. pub fn total_spend_amount(&self) -> MmNumber { match self { + DexFee::NoFee => 0.into(), DexFee::Standard(t) => t.clone(), DexFee::WithBurn { fee_amount, burn_amount, + .. } => fee_amount + burn_amount, } } /// Converts the fee amount to micro-units based on the specified decimal places. - pub fn fee_uamount(&self, decimals: u8) -> NumConversResult { + pub fn fee_amount_as_u64(&self, decimals: u8) -> NumConversResult { let fee_amount = self.fee_amount(); utxo::sat_from_big_decimal(&fee_amount.into(), decimals) } /// Converts the burn amount to micro-units, if applicable, based on the specified decimal places. - pub fn burn_uamount(&self, decimals: u8) -> NumConversResult> { + pub fn burn_amount_as_u64(&self, decimals: u8) -> NumConversResult> { if let Some(burn_amount) = self.burn_amount() { Ok(Some(utxo::sat_from_big_decimal(&burn_amount.into(), decimals)?)) } else { @@ -5580,9 +5755,9 @@ where #[cfg(test)] mod tests { use super::*; - use common::block_on; use mm2_test_helpers::for_tests::RICK; + use mocktopus::mocking::{MockResult, Mockable}; #[test] fn test_lp_coinfind() { @@ -5633,6 +5808,143 @@ mod tests { assert!(matches!(Some(coin), _found)); } + + #[test] + fn test_dex_fee_amount() { + let base = "BTC"; + let btc = TestCoin::new(base); + TestCoin::should_burn_dex_fee.mock_safe(|_| MockResult::Return(true)); + TestCoin::min_tx_amount.mock_safe(|_| MockResult::Return(MmNumber::from("0.0001").into())); + let rel = "ETH"; + let amount = 1.into(); + let actual_fee = DexFee::new_from_taker_coin(&btc, rel, &amount, None); + let expected_fee = DexFee::WithBurn { + fee_amount: amount.clone() / 777u64.into() * "0.75".into(), + burn_amount: amount / 777u64.into() * "0.25".into(), + burn_destination: DexFeeBurnDestination::PreBurnAccount, + }; + assert_eq!(expected_fee, actual_fee); + TestCoin::should_burn_dex_fee.clear_mock(); + + let base = "KMD"; + let kmd = TestCoin::new(base); + TestCoin::should_burn_dex_fee.mock_safe(|_| MockResult::Return(true)); + TestCoin::min_tx_amount.mock_safe(|_| MockResult::Return(MmNumber::from("0.0001").into())); + let rel = "ETH"; + let amount = 1.into(); + let actual_fee = DexFee::new_from_taker_coin(&kmd, rel, &amount, None); + let expected_fee = amount.clone() * (9, 7770).into() * MmNumber::from("0.75"); + let expected_burn_amount = amount * (9, 7770).into() * MmNumber::from("0.25"); + assert_eq!( + DexFee::WithBurn { + fee_amount: expected_fee, + burn_amount: expected_burn_amount, + burn_destination: DexFeeBurnDestination::KmdOpReturn, + }, + actual_fee + ); + TestCoin::should_burn_dex_fee.clear_mock(); + + // check the case when KMD taker fee is close to dust (0.75 of fee < dust) + let base = "KMD"; + let kmd = TestCoin::new(base); + TestCoin::should_burn_dex_fee.mock_safe(|_| MockResult::Return(true)); + TestCoin::min_tx_amount.mock_safe(|_| MockResult::Return(MmNumber::from("0.00001").into())); + let rel = "BTC"; + let amount = (1001 * 777, 90000000).into(); + let actual_fee = DexFee::new_from_taker_coin(&kmd, rel, &amount, None); + assert_eq!( + DexFee::WithBurn { + fee_amount: "0.00001".into(), // equals to min_tx_amount + burn_amount: "0.00000001".into(), + burn_destination: DexFeeBurnDestination::KmdOpReturn, + }, + actual_fee + ); + TestCoin::should_burn_dex_fee.clear_mock(); + + let base = "BTC"; + let btc = TestCoin::new(base); + TestCoin::should_burn_dex_fee.mock_safe(|_| MockResult::Return(true)); + TestCoin::min_tx_amount.mock_safe(|_| MockResult::Return(MmNumber::from("0.00001").into())); + let rel = "KMD"; + let amount = 1.into(); + let actual_fee = DexFee::new_from_taker_coin(&btc, rel, &amount, None); + let expected_fee = DexFee::WithBurn { + fee_amount: amount.clone() * (9, 7770).into() * "0.75".into(), + burn_amount: amount * (9, 7770).into() * "0.25".into(), + burn_destination: DexFeeBurnDestination::PreBurnAccount, + }; + assert_eq!(expected_fee, actual_fee); + TestCoin::should_burn_dex_fee.clear_mock(); + + // whole dex fee (0.001 * 9 / 7770) less than min tx amount (0.00001) + let base = "BTC"; + let btc = TestCoin::new(base); + TestCoin::should_burn_dex_fee.mock_safe(|_| MockResult::Return(true)); + TestCoin::min_tx_amount.mock_safe(|_| MockResult::Return(MmNumber::from("0.00001").into())); + let rel = "KMD"; + let amount: MmNumber = "0.001".parse::().unwrap().into(); + let actual_fee = DexFee::new_from_taker_coin(&btc, rel, &amount, None); + assert_eq!(DexFee::Standard("0.00001".into()), actual_fee); + TestCoin::should_burn_dex_fee.clear_mock(); + + // 75% of dex fee (0.03 * 9/7770 * 0.75) is over the min tx amount (0.00001) + // but non-kmd burn amount is less than the min tx amount + let base = "BTC"; + let btc = TestCoin::new(base); + TestCoin::should_burn_dex_fee.mock_safe(|_| MockResult::Return(true)); + TestCoin::min_tx_amount.mock_safe(|_| MockResult::Return(MmNumber::from("0.00001").into())); + let rel = "KMD"; + let amount: MmNumber = "0.03".parse::().unwrap().into(); + let actual_fee = DexFee::new_from_taker_coin(&btc, rel, &amount, None); + assert_eq!(DexFee::Standard(amount * (9, 7770).into()), actual_fee); + TestCoin::should_burn_dex_fee.clear_mock(); + + // burning from eth currently not supported + let base = "USDT-ERC20"; + let btc = TestCoin::new(base); + TestCoin::should_burn_dex_fee.mock_safe(|_| MockResult::Return(false)); + TestCoin::min_tx_amount.mock_safe(|_| MockResult::Return(MmNumber::from("0.00001").into())); + let rel = "BTC"; + let amount: MmNumber = "1".parse::().unwrap().into(); + let actual_fee = DexFee::new_from_taker_coin(&btc, rel, &amount, None); + assert_eq!(DexFee::Standard(amount / "777".into()), actual_fee); + TestCoin::should_burn_dex_fee.clear_mock(); + + let base = "NUCLEUS"; + let btc = TestCoin::new(base); + TestCoin::should_burn_dex_fee.mock_safe(|_| MockResult::Return(true)); + TestCoin::min_tx_amount.mock_safe(|_| MockResult::Return(MmNumber::from("0.000001").into())); + let rel = "IRIS"; + let amount: MmNumber = "0.008".parse::().unwrap().into(); + let actual_fee = DexFee::new_from_taker_coin(&btc, rel, &amount, None); + let std_fee = amount / "777".into(); + let fee_amount = std_fee.clone() * "0.75".into(); + let burn_amount = std_fee - fee_amount.clone(); + assert_eq!( + DexFee::WithBurn { + fee_amount, + burn_amount, + burn_destination: DexFeeBurnDestination::PreBurnAccount, + }, + actual_fee + ); + TestCoin::should_burn_dex_fee.clear_mock(); + + // test NoFee if taker is dex + let base = "BTC"; + let btc = TestCoin::new(base); + TestCoin::should_burn_dex_fee.mock_safe(|_| MockResult::Return(true)); + TestCoin::dex_pubkey.mock_safe(|_| MockResult::Return(DEX_BURN_ADDR_RAW_PUBKEY.as_slice())); + TestCoin::min_tx_amount.mock_safe(|_| MockResult::Return(MmNumber::from("0.00001").into())); + let rel = "KMD"; + let amount: MmNumber = "0.03".parse::().unwrap().into(); + let actual_fee = DexFee::new_from_taker_coin(&btc, rel, &amount, Some(DEX_BURN_ADDR_RAW_PUBKEY.as_slice())); + assert_eq!(DexFee::NoFee, actual_fee); + TestCoin::should_burn_dex_fee.clear_mock(); + TestCoin::dex_pubkey.clear_mock(); + } } #[cfg(all(feature = "for-tests", not(target_arch = "wasm32")))] diff --git a/mm2src/coins/lp_price.rs b/mm2src/coins/lp_price.rs index fb339d3752..33a8771349 100644 --- a/mm2src/coins/lp_price.rs +++ b/mm2src/coins/lp_price.rs @@ -82,6 +82,7 @@ pub enum Provider { Forex, #[serde(rename = "nomics")] Nomics, + #[cfg(any(test, feature = "for-tests"))] #[serde(rename = "testcoin")] TestCoin, #[serde(rename = "unknown", other)] diff --git a/mm2src/coins/my_tx_history_v2.rs b/mm2src/coins/my_tx_history_v2.rs index f7348a5e78..16914d2bf9 100644 --- a/mm2src/coins/my_tx_history_v2.rs +++ b/mm2src/coins/my_tx_history_v2.rs @@ -10,7 +10,6 @@ use crate::{coin_conf, lp_coinfind_or_err, BlockHeightAndTime, CoinFindError, HD use async_trait::async_trait; use bitcrypto::sha256; use common::{calc_total_pages, ten, HttpStatusCode, PagingOptionsEnum, StatusCode}; -use crypto::StandardHDPath; use derive_more::Display; use enum_derives::EnumFromStringify; use futures::compat::Future01CompatExt; @@ -272,7 +271,6 @@ pub enum MyTxHistoryTarget { account_id: u32, }, AddressId(HDPathAccountToAddressId), - AddressDerivationPath(StandardHDPath), } #[derive(Clone, Deserialize)] diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index 8cbf8a8786..c23d9e23dd 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -760,14 +760,8 @@ impl UtxoCommonOps for Qrc20Coin { #[async_trait] impl SwapOps for Qrc20Coin { - async fn send_taker_fee( - &self, - fee_addr: &[u8], - dex_fee: DexFee, - _uuid: &[u8], - _expire_at: u64, - ) -> TransactionResult { - let to_address = try_tx_s!(self.contract_address_from_raw_pubkey(fee_addr)); + async fn send_taker_fee(&self, dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionResult { + let to_address = try_tx_s!(self.contract_address_from_raw_pubkey(self.dex_pubkey())); let amount = try_tx_s!(wei_from_big_decimal(&dex_fee.fee_amount().into(), self.utxo.decimals)); let transfer_output = try_tx_s!(self.transfer_output(to_address, amount, QRC20_GAS_LIMIT_DEFAULT, QRC20_GAS_PRICE_DEFAULT)); @@ -865,7 +859,7 @@ impl SwapOps for Qrc20Coin { )); } let fee_addr = self - .contract_address_from_raw_pubkey(validate_fee_args.fee_addr) + .contract_address_from_raw_pubkey(self.dex_pubkey()) .map_to_mm(ValidatePaymentError::WrongPaymentTx)?; let expected_value = wei_from_big_decimal(&validate_fee_args.dex_fee.fee_amount().into(), self.utxo.decimals)?; @@ -1281,6 +1275,9 @@ impl MarketCoinOps for Qrc20Coin { MmNumber::from(1) / MmNumber::from(10u64.pow(pow)) } + #[inline] + fn should_burn_dex_fee(&self) -> bool { false } + fn is_trezor(&self) -> bool { self.as_ref().priv_key_policy.is_trezor() } } diff --git a/mm2src/coins/qrc20/qrc20_tests.rs b/mm2src/coins/qrc20/qrc20_tests.rs index 23977f001b..67073a5811 100644 --- a/mm2src/coins/qrc20/qrc20_tests.rs +++ b/mm2src/coins/qrc20/qrc20_tests.rs @@ -332,18 +332,21 @@ fn test_validate_fee() { let result = block_on(coin.validate_fee(ValidateFeeArgs { fee_tx: &tx, expected_sender: &sender_pub, - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &DexFee::Standard(amount.clone().into()), min_block_number: 0, uuid: &[], })); assert!(result.is_ok()); - let fee_addr_dif = hex::decode("03bc2c7ba671bae4a6fc835244c9762b41647b9827d4780a89a949b984a8ddcc05").unwrap(); + // wrong dex address + ::dex_pubkey.mock_safe(|_| { + MockResult::Return(Box::leak(Box::new( + hex::decode("03bc2c7ba671bae4a6fc835244c9762b41647b9827d4780a89a949b984a8ddcc05").unwrap(), + ))) + }); let err = block_on(coin.validate_fee(ValidateFeeArgs { fee_tx: &tx, expected_sender: &sender_pub, - fee_addr: &fee_addr_dif, dex_fee: &DexFee::Standard(amount.clone().into()), min_block_number: 0, uuid: &[], @@ -355,11 +358,11 @@ fn test_validate_fee() { ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("QRC20 Fee tx was sent to wrong address")), _ => panic!("Expected `WrongPaymentTx` wrong receiver address, found {:?}", err), } + ::dex_pubkey.clear_mock(); let err = block_on(coin.validate_fee(ValidateFeeArgs { fee_tx: &tx, expected_sender: &DEX_FEE_ADDR_RAW_PUBKEY, - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &DexFee::Standard(amount.clone().into()), min_block_number: 0, uuid: &[], @@ -375,7 +378,6 @@ fn test_validate_fee() { let err = block_on(coin.validate_fee(ValidateFeeArgs { fee_tx: &tx, expected_sender: &sender_pub, - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &DexFee::Standard(amount.clone().into()), min_block_number: 2000000, uuid: &[], @@ -392,7 +394,6 @@ fn test_validate_fee() { let err = block_on(coin.validate_fee(ValidateFeeArgs { fee_tx: &tx, expected_sender: &sender_pub, - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &DexFee::Standard(amount_dif.into()), min_block_number: 0, uuid: &[], @@ -413,7 +414,6 @@ fn test_validate_fee() { let err = block_on(coin.validate_fee(ValidateFeeArgs { fee_tx: &tx, expected_sender: &sender_pub, - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &DexFee::Standard(amount.into()), min_block_number: 0, uuid: &[], diff --git a/mm2src/coins/siacoin.rs b/mm2src/coins/siacoin.rs index 46fe63deba..e81820247a 100644 --- a/mm2src/coins/siacoin.rs +++ b/mm2src/coins/siacoin.rs @@ -393,18 +393,14 @@ impl MarketCoinOps for SiaCoin { fn min_trading_vol(&self) -> MmNumber { unimplemented!() } + fn should_burn_dex_fee(&self) -> bool { unimplemented!() } + fn is_trezor(&self) -> bool { self.0.priv_key_policy.is_trezor() } } #[async_trait] impl SwapOps for SiaCoin { - async fn send_taker_fee( - &self, - _fee_addr: &[u8], - _dex_fee: DexFee, - _uuid: &[u8], - _expire_at: u64, - ) -> TransactionResult { + async fn send_taker_fee(&self, _dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionResult { unimplemented!() } diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index aa3aabf251..b8efb9a263 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -37,10 +37,11 @@ use common::executor::{abortable_queue::AbortableQueue, AbortableSystem}; use common::executor::{AbortedError, Timer}; use common::log::{debug, warn}; use common::{get_utc_timestamp, now_sec, Future01CompatExt, PagingOptions, DEX_FEE_ADDR_PUBKEY}; -use cosmrs::bank::MsgSend; +use cosmrs::bank::{MsgMultiSend, MsgSend, MultiSendIo}; use cosmrs::crypto::secp256k1::SigningKey; use cosmrs::proto::cosmos::auth::v1beta1::{BaseAccount, QueryAccountRequest, QueryAccountResponse}; -use cosmrs::proto::cosmos::bank::v1beta1::{MsgSend as MsgSendProto, QueryBalanceRequest, QueryBalanceResponse}; +use cosmrs::proto::cosmos::bank::v1beta1::{MsgMultiSend as MsgMultiSendProto, MsgSend as MsgSendProto, + QueryBalanceRequest, QueryBalanceResponse}; use cosmrs::proto::cosmos::base::query::v1beta1::PageRequest; use cosmrs::proto::cosmos::base::tendermint::v1beta1::{GetBlockByHeightRequest, GetBlockByHeightResponse, GetLatestBlockRequest, GetLatestBlockResponse}; @@ -86,6 +87,8 @@ use std::str::FromStr; use std::sync::{Arc, Mutex}; use uuid::Uuid; +#[cfg(test)] use mocktopus::macros::*; + // ABCI Request Paths const ABCI_GET_LATEST_BLOCK_PATH: &str = "/cosmos.base.tendermint.v1beta1.Service/GetLatestBlock"; const ABCI_GET_BLOCK_BY_HEIGHT_PATH: &str = "/cosmos.base.tendermint.v1beta1.Service/GetBlockByHeight"; @@ -641,6 +644,7 @@ impl TendermintCommons for TendermintCoin { } } +#[cfg_attr(test, mockable)] impl TendermintCoin { #[allow(clippy::too_many_arguments)] pub async fn init( @@ -652,7 +656,7 @@ impl TendermintCoin { tx_history: bool, activation_policy: TendermintActivationPolicy, is_keplr_from_ledger: bool, - ) -> MmResult { + ) -> MmResult { if nodes.is_empty() { return MmError::err(TendermintInitError { ticker, @@ -952,7 +956,9 @@ impl TendermintCoin { memo: &str, withdraw_fee: Option, ) -> MmResult { - let Ok(activated_priv_key) = self.activation_policy.activated_key_or_err() else { + let activated_priv_key = if let Ok(activated_priv_key) = self.activation_policy.activated_key_or_err() { + activated_priv_key + } else { let (gas_price, gas_limit) = self.gas_info_for_withdraw(&withdraw_fee, GAS_LIMIT_DEFAULT); let amount = ((GAS_WANTED_BASE_VALUE * 1.5) * gas_price).ceil(); @@ -1031,7 +1037,9 @@ impl TendermintCoin { memo: &str, withdraw_fee: Option, ) -> MmResult { - let Some(priv_key) = priv_key else { + let priv_key = if let Some(priv_key) = priv_key { + priv_key + } else { let (gas_price, _) = self.gas_info_for_withdraw(&withdraw_fee, 0); return Ok(((GAS_WANTED_BASE_VALUE * 1.5) * gas_price).ceil() as u64); }; @@ -1103,9 +1111,10 @@ impl TendermintCoin { .account .or_mm_err(|| TendermintCoinRpcError::InvalidResponse("Account is None".into()))?; + let account_prefix = self.account_prefix.clone(); let base_account = match BaseAccount::decode(account.value.as_slice()) { Ok(account) => account, - Err(err) if &self.account_prefix == "iaa" => { + Err(err) if account_prefix.as_str() == "iaa" => { let ethermint_account = EthermintAccount::decode(account.value.as_slice())?; ethermint_account @@ -1400,6 +1409,7 @@ impl TendermintCoin { Ok(SerializedUnsignedTx { tx_json, body_bytes }) } + #[allow(clippy::let_unit_value)] // for mockable pub fn add_activated_token_info(&self, ticker: String, decimals: u8, denom: Denom) { self.tokens_info .lock() @@ -1549,8 +1559,7 @@ impl TendermintCoin { pub(super) fn send_taker_fee_for_denom( &self, - fee_addr: &[u8], - amount: BigDecimal, + dex_fee: &DexFee, denom: Denom, decimals: u8, uuid: &[u8], @@ -1558,20 +1567,56 @@ impl TendermintCoin { ) -> TransactionFut { let memo = try_tx_fus!(Uuid::from_slice(uuid)).to_string(); let from_address = self.account_id.clone(); - let pubkey_hash = dhash160(fee_addr); - let to_address = try_tx_fus!(AccountId::new(&self.account_prefix, pubkey_hash.as_slice())); - - let amount_as_u64 = try_tx_fus!(sat_from_big_decimal(&amount, decimals)); - let amount = cosmrs::Amount::from(amount_as_u64); + let dex_pubkey_hash = dhash160(self.dex_pubkey()); + let burn_pubkey_hash = dhash160(self.burn_pubkey()); + let dex_address = try_tx_fus!(AccountId::new(&self.account_prefix, dex_pubkey_hash.as_slice())); + let burn_address = try_tx_fus!(AccountId::new(&self.account_prefix, burn_pubkey_hash.as_slice())); - let amount = vec![Coin { denom, amount }]; + let fee_amount_as_u64 = try_tx_fus!(dex_fee.fee_amount_as_u64(decimals)); + let fee_amount = vec![Coin { + denom: denom.clone(), + amount: cosmrs::Amount::from(fee_amount_as_u64), + }]; - let tx_payload = try_tx_fus!(MsgSend { - from_address, - to_address, - amount, - } - .to_any()); + let tx_result = match dex_fee { + DexFee::NoFee => try_tx_fus!(Err("Unexpected DexFee::NoFee".to_owned())), + DexFee::Standard(_) => MsgSend { + from_address, + to_address: dex_address, + amount: fee_amount, + } + .to_any(), + DexFee::WithBurn { .. } => { + let burn_amount_as_u64 = try_tx_fus!(dex_fee.burn_amount_as_u64(decimals)).unwrap_or_default(); + let burn_amount = vec![Coin { + denom: denom.clone(), + amount: cosmrs::Amount::from(burn_amount_as_u64), + }]; + let total_amount_as_u64 = fee_amount_as_u64 + burn_amount_as_u64; + let total_amount = vec![Coin { + denom, + amount: cosmrs::Amount::from(total_amount_as_u64), + }]; + MsgMultiSend { + inputs: vec![MultiSendIo { + address: from_address, + coins: total_amount, + }], + outputs: vec![ + MultiSendIo { + address: dex_address, + coins: fee_amount, + }, + MultiSendIo { + address: burn_address, + coins: burn_amount, + }, + ], + } + .to_any() + }, + }; + let tx_payload = try_tx_fus!(tx_result); let coin = self.clone(); let fut = async move { @@ -1608,8 +1653,7 @@ impl TendermintCoin { &self, fee_tx: &TransactionEnum, expected_sender: &[u8], - fee_addr: &[u8], - amount: &BigDecimal, + dex_fee: &DexFee, decimals: u8, uuid: &[u8], denom: String, @@ -1628,66 +1672,40 @@ impl TendermintCoin { let sender_pubkey_hash = dhash160(expected_sender); let expected_sender_address = try_f!(AccountId::new(&self.account_prefix, sender_pubkey_hash.as_slice()) - .map_to_mm(|r| ValidatePaymentError::InvalidParameter(r.to_string()))) - .to_string(); - - let dex_fee_addr_pubkey_hash = dhash160(fee_addr); - let expected_dex_fee_address = try_f!(AccountId::new( - &self.account_prefix, - dex_fee_addr_pubkey_hash.as_slice() - ) - .map_to_mm(|r| ValidatePaymentError::InvalidParameter(r.to_string()))) - .to_string(); - - let expected_amount = try_f!(sat_from_big_decimal(amount, decimals)); - let expected_amount = CoinProto { - denom, - amount: expected_amount.to_string(), - }; + .map_to_mm(|r| ValidatePaymentError::InvalidParameter(r.to_string()))); let coin = self.clone(); + let dex_fee = dex_fee.clone(); let fut = async move { let tx_body = TxBody::decode(tx.data.body_bytes.as_slice()) .map_to_mm(|e| ValidatePaymentError::TxDeserializationError(e.to_string()))?; - if tx_body.messages.len() != 1 { - return MmError::err(ValidatePaymentError::WrongPaymentTx( - "Tx body must have exactly one message".to_string(), - )); - } - - let msg = MsgSendProto::decode(tx_body.messages[0].value.as_slice()) - .map_to_mm(|e| ValidatePaymentError::TxDeserializationError(e.to_string()))?; - if msg.to_address != expected_dex_fee_address { - return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "Dex fee is sent to wrong address: {}, expected {}", - msg.to_address, expected_dex_fee_address - ))); - } - - if msg.amount.len() != 1 { - return MmError::err(ValidatePaymentError::WrongPaymentTx( - "Msg must have exactly one Coin".to_string(), - )); - } - - if msg.amount[0] != expected_amount { - return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "Invalid amount {:?}, expected {:?}", - msg.amount[0], expected_amount - ))); - } - if msg.from_address != expected_sender_address { - return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "Invalid sender: {}, expected {}", - msg.from_address, expected_sender_address - ))); + match dex_fee { + DexFee::NoFee => { + return MmError::err(ValidatePaymentError::InternalError( + "unexpected DexFee::NoFee".to_string(), + )) + }, + DexFee::Standard(_) => coin.validate_standard_dex_fee( + &tx_body, + &expected_sender_address, + &dex_fee, + decimals, + denom.clone(), + )?, + DexFee::WithBurn { .. } => coin.validate_with_burn_dex_fee( + &tx_body, + &expected_sender_address, + &dex_fee, + decimals, + denom.clone(), + )?, } if tx_body.memo != uuid { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "Invalid memo: {}, expected {}", - msg.from_address, uuid + tx_body.memo, uuid ))); } @@ -1787,6 +1805,152 @@ impl TendermintCoin { } } + fn validate_standard_dex_fee( + &self, + tx_body: &TxBody, + expected_sender_address: &AccountId, + dex_fee: &DexFee, + decimals: u8, + denom: String, + ) -> MmResult<(), ValidatePaymentError> { + if tx_body.messages.len() != 1 { + return MmError::err(ValidatePaymentError::WrongPaymentTx( + "Tx body must have exactly one message".to_string(), + )); + } + + let dex_pubkey_hash = dhash160(self.dex_pubkey()); + let expected_dex_address = AccountId::new(&self.account_prefix, dex_pubkey_hash.as_slice()) + .map_to_mm(|r| ValidatePaymentError::InvalidParameter(r.to_string()))?; + + let fee_amount_as_u64 = dex_fee.fee_amount_as_u64(decimals)?; + let expected_dex_amount = CoinProto { + denom, + amount: fee_amount_as_u64.to_string(), + }; + + let msg = MsgSendProto::decode(tx_body.messages[0].value.as_slice()) + .map_to_mm(|e| ValidatePaymentError::TxDeserializationError(e.to_string()))?; + if msg.to_address != expected_dex_address.as_ref() { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Dex fee is sent to wrong address: {}, expected {}", + msg.to_address, expected_dex_address + ))); + } + if msg.amount.len() != 1 { + return MmError::err(ValidatePaymentError::WrongPaymentTx( + "Msg must have exactly one Coin".to_string(), + )); + } + if msg.amount[0] != expected_dex_amount { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Invalid amount {:?}, expected {:?}", + msg.amount[0], expected_dex_amount + ))); + } + if msg.from_address != expected_sender_address.as_ref() { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Invalid sender: {}, expected {}", + msg.from_address, expected_sender_address + ))); + } + Ok(()) + } + + fn validate_with_burn_dex_fee( + &self, + tx_body: &TxBody, + expected_sender_address: &AccountId, + dex_fee: &DexFee, + decimals: u8, + denom: String, + ) -> MmResult<(), ValidatePaymentError> { + if tx_body.messages.len() != 1 { + return MmError::err(ValidatePaymentError::WrongPaymentTx( + "Tx body must have exactly one message".to_string(), + )); + } + + let dex_pubkey_hash = dhash160(self.dex_pubkey()); + let expected_dex_address = AccountId::new(&self.account_prefix, dex_pubkey_hash.as_slice()) + .map_to_mm(|r| ValidatePaymentError::InvalidParameter(r.to_string()))?; + + let burn_pubkey_hash = dhash160(self.burn_pubkey()); + let expected_burn_address = AccountId::new(&self.account_prefix, burn_pubkey_hash.as_slice()) + .map_to_mm(|r| ValidatePaymentError::InvalidParameter(r.to_string()))?; + + let fee_amount_as_u64 = dex_fee.fee_amount_as_u64(decimals)?; + let expected_dex_amount = CoinProto { + denom: denom.clone(), + amount: fee_amount_as_u64.to_string(), + }; + let burn_amount_as_u64 = dex_fee.burn_amount_as_u64(decimals)?.unwrap_or_default(); + let expected_burn_amount = CoinProto { + denom, + amount: burn_amount_as_u64.to_string(), + }; + + let msg = MsgMultiSendProto::decode(tx_body.messages[0].value.as_slice()) + .map_to_mm(|e| ValidatePaymentError::TxDeserializationError(e.to_string()))?; + if msg.outputs.len() != 2 { + return MmError::err(ValidatePaymentError::WrongPaymentTx( + "Msg must have exactly two outputs".to_string(), + )); + } + + // Validate dex fee output + if msg.outputs[0].address != expected_dex_address.as_ref() { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Dex fee is sent to wrong address: {}, expected {}", + msg.outputs[0].address, expected_dex_address + ))); + } + if msg.outputs[0].coins.len() != 1 { + return MmError::err(ValidatePaymentError::WrongPaymentTx( + "Dex fee output must have exactly one Coin".to_string(), + )); + } + if msg.outputs[0].coins[0] != expected_dex_amount { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Invalid dex fee amount {:?}, expected {:?}", + msg.outputs[0].coins[0], expected_dex_amount + ))); + } + + // Validate burn output + if msg.outputs[1].address != expected_burn_address.as_ref() { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Burn fee is sent to wrong address: {}, expected {}", + msg.outputs[1].address, expected_burn_address + ))); + } + if msg.outputs[1].coins.len() != 1 { + return MmError::err(ValidatePaymentError::WrongPaymentTx( + "Burn fee output must have exactly one Coin".to_string(), + )); + } + if msg.outputs[1].coins[0] != expected_burn_amount { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Invalid burn amount {:?}, expected {:?}", + msg.outputs[1].coins[0], expected_burn_amount + ))); + } + if msg.inputs.len() != 1 { + return MmError::err(ValidatePaymentError::WrongPaymentTx( + "Msg must have exactly one input".to_string(), + )); + } + + // validate input + if msg.inputs[0].address != expected_sender_address.as_ref() { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Invalid sender: {}, expected {}", + msg.inputs[0].address, expected_sender_address + ))); + } + Ok(()) + } + pub(super) async fn get_sender_trade_fee_for_denom( &self, ticker: String, @@ -1985,9 +2149,9 @@ impl TendermintCoin { amount >= &min_tx_amount } - async fn search_for_swap_tx_spend( + async fn search_for_swap_tx_spend<'l>( &self, - input: SearchForSwapTxSpendInput<'_>, + input: SearchForSwapTxSpendInput<'l>, ) -> MmResult, SearchForSwapTxSpendErr> { let tx = cosmrs::Tx::from_bytes(input.tx)?; let first_message = tx @@ -2899,6 +3063,9 @@ impl MarketCoinOps for TendermintCoin { #[inline] fn min_trading_vol(&self) -> MmNumber { self.min_tx_amount().into() } + #[inline] + fn should_burn_dex_fee(&self) -> bool { true } + fn is_trezor(&self) -> bool { match &self.activation_policy { TendermintActivationPolicy::PrivateKey(pk) => pk.is_trezor(), @@ -2910,17 +3077,10 @@ impl MarketCoinOps for TendermintCoin { #[async_trait] #[allow(unused_variables)] impl SwapOps for TendermintCoin { - async fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, uuid: &[u8], expire_at: u64) -> TransactionResult { - self.send_taker_fee_for_denom( - fee_addr, - dex_fee.fee_amount().into(), - self.denom.clone(), - self.decimals, - uuid, - expire_at, - ) - .compat() - .await + async fn send_taker_fee(&self, dex_fee: DexFee, uuid: &[u8], expire_at: u64) -> TransactionResult { + self.send_taker_fee_for_denom(&dex_fee, self.denom.clone(), self.decimals, uuid, expire_at) + .compat() + .await } async fn send_maker_payment(&self, maker_payment_args: SendPaymentArgs<'_>) -> TransactionResult { @@ -3077,8 +3237,7 @@ impl SwapOps for TendermintCoin { self.validate_fee_for_denom( validate_fee_args.fee_tx, validate_fee_args.expected_sender, - validate_fee_args.fee_addr, - &validate_fee_args.dex_fee.fee_amount().into(), + validate_fee_args.dex_fee, self.decimals, validate_fee_args.uuid, self.denom.to_string(), @@ -3534,10 +3693,12 @@ fn parse_expected_sequence_number(e: &str) -> MmResult TendermintProtocolInfo { + TendermintProtocolInfo { + decimals: 6, + denom: String::from("ibc/F7F28FF3C09024A0225EDBBDB207E5872D2B4EF2FB874FE47B05EF9C9A7D211C"), + account_prefix: String::from("nuc"), + chain_id: String::from("nucleus-testnet"), + gas_price: None, + chain_registry_name: None, + } + } + + fn get_tx_signer_pubkey_unprefixed(tx: &Tx, i: usize) -> Vec { + tx.auth_info.as_ref().unwrap().signer_infos[i] + .public_key + .as_ref() + .unwrap() + .value[2..] + .to_vec() + } + #[test] fn test_tx_hash_str_from_bytes() { let tx_hex = "0a97010a8f010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e64126f0a2d636f736d6f7331737661773061716334353834783832356a753775613033673578747877643061686c3836687a122d636f736d6f7331737661773061716334353834783832356a753775613033673578747877643061686c3836687a1a0f0a057561746f6d120631303030303018d998bf0512670a500a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a2102000eef4ab169e7b26a4a16c47420c4176ab702119ba57a8820fb3e53c8e7506212040a020801180312130a0d0a057561746f6d12043130303010a08d061a4093e5aec96f7d311d129f5ec8714b21ad06a75e483ba32afab86354400b2ac8350bfc98731bbb05934bf138282750d71aadbe08ceb6bb195f2b55e1bbfdddaaad"; @@ -3893,19 +4074,20 @@ pub mod tendermint_coin_tests { // CreateHtlc tx, validation should fail because first message of dex fee tx must be MsgSend // https://nyancat.iobscan.io/#/tx?txHash=2DB382CE3D9953E4A94957B475B0E8A98F5B6DDB32D6BF0F6A765D949CF4A727 - let create_htlc_tx_hash = "2DB382CE3D9953E4A94957B475B0E8A98F5B6DDB32D6BF0F6A765D949CF4A727"; - let create_htlc_tx_bytes = block_on(coin.request_tx(create_htlc_tx_hash.into())) - .unwrap() - .encode_to_vec(); + let create_htlc_tx_response = GetTxResponse::decode(hex::decode("0ac4030a96020a8e020a1b2f697269736d6f642e68746c632e4d736743726561746548544c4312ee010a2a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76122a696161316530727838376d646a37397a656a65777563346a6737716c39756432323836673275733866321a40623736353830316334303930363762623837396565326563666665363138623931643734346663343030303030303030303030303030303030303030303030302a0d0a036e696d120631303030303032403063333463373165626132613531373338363939663966336436646166666231356265353736653865643534333230333438353739316235646133396431306440ea3c18afaba80212670a510a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21025a37975c079a7543603fcab24e2565a4adee3cf9af8934690e103282fa40251112040a02080118a50312120a0c0a05756e79616e120332303010a08d061a4029dfbe5fc6ec9ed257e0f3a86542cb9da0d6047620274f22265c4fb8221ed45830236adef675f76962f74e4cfcc7a10e1390f4d2071bc7dd07838e300381952612882208ccaaa8021240324442333832434533443939353345344139343935374234373542304538413938463542364444423332443642463046364137363544393439434634413732372ac60130413631304131423246363937323639373336443646363432453638373436433633324534443733363734333732363536313734363534383534344334333132343230413430343634333339343433383433333033353336343233363339343233323436333433313331333734353332343134333433333533323337343133343339333933303435333734353434333234323336343533323432343634313334343333323334333533373335333034343339333333353434333833313332333434333330333832cc095b7b226576656e7473223a5b7b2274797065223a22636f696e5f7265636569766564222c2261747472696275746573223a5b7b226b6579223a227265636569766572222c2276616c7565223a2269616131613778796e6a3463656674386b67646a72366b637130733037793363637961366d65707a646d227d2c7b226b6579223a22616d6f756e74222c2276616c7565223a223130303030306e696d227d5d7d2c7b2274797065223a22636f696e5f7370656e74222c2261747472696275746573223a5b7b226b6579223a227370656e646572222c2276616c7565223a22696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76227d2c7b226b6579223a22616d6f756e74222c2276616c7565223a223130303030306e696d227d5d7d2c7b2274797065223a226372656174655f68746c63222c2261747472696275746573223a5b7b226b6579223a226964222c2276616c7565223a2246433944384330353642363942324634313137453241434335323741343939304537454432423645324246413443323435373530443933354438313234433038227d2c7b226b6579223a2273656e646572222c2276616c7565223a22696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76227d2c7b226b6579223a227265636569766572222c2276616c7565223a22696161316530727838376d646a37397a656a65777563346a6737716c3975643232383667327573386632227d2c7b226b6579223a2272656365697665725f6f6e5f6f746865725f636861696e222c2276616c7565223a2262373635383031633430393036376262383739656532656366666536313862393164373434666334303030303030303030303030303030303030303030303030227d2c7b226b6579223a2273656e6465725f6f6e5f6f746865725f636861696e227d2c7b226b6579223a227472616e73666572222c2276616c7565223a2266616c7365227d5d7d2c7b2274797065223a226d657373616765222c2261747472696275746573223a5b7b226b6579223a22616374696f6e222c2276616c7565223a222f697269736d6f642e68746c632e4d736743726561746548544c43227d2c7b226b6579223a2273656e646572222c2276616c7565223a22696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76227d2c7b226b6579223a226d6f64756c65222c2276616c7565223a2268746c63227d2c7b226b6579223a2273656e646572222c2276616c7565223a22696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76227d5d7d2c7b2274797065223a227472616e73666572222c2261747472696275746573223a5b7b226b6579223a22726563697069656e74222c2276616c7565223a2269616131613778796e6a3463656674386b67646a72366b637130733037793363637961366d65707a646d227d2c7b226b6579223a2273656e646572222c2276616c7565223a22696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76227d2c7b226b6579223a22616d6f756e74222c2276616c7565223a223130303030306e696d227d5d7d5d7d5d3ac7061a5c0a0d636f696e5f726563656976656412360a087265636569766572122a69616131613778796e6a3463656674386b67646a72366b637130733037793363637961366d65707a646d12130a06616d6f756e7412093130303030306e696d1a580a0a636f696e5f7370656e7412350a077370656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a7612130a06616d6f756e7412093130303030306e696d1acc020a0b6372656174655f68746c6312460a02696412404643394438433035364236394232463431313745324143433532374134393930453745443242364532424641344332343537353044393335443831323443303812340a0673656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a7612360a087265636569766572122a696161316530727838376d646a37397a656a65777563346a6737716c3975643232383667327573386632125b0a1772656365697665725f6f6e5f6f746865725f636861696e12406237363538303163343039303637626238373965653265636666653631386239316437343466633430303030303030303030303030303030303030303030303012170a1573656e6465725f6f6e5f6f746865725f636861696e12110a087472616e73666572120566616c73651aac010a076d65737361676512250a06616374696f6e121b2f697269736d6f642e68746c632e4d736743726561746548544c4312340a0673656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76120e0a066d6f64756c65120468746c6312340a0673656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a761a8e010a087472616e7366657212370a09726563697069656e74122a69616131613778796e6a3463656674386b67646a72366b637130733037793363637961366d65707a646d12340a0673656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a7612130a06616d6f756e7412093130303030306e696d48a08d06509bd3045ade030a152f636f736d6f732e74782e763162657461312e547812c4030a96020a8e020a1b2f697269736d6f642e68746c632e4d736743726561746548544c4312ee010a2a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76122a696161316530727838376d646a37397a656a65777563346a6737716c39756432323836673275733866321a40623736353830316334303930363762623837396565326563666665363138623931643734346663343030303030303030303030303030303030303030303030302a0d0a036e696d120631303030303032403063333463373165626132613531373338363939663966336436646166666231356265353736653865643534333230333438353739316235646133396431306440ea3c18afaba80212670a510a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21025a37975c079a7543603fcab24e2565a4adee3cf9af8934690e103282fa40251112040a02080118a50312120a0c0a05756e79616e120332303010a08d061a4029dfbe5fc6ec9ed257e0f3a86542cb9da0d6047620274f22265c4fb8221ed45830236adef675f76962f74e4cfcc7a10e1390f4d2071bc7dd07838e30038195266214323032322d30392d31355432333a30343a35355a6a410a027478123b0a076163635f736571122e696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a762f34323118016a6d0a02747812670a097369676e617475726512584b642b2b583862736e744a5834504f6f5a554c4c6e614457424859674a3038694a6c7850754349653146677749327265396e583361574c33546b7a387836454f45354430306763627839304867343477413447564a673d3d18016a5b0a0a636f696e5f7370656e7412370a077370656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76180112140a06616d6f756e741208323030756e79616e18016a5f0a0d636f696e5f726563656976656412380a087265636569766572122a696161313778706676616b6d32616d67393632796c73366638347a336b656c6c3863356c396d72336676180112140a06616d6f756e741208323030756e79616e18016a93010a087472616e7366657212390a09726563697069656e74122a696161313778706676616b6d32616d67393632796c73366638347a336b656c6c3863356c396d72336676180112360a0673656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76180112140a06616d6f756e741208323030756e79616e18016a410a076d65737361676512360a0673656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a7618016a170a02747812110a036665651208323030756e79616e18016a320a076d65737361676512270a06616374696f6e121b2f697269736d6f642e68746c632e4d736743726561746548544c4318016a5c0a0a636f696e5f7370656e7412370a077370656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76180112150a06616d6f756e7412093130303030306e696d18016a600a0d636f696e5f726563656976656412380a087265636569766572122a69616131613778796e6a3463656674386b67646a72366b637130733037793363637961366d65707a646d180112150a06616d6f756e7412093130303030306e696d18016a94010a087472616e7366657212390a09726563697069656e74122a69616131613778796e6a3463656674386b67646a72366b637130733037793363637961366d65707a646d180112360a0673656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76180112150a06616d6f756e7412093130303030306e696d18016a410a076d65737361676512360a0673656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a7618016ad8020a0b6372656174655f68746c6312480a026964124046433944384330353642363942324634313137453241434335323741343939304537454432423645324246413443323435373530443933354438313234433038180112360a0673656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76180112380a087265636569766572122a696161316530727838376d646a37397a656a65777563346a6737716c39756432323836673275733866321801125d0a1772656365697665725f6f6e5f6f746865725f636861696e124062373635383031633430393036376262383739656532656366666536313862393164373434666334303030303030303030303030303030303030303030303030180112190a1573656e6465725f6f6e5f6f746865725f636861696e180112130a087472616e73666572120566616c736518016a530a076d65737361676512100a066d6f64756c65120468746c63180112360a0673656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a761801").unwrap().as_slice()).unwrap(); + let mock_tx = create_htlc_tx_response.tx.as_ref().unwrap().clone(); + TendermintCoin::request_tx.mock_safe(move |_, _| { + let mock_tx = mock_tx.clone(); + MockResult::Return(Box::pin(async move { Ok(mock_tx) })) + }); let create_htlc_tx = TransactionEnum::CosmosTransaction(CosmosTransaction { - data: TxRaw::decode(create_htlc_tx_bytes.as_slice()).unwrap(), + data: TxRaw::decode(create_htlc_tx_response.tx.as_ref().unwrap().encode_to_vec().as_slice()).unwrap(), }); let invalid_amount: MmNumber = 1.into(); let error = block_on(coin.validate_fee(ValidateFeeArgs { fee_tx: &create_htlc_tx, expected_sender: &[], - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &DexFee::Standard(invalid_amount.clone()), min_block_number: 0, uuid: &[1; 16], @@ -3922,22 +4104,31 @@ pub mod tendermint_coin_tests { error ), } + TendermintCoin::request_tx.clear_mock(); // just a random transfer tx not related to AtomicDEX, should fail on recipient address check // https://nyancat.iobscan.io/#/tx?txHash=65815814E7D74832D87956144C1E84801DC94FE9A509D207A0ABC3F17775E5DF - let random_transfer_tx_hash = "65815814E7D74832D87956144C1E84801DC94FE9A509D207A0ABC3F17775E5DF"; - let random_transfer_tx_bytes = block_on(coin.request_tx(random_transfer_tx_hash.into())) - .unwrap() - .encode_to_vec(); - + let random_transfer_tx_response = GetTxResponse::decode(hex::decode("0ac6020a95010a8c010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e64126c0a2a696161317039703230667468306c7665647634736d7733327339377079386e74657230716e7774727538122a696161316b36636d636b7875757732647a7a6b76747a7239776c7467356c3633747361746b6c71357a791a120a05756e79616e1209313030303030303030120474657374126a0a510a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a2103327a4866304ead15d941dbbdf2d2563514fcc94d25e4af897a71681a02b637b212040a02080118880212150a0f0a05756e79616e120632303030303010c09a0c1a402d1c8c1e1a44bd56fe24947d6ed6cae27c6f8a46e3e9beaaad9798dc842ae4ea0c0a20f33144c8fad3490638455b65f63decdb74c347a7c97d0469f5de453fe312a41608febfba021240363538313538313445374437343833324438373935363134344331453834383031444339344645394135303944323037413041424333463137373735453544462a403041314530413143324636333646373336443646373332453632363136453642324537363331363236353734363133313245344437333637353336353645363432da055b7b226576656e7473223a5b7b2274797065223a22636f696e5f7265636569766564222c2261747472696275746573223a5b7b226b6579223a227265636569766572222c2276616c7565223a22696161316b36636d636b7875757732647a7a6b76747a7239776c7467356c3633747361746b6c71357a79227d2c7b226b6579223a22616d6f756e74222c2276616c7565223a22313030303030303030756e79616e227d5d7d2c7b2274797065223a22636f696e5f7370656e74222c2261747472696275746573223a5b7b226b6579223a227370656e646572222c2276616c7565223a22696161317039703230667468306c7665647634736d7733327339377079386e74657230716e7774727538227d2c7b226b6579223a22616d6f756e74222c2276616c7565223a22313030303030303030756e79616e227d5d7d2c7b2274797065223a226d657373616765222c2261747472696275746573223a5b7b226b6579223a22616374696f6e222c2276616c7565223a222f636f736d6f732e62616e6b2e763162657461312e4d736753656e64227d2c7b226b6579223a2273656e646572222c2276616c7565223a22696161317039703230667468306c7665647634736d7733327339377079386e74657230716e7774727538227d2c7b226b6579223a226d6f64756c65222c2276616c7565223a2262616e6b227d5d7d2c7b2274797065223a227472616e73666572222c2261747472696275746573223a5b7b226b6579223a22726563697069656e74222c2276616c7565223a22696161316b36636d636b7875757732647a7a6b76747a7239776c7467356c3633747361746b6c71357a79227d2c7b226b6579223a2273656e646572222c2276616c7565223a22696161317039703230667468306c7665647634736d7733327339377079386e74657230716e7774727538227d2c7b226b6579223a22616d6f756e74222c2276616c7565223a22313030303030303030756e79616e227d5d7d5d7d5d3ad1031a610a0d636f696e5f726563656976656412360a087265636569766572122a696161316b36636d636b7875757732647a7a6b76747a7239776c7467356c3633747361746b6c71357a7912180a06616d6f756e74120e313030303030303030756e79616e1a5d0a0a636f696e5f7370656e7412350a077370656e646572122a696161317039703230667468306c7665647634736d7733327339377079386e74657230716e777472753812180a06616d6f756e74120e313030303030303030756e79616e1a770a076d65737361676512260a06616374696f6e121c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e6412340a0673656e646572122a696161317039703230667468306c7665647634736d7733327339377079386e74657230716e7774727538120e0a066d6f64756c65120462616e6b1a93010a087472616e7366657212370a09726563697069656e74122a696161316b36636d636b7875757732647a7a6b76747a7239776c7467356c3633747361746b6c71357a7912340a0673656e646572122a696161317039703230667468306c7665647634736d7733327339377079386e74657230716e777472753812180a06616d6f756e74120e313030303030303030756e79616e48c09a0c5092e5035ae0020a152f636f736d6f732e74782e763162657461312e547812c6020a95010a8c010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e64126c0a2a696161317039703230667468306c7665647634736d7733327339377079386e74657230716e7774727538122a696161316b36636d636b7875757732647a7a6b76747a7239776c7467356c3633747361746b6c71357a791a120a05756e79616e1209313030303030303030120474657374126a0a510a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a2103327a4866304ead15d941dbbdf2d2563514fcc94d25e4af897a71681a02b637b212040a02080118880212150a0f0a05756e79616e120632303030303010c09a0c1a402d1c8c1e1a44bd56fe24947d6ed6cae27c6f8a46e3e9beaaad9798dc842ae4ea0c0a20f33144c8fad3490638455b65f63decdb74c347a7c97d0469f5de453fe36214323032322d31302d30335430363a35313a31375a6a410a027478123b0a076163635f736571122e696161317039703230667468306c7665647634736d7733327339377079386e74657230716e77747275382f32363418016a6d0a02747812670a097369676e617475726512584c52794d486870457656622b4a4a52396274624b346e7876696b626a36623671725a655933495171354f6f4d4369447a4d5554492b744e4a426a68465732583250657a62644d4e4870386c3942476e31336b552f34773d3d18016a5e0a0a636f696e5f7370656e7412370a077370656e646572122a696161317039703230667468306c7665647634736d7733327339377079386e74657230716e7774727538180112170a06616d6f756e74120b323030303030756e79616e18016a620a0d636f696e5f726563656976656412380a087265636569766572122a696161313778706676616b6d32616d67393632796c73366638347a336b656c6c3863356c396d72336676180112170a06616d6f756e74120b323030303030756e79616e18016a96010a087472616e7366657212390a09726563697069656e74122a696161313778706676616b6d32616d67393632796c73366638347a336b656c6c3863356c396d72336676180112360a0673656e646572122a696161317039703230667468306c7665647634736d7733327339377079386e74657230716e7774727538180112170a06616d6f756e74120b323030303030756e79616e18016a410a076d65737361676512360a0673656e646572122a696161317039703230667468306c7665647634736d7733327339377079386e74657230716e777472753818016a1a0a02747812140a03666565120b323030303030756e79616e18016a330a076d65737361676512280a06616374696f6e121c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e6418016a610a0a636f696e5f7370656e7412370a077370656e646572122a696161317039703230667468306c7665647634736d7733327339377079386e74657230716e77747275381801121a0a06616d6f756e74120e313030303030303030756e79616e18016a650a0d636f696e5f726563656976656412380a087265636569766572122a696161316b36636d636b7875757732647a7a6b76747a7239776c7467356c3633747361746b6c71357a791801121a0a06616d6f756e74120e313030303030303030756e79616e18016a99010a087472616e7366657212390a09726563697069656e74122a696161316b36636d636b7875757732647a7a6b76747a7239776c7467356c3633747361746b6c71357a79180112360a0673656e646572122a696161317039703230667468306c7665647634736d7733327339377079386e74657230716e77747275381801121a0a06616d6f756e74120e313030303030303030756e79616e18016a410a076d65737361676512360a0673656e646572122a696161317039703230667468306c7665647634736d7733327339377079386e74657230716e777472753818016a1b0a076d65737361676512100a066d6f64756c65120462616e6b1801").unwrap().as_slice()).unwrap(); + let mock_tx = random_transfer_tx_response.tx.as_ref().unwrap().clone(); + TendermintCoin::request_tx.mock_safe(move |_, _| { + let mock_tx = mock_tx.clone(); + MockResult::Return(Box::pin(async move { Ok(mock_tx) })) + }); let random_transfer_tx = TransactionEnum::CosmosTransaction(CosmosTransaction { - data: TxRaw::decode(random_transfer_tx_bytes.as_slice()).unwrap(), + data: TxRaw::decode( + random_transfer_tx_response + .tx + .as_ref() + .unwrap() + .encode_to_vec() + .as_slice(), + ) + .unwrap(), }); let error = block_on(coin.validate_fee(ValidateFeeArgs { fee_tx: &random_transfer_tx, expected_sender: &[], - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &DexFee::Standard(invalid_amount.clone()), min_block_number: 0, uuid: &[1; 16], @@ -3949,26 +4140,25 @@ pub mod tendermint_coin_tests { ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("sent to wrong address")), _ => panic!("Expected `WrongPaymentTx` wrong address, found {:?}", error), } + TendermintCoin::request_tx.clear_mock(); // dex fee tx sent during real swap // https://nyancat.iobscan.io/#/tx?txHash=8AA6B9591FE1EE93C8B89DE4F2C59B2F5D3473BD9FB5F3CFF6A5442BEDC881D7 - let dex_fee_hash = "8AA6B9591FE1EE93C8B89DE4F2C59B2F5D3473BD9FB5F3CFF6A5442BEDC881D7"; - let dex_fee_tx = block_on(coin.request_tx(dex_fee_hash.into())).unwrap(); + let dex_fee_tx_response = GetTxResponse::decode(hex::decode("0abc020a8e010a86010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e6412660a2a69616131647863376c64676b336e666e356b373671706c75703967397868786e7966346d6570396b7038122a696161316567307167617a37336a737676727676747a713478383233686d7a387161706c64643078347a1a0c0a05756e79616e120331303018a89bb00212670a500a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a2103d4f75874e5f2a51d9d22f747ebd94da63207b08c7b023b09865051f074eb7ea412040a020801180612130a0d0a05756e79616e12043130303010a08d061a40784831c62a96658e9b0c484bbf684465788701c4fbd46c744f20f4ade3dbba1152f279c8afb118ae500ed9dc1260a8125a0f173c91ea408a3a3e0bd42b226ae012da1508c59ab0021240384141364239353931464531454539334338423839444534463243353942324635443334373342443946423546334346463641353434324245444338383144372a403041314530413143324636333646373336443646373332453632363136453642324537363331363236353734363133313245344437333637353336353645363432c8055b7b226576656e7473223a5b7b2274797065223a22636f696e5f7265636569766564222c2261747472696275746573223a5b7b226b6579223a227265636569766572222c2276616c7565223a22696161316567307167617a37336a737676727676747a713478383233686d7a387161706c64643078347a227d2c7b226b6579223a22616d6f756e74222c2276616c7565223a22313030756e79616e227d5d7d2c7b2274797065223a22636f696e5f7370656e74222c2261747472696275746573223a5b7b226b6579223a227370656e646572222c2276616c7565223a2269616131647863376c64676b336e666e356b373671706c75703967397868786e7966346d6570396b7038227d2c7b226b6579223a22616d6f756e74222c2276616c7565223a22313030756e79616e227d5d7d2c7b2274797065223a226d657373616765222c2261747472696275746573223a5b7b226b6579223a22616374696f6e222c2276616c7565223a222f636f736d6f732e62616e6b2e763162657461312e4d736753656e64227d2c7b226b6579223a2273656e646572222c2276616c7565223a2269616131647863376c64676b336e666e356b373671706c75703967397868786e7966346d6570396b7038227d2c7b226b6579223a226d6f64756c65222c2276616c7565223a2262616e6b227d5d7d2c7b2274797065223a227472616e73666572222c2261747472696275746573223a5b7b226b6579223a22726563697069656e74222c2276616c7565223a22696161316567307167617a37336a737676727676747a713478383233686d7a387161706c64643078347a227d2c7b226b6579223a2273656e646572222c2276616c7565223a2269616131647863376c64676b336e666e356b373671706c75703967397868786e7966346d6570396b7038227d2c7b226b6579223a22616d6f756e74222c2276616c7565223a22313030756e79616e227d5d7d5d7d5d3abf031a5b0a0d636f696e5f726563656976656412360a087265636569766572122a696161316567307167617a37336a737676727676747a713478383233686d7a387161706c64643078347a12120a06616d6f756e741208313030756e79616e1a570a0a636f696e5f7370656e7412350a077370656e646572122a69616131647863376c64676b336e666e356b373671706c75703967397868786e7966346d6570396b703812120a06616d6f756e741208313030756e79616e1a770a076d65737361676512260a06616374696f6e121c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e6412340a0673656e646572122a69616131647863376c64676b336e666e356b373671706c75703967397868786e7966346d6570396b7038120e0a066d6f64756c65120462616e6b1a8d010a087472616e7366657212370a09726563697069656e74122a696161316567307167617a37336a737676727676747a713478383233686d7a387161706c64643078347a12340a0673656e646572122a69616131647863376c64676b336e666e356b373671706c75703967397868786e7966346d6570396b703812120a06616d6f756e741208313030756e79616e48a08d0650acdf035ad6020a152f636f736d6f732e74782e763162657461312e547812bc020a8e010a86010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e6412660a2a69616131647863376c64676b336e666e356b373671706c75703967397868786e7966346d6570396b7038122a696161316567307167617a37336a737676727676747a713478383233686d7a387161706c64643078347a1a0c0a05756e79616e120331303018a89bb00212670a500a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a2103d4f75874e5f2a51d9d22f747ebd94da63207b08c7b023b09865051f074eb7ea412040a020801180612130a0d0a05756e79616e12043130303010a08d061a40784831c62a96658e9b0c484bbf684465788701c4fbd46c744f20f4ade3dbba1152f279c8afb118ae500ed9dc1260a8125a0f173c91ea408a3a3e0bd42b226ae06214323032322d30392d32335431313a31313a35395a6a3f0a02747812390a076163635f736571122c69616131647863376c64676b336e666e356b373671706c75703967397868786e7966346d6570396b70382f3618016a6d0a02747812670a097369676e6174757265125865456778786971575a5936624445684c763268455a5869484163543731477830547944307265506275684653386e6e4972374559726c414f32647753594b675357673858504a487151496f36506776554b794a7134413d3d18016a5c0a0a636f696e5f7370656e7412370a077370656e646572122a69616131647863376c64676b336e666e356b373671706c75703967397868786e7966346d6570396b7038180112150a06616d6f756e74120931303030756e79616e18016a600a0d636f696e5f726563656976656412380a087265636569766572122a696161313778706676616b6d32616d67393632796c73366638347a336b656c6c3863356c396d72336676180112150a06616d6f756e74120931303030756e79616e18016a94010a087472616e7366657212390a09726563697069656e74122a696161313778706676616b6d32616d67393632796c73366638347a336b656c6c3863356c396d72336676180112360a0673656e646572122a69616131647863376c64676b336e666e356b373671706c75703967397868786e7966346d6570396b7038180112150a06616d6f756e74120931303030756e79616e18016a410a076d65737361676512360a0673656e646572122a69616131647863376c64676b336e666e356b373671706c75703967397868786e7966346d6570396b703818016a180a02747812120a03666565120931303030756e79616e18016a330a076d65737361676512280a06616374696f6e121c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e6418016a5b0a0a636f696e5f7370656e7412370a077370656e646572122a69616131647863376c64676b336e666e356b373671706c75703967397868786e7966346d6570396b7038180112140a06616d6f756e741208313030756e79616e18016a5f0a0d636f696e5f726563656976656412380a087265636569766572122a696161316567307167617a37336a737676727676747a713478383233686d7a387161706c64643078347a180112140a06616d6f756e741208313030756e79616e18016a93010a087472616e7366657212390a09726563697069656e74122a696161316567307167617a37336a737676727676747a713478383233686d7a387161706c64643078347a180112360a0673656e646572122a69616131647863376c64676b336e666e356b373671706c75703967397868786e7966346d6570396b7038180112140a06616d6f756e741208313030756e79616e18016a410a076d65737361676512360a0673656e646572122a69616131647863376c64676b336e666e356b373671706c75703967397868786e7966346d6570396b703818016a1b0a076d65737361676512100a066d6f64756c65120462616e6b1801").unwrap().as_slice()).unwrap(); + let mock_tx = dex_fee_tx_response.tx.as_ref().unwrap().clone(); + TendermintCoin::request_tx.mock_safe(move |_, _| { + let mock_tx = mock_tx.clone(); + MockResult::Return(Box::pin(async move { Ok(mock_tx) })) + }); - let pubkey = dex_fee_tx.auth_info.as_ref().unwrap().signer_infos[0] - .public_key - .as_ref() - .unwrap() - .value[2..] - .to_vec(); + let pubkey = get_tx_signer_pubkey_unprefixed(dex_fee_tx_response.tx.as_ref().unwrap(), 0); let dex_fee_tx = TransactionEnum::CosmosTransaction(CosmosTransaction { - data: TxRaw::decode(dex_fee_tx.encode_to_vec().as_slice()).unwrap(), + data: TxRaw::decode(dex_fee_tx_response.tx.as_ref().unwrap().encode_to_vec().as_slice()).unwrap(), }); let error = block_on(coin.validate_fee(ValidateFeeArgs { fee_tx: &dex_fee_tx, expected_sender: &[], - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &DexFee::Standard(invalid_amount), min_block_number: 0, uuid: &[1; 16], @@ -3986,7 +4176,6 @@ pub mod tendermint_coin_tests { let error = block_on(coin.validate_fee(ValidateFeeArgs { fee_tx: &dex_fee_tx, expected_sender: &DEX_FEE_ADDR_RAW_PUBKEY, - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &DexFee::Standard(valid_amount.clone().into()), min_block_number: 0, uuid: &[1; 16], @@ -4003,7 +4192,6 @@ pub mod tendermint_coin_tests { let error = block_on(coin.validate_fee(ValidateFeeArgs { fee_tx: &dex_fee_tx, expected_sender: &pubkey, - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &DexFee::Standard(valid_amount.into()), min_block_number: 0, uuid: &[1; 16], @@ -4017,31 +4205,93 @@ pub mod tendermint_coin_tests { } // https://nyancat.iobscan.io/#/tx?txHash=5939A9D1AF57BB828714E0C4C4D7F2AEE349BB719B0A1F25F8FBCC3BB227C5F9 - let fee_with_memo_hash = "5939A9D1AF57BB828714E0C4C4D7F2AEE349BB719B0A1F25F8FBCC3BB227C5F9"; - let fee_with_memo_tx = block_on(coin.request_tx(fee_with_memo_hash.into())).unwrap(); - - let pubkey = fee_with_memo_tx.auth_info.as_ref().unwrap().signer_infos[0] - .public_key - .as_ref() - .unwrap() - .value[2..] - .to_vec(); + let fee_with_memo_tx_response = GetTxResponse::decode(hex::decode("0ae2020ab2010a84010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e6412640a2a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76122a696161316567307167617a37336a737676727676747a713478383233686d7a387161706c64643078347a1a0a0a036e696d1203313030122463616536303131622d393831302d343731302d623738342d31653564643062336130643018dbe0bb0212690a510a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21025a37975c079a7543603fcab24e2565a4adee3cf9af8934690e103282fa40251112040a02080118a50412140a0e0a05756e79616e1205353030303010a08d061a4078295295db2e305b7b53c6b7154f1d6b1c311fd10aaf56ad96840e59f403bae045f2ca5920e7bef679eacd200d6f30eca7d3571b93dcde38c8c130e1c1d9e4c712f41508f8dfbb021240353933394139443141463537424238323837313445304334433444374632414545333439424237313942304131463235463846424343334242323237433546392a403041314530413143324636333646373336443646373332453632363136453642324537363331363236353734363133313245344437333637353336353645363432c2055b7b226576656e7473223a5b7b2274797065223a22636f696e5f7265636569766564222c2261747472696275746573223a5b7b226b6579223a227265636569766572222c2276616c7565223a22696161316567307167617a37336a737676727676747a713478383233686d7a387161706c64643078347a227d2c7b226b6579223a22616d6f756e74222c2276616c7565223a223130306e696d227d5d7d2c7b2274797065223a22636f696e5f7370656e74222c2261747472696275746573223a5b7b226b6579223a227370656e646572222c2276616c7565223a22696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76227d2c7b226b6579223a22616d6f756e74222c2276616c7565223a223130306e696d227d5d7d2c7b2274797065223a226d657373616765222c2261747472696275746573223a5b7b226b6579223a22616374696f6e222c2276616c7565223a222f636f736d6f732e62616e6b2e763162657461312e4d736753656e64227d2c7b226b6579223a2273656e646572222c2276616c7565223a22696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76227d2c7b226b6579223a226d6f64756c65222c2276616c7565223a2262616e6b227d5d7d2c7b2274797065223a227472616e73666572222c2261747472696275746573223a5b7b226b6579223a22726563697069656e74222c2276616c7565223a22696161316567307167617a37336a737676727676747a713478383233686d7a387161706c64643078347a227d2c7b226b6579223a2273656e646572222c2276616c7565223a22696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76227d2c7b226b6579223a22616d6f756e74222c2276616c7565223a223130306e696d227d5d7d5d7d5d3ab9031a590a0d636f696e5f726563656976656412360a087265636569766572122a696161316567307167617a37336a737676727676747a713478383233686d7a387161706c64643078347a12100a06616d6f756e7412063130306e696d1a550a0a636f696e5f7370656e7412350a077370656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a7612100a06616d6f756e7412063130306e696d1a770a076d65737361676512260a06616374696f6e121c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e6412340a0673656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76120e0a066d6f64756c65120462616e6b1a8b010a087472616e7366657212370a09726563697069656e74122a696161316567307167617a37336a737676727676747a713478383233686d7a387161706c64643078347a12340a0673656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a7612100a06616d6f756e7412063130306e696d48a08d0650d4e1035afc020a152f636f736d6f732e74782e763162657461312e547812e2020ab2010a84010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e6412640a2a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76122a696161316567307167617a37336a737676727676747a713478383233686d7a387161706c64643078347a1a0a0a036e696d1203313030122463616536303131622d393831302d343731302d623738342d31653564643062336130643018dbe0bb0212690a510a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21025a37975c079a7543603fcab24e2565a4adee3cf9af8934690e103282fa40251112040a02080118a50412140a0e0a05756e79616e1205353030303010a08d061a4078295295db2e305b7b53c6b7154f1d6b1c311fd10aaf56ad96840e59f403bae045f2ca5920e7bef679eacd200d6f30eca7d3571b93dcde38c8c130e1c1d9e4c76214323032322d31302d30345431313a33343a35355a6a410a027478123b0a076163635f736571122e696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a762f35343918016a6d0a02747812670a097369676e6174757265125865436c536c6473754d4674375538613346553864617877784839454b723161746c6f514f57665144757542463873705a494f652b396e6e717a53414e627a447370394e5847355063336a6a497754446877646e6b78773d3d18016a5d0a0a636f696e5f7370656e7412370a077370656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76180112160a06616d6f756e74120a3530303030756e79616e18016a610a0d636f696e5f726563656976656412380a087265636569766572122a696161313778706676616b6d32616d67393632796c73366638347a336b656c6c3863356c396d72336676180112160a06616d6f756e74120a3530303030756e79616e18016a95010a087472616e7366657212390a09726563697069656e74122a696161313778706676616b6d32616d67393632796c73366638347a336b656c6c3863356c396d72336676180112360a0673656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76180112160a06616d6f756e74120a3530303030756e79616e18016a410a076d65737361676512360a0673656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a7618016a190a02747812130a03666565120a3530303030756e79616e18016a330a076d65737361676512280a06616374696f6e121c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e6418016a590a0a636f696e5f7370656e7412370a077370656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76180112120a06616d6f756e7412063130306e696d18016a5d0a0d636f696e5f726563656976656412380a087265636569766572122a696161316567307167617a37336a737676727676747a713478383233686d7a387161706c64643078347a180112120a06616d6f756e7412063130306e696d18016a91010a087472616e7366657212390a09726563697069656e74122a696161316567307167617a37336a737676727676747a713478383233686d7a387161706c64643078347a180112360a0673656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76180112120a06616d6f756e7412063130306e696d18016a410a076d65737361676512360a0673656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a7618016a1b0a076d65737361676512100a066d6f64756c65120462616e6b1801").unwrap().as_slice()).unwrap(); + let mock_tx = fee_with_memo_tx_response.tx.as_ref().unwrap().clone(); + TendermintCoin::request_tx.mock_safe(move |_, _| { + let mock_tx = mock_tx.clone(); + MockResult::Return(Box::pin(async move { Ok(mock_tx) })) + }); + let pubkey = get_tx_signer_pubkey_unprefixed(fee_with_memo_tx_response.tx.as_ref().unwrap(), 0); let fee_with_memo_tx = TransactionEnum::CosmosTransaction(CosmosTransaction { - data: TxRaw::decode(fee_with_memo_tx.encode_to_vec().as_slice()).unwrap(), + data: TxRaw::decode( + fee_with_memo_tx_response + .tx + .as_ref() + .unwrap() + .encode_to_vec() + .as_slice(), + ) + .unwrap(), }); let uuid: Uuid = "cae6011b-9810-4710-b784-1e5dd0b3a0d0".parse().unwrap(); - let amount: BigDecimal = "0.0001".parse().unwrap(); + let dex_fee = DexFee::Standard(MmNumber::from("0.0001")); + block_on( + coin.validate_fee_for_denom(&fee_with_memo_tx, &pubkey, &dex_fee, 6, uuid.as_bytes(), "nim".into()) + .compat(), + ) + .unwrap(); + TendermintCoin::request_tx.clear_mock(); + } + + #[test] + fn validate_taker_fee_with_burn_test() { + const NUCLEUS_TEST_SEED: &str = "nucleus test seed"; + + let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc(); + let conf = TendermintConf { + avg_blocktime: AVG_BLOCKTIME, + derivation_path: None, + }; + + let key_pair = key_pair_from_seed(NUCLEUS_TEST_SEED).unwrap(); + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); + let nucleus_nodes = vec![RpcNode::for_test("http://localhost:26657")]; + let iris_ibc_nucleus_protocol = get_iris_ibc_nucleus_protocol(); + let iris_ibc_nucleus_denom = + String::from("ibc/F7F28FF3C09024A0225EDBBDB207E5872D2B4EF2FB874FE47B05EF9C9A7D211C"); + let coin = block_on(TendermintCoin::init( + &ctx, + "NUCLEUS-TEST".to_string(), + conf, + iris_ibc_nucleus_protocol, + nucleus_nodes, + false, + activation_policy, + false, + )) + .unwrap(); + + // tx from docker test (no real swaps yet) + let fee_with_burn_tx = Tx::decode(hex::decode("0abd030a91030a212f636f736d6f732e62616e6b2e763162657461312e4d73674d756c746953656e6412eb020a770a2a6e7563316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c65647736337912490a446962632f4637463238464633433039303234413032323545444242444232303745353837324432423445463246423837344645343742303545463943394137443231314312013912770a2a6e7563316567307167617a37336a737676727676747a713478383233686d7a387161706c656877326b3212490a446962632f4637463238464633433039303234413032323545444242444232303745353837324432423445463246423837344645343742303545463943394137443231314312013712770a2a6e756331797937346b393278707437367a616e6c3276363837636175393861666d70363071723564743712490a446962632f46374632384646334330393032344130323235454442424442323037453538373244324234454632464238373446453437423035454639433941374432313143120132122433656338646436352d313036342d346630362d626166332d66373265623563396230346418b50a12680a500a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21025a37975c079a7543603fcab24e2565a4adee3cf9af8934690e103282fa40251112040a020801180312140a0e0a05756e75636c1205333338383510c8d0071a40852793cb49aeaff1f895fa18a4fc0a63a5c54813fd57b3f5a2af9d0d849a04cb4abe81bc8feb4178603e1c9eed4e4464157f0bffb7cf51ef3beb80f48cd73b91").unwrap().as_slice()).unwrap(); + let mock_tx = fee_with_burn_tx.clone(); + TendermintCoin::request_tx.mock_safe(move |_, _| { + let mock_tx = mock_tx.clone(); + MockResult::Return(Box::pin(async move { Ok(mock_tx) })) + }); + + let pubkey = get_tx_signer_pubkey_unprefixed(&fee_with_burn_tx, 0); + let fee_with_burn_cosmos_tx = TransactionEnum::CosmosTransaction(CosmosTransaction { + data: TxRaw::decode(fee_with_burn_tx.encode_to_vec().as_slice()).unwrap(), + }); + + let uuid: Uuid = "3ec8dd65-1064-4f06-baf3-f72eb5c9b04d".parse().unwrap(); + let dex_fee = DexFee::WithBurn { + fee_amount: MmNumber::from("0.000007"), // Amount is 0.008, both dex and burn fees rounded down + burn_amount: MmNumber::from("0.000002"), + burn_destination: DexFeeBurnDestination::PreBurnAccount, + }; block_on( coin.validate_fee_for_denom( - &fee_with_memo_tx, + &fee_with_burn_cosmos_tx, &pubkey, - &DEX_FEE_ADDR_RAW_PUBKEY, - &amount, + &dex_fee, 6, uuid.as_bytes(), - "nim".into(), + iris_ibc_nucleus_denom, ) .compat(), ) diff --git a/mm2src/coins/tendermint/tendermint_token.rs b/mm2src/coins/tendermint/tendermint_token.rs index 8eeb0cd09b..97dec960a6 100644 --- a/mm2src/coins/tendermint/tendermint_token.rs +++ b/mm2src/coins/tendermint/tendermint_token.rs @@ -105,16 +105,9 @@ impl TendermintToken { #[async_trait] #[allow(unused_variables)] impl SwapOps for TendermintToken { - async fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, uuid: &[u8], expire_at: u64) -> TransactionResult { + async fn send_taker_fee(&self, dex_fee: DexFee, uuid: &[u8], expire_at: u64) -> TransactionResult { self.platform_coin - .send_taker_fee_for_denom( - fee_addr, - dex_fee.fee_amount().into(), - self.denom.clone(), - self.decimals, - uuid, - expire_at, - ) + .send_taker_fee_for_denom(&dex_fee, self.denom.clone(), self.decimals, uuid, expire_at) .compat() .await } @@ -182,8 +175,7 @@ impl SwapOps for TendermintToken { .validate_fee_for_denom( validate_fee_args.fee_tx, validate_fee_args.expected_sender, - validate_fee_args.fee_addr, - &validate_fee_args.dex_fee.fee_amount().into(), + validate_fee_args.dex_fee, self.decimals, validate_fee_args.uuid, self.denom.to_string(), @@ -482,6 +474,9 @@ impl MarketCoinOps for TendermintToken { #[inline] fn min_trading_vol(&self) -> MmNumber { self.min_tx_amount().into() } + #[inline] + fn should_burn_dex_fee(&self) -> bool { true } + fn is_trezor(&self) -> bool { self.platform_coin.is_trezor() } } diff --git a/mm2src/coins/test_coin.rs b/mm2src/coins/test_coin.rs index 308868dc3c..516eff228e 100644 --- a/mm2src/coins/test_coin.rs +++ b/mm2src/coins/test_coin.rs @@ -26,6 +26,7 @@ use keys::KeyPair; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::{BigDecimal, MmNumber}; +#[cfg(any(test, feature = "for-tests"))] use mocktopus::macros::*; use rpc::v1::types::Bytes as BytesJson; use serde_json::Value as Json; @@ -57,8 +58,7 @@ impl TestCoin { } #[async_trait] -#[mockable] -#[async_trait] +#[cfg_attr(any(test, feature = "for-tests"), mockable)] impl MarketCoinOps for TestCoin { fn ticker(&self) -> &str { &self.ticker } @@ -108,13 +108,17 @@ impl MarketCoinOps for TestCoin { fn min_trading_vol(&self) -> MmNumber { MmNumber::from("0.00777") } + fn is_kmd(&self) -> bool { &self.ticker == "KMD" } + + fn should_burn_dex_fee(&self) -> bool { false } + fn is_trezor(&self) -> bool { unimplemented!() } } #[async_trait] -#[mockable] +#[cfg_attr(any(test, feature = "for-tests"), mockable)] impl SwapOps for TestCoin { - async fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, uuid: &[u8], expire_at: u64) -> TransactionResult { + async fn send_taker_fee(&self, dex_fee: DexFee, uuid: &[u8], expire_at: u64) -> TransactionResult { unimplemented!() } @@ -265,7 +269,7 @@ impl MakerSwapTakerCoin for TestCoin { } #[async_trait] -#[mockable] +#[cfg_attr(any(test, feature = "for-tests"), mockable)] impl WatcherOps for TestCoin { fn create_maker_payment_spend_preimage( &self, @@ -339,7 +343,7 @@ impl WatcherOps for TestCoin { } #[async_trait] -#[mockable] +#[cfg_attr(any(test, feature = "for-tests"), mockable)] impl MmCoin for TestCoin { fn is_asset_chain(&self) -> bool { unimplemented!() } @@ -468,7 +472,7 @@ impl ParseCoinAssocTypes for TestCoin { } #[async_trait] -#[mockable] +#[cfg_attr(any(test, feature = "for-tests"), mockable)] impl TakerCoinSwapOpsV2 for TestCoin { async fn send_taker_funding(&self, args: SendTakerFundingArgs<'_>) -> Result { todo!() } @@ -571,4 +575,7 @@ impl CommonSwapOpsV2 for TestCoin { fn derive_htlc_pubkey_v2(&self, _swap_unique_data: &[u8]) -> Self::Pubkey { todo!() } fn derive_htlc_pubkey_v2_bytes(&self, _swap_unique_data: &[u8]) -> Vec { todo!() } + + #[inline(always)] + fn taker_pubkey_bytes(&self) -> Option> { todo!() } } diff --git a/mm2src/coins/utxo/bch.rs b/mm2src/coins/utxo/bch.rs index 1a37738db0..7f49aefce8 100644 --- a/mm2src/coins/utxo/bch.rs +++ b/mm2src/coins/utxo/bch.rs @@ -875,16 +875,8 @@ impl UtxoCommonOps for BchCoin { #[async_trait] impl SwapOps for BchCoin { #[inline] - async fn send_taker_fee( - &self, - fee_addr: &[u8], - dex_fee: DexFee, - _uuid: &[u8], - _expire_at: u64, - ) -> TransactionResult { - utxo_common::send_taker_fee(self.clone(), fee_addr, dex_fee) - .compat() - .await + async fn send_taker_fee(&self, dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionResult { + utxo_common::send_taker_fee(self.clone(), dex_fee).compat().await } #[inline] @@ -942,9 +934,8 @@ impl SwapOps for BchCoin { tx, utxo_common::DEFAULT_FEE_VOUT, validate_fee_args.expected_sender, - validate_fee_args.dex_fee, + validate_fee_args.dex_fee.clone(), validate_fee_args.min_block_number, - validate_fee_args.fee_addr, ) .compat() .await @@ -1282,6 +1273,8 @@ impl MarketCoinOps for BchCoin { fn min_trading_vol(&self) -> MmNumber { utxo_common::min_trading_vol(self.as_ref()) } + fn should_burn_dex_fee(&self) -> bool { utxo_common::should_burn_dex_fee() } + fn is_trezor(&self) -> bool { self.as_ref().priv_key_policy.is_trezor() } } diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index dc5fc2a934..4adedb9439 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -510,16 +510,8 @@ impl UtxoStandardOps for QtumCoin { #[async_trait] impl SwapOps for QtumCoin { #[inline] - async fn send_taker_fee( - &self, - fee_addr: &[u8], - dex_fee: DexFee, - _uuid: &[u8], - _expire_at: u64, - ) -> TransactionResult { - utxo_common::send_taker_fee(self.clone(), fee_addr, dex_fee) - .compat() - .await + async fn send_taker_fee(&self, dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionResult { + utxo_common::send_taker_fee(self.clone(), dex_fee).compat().await } #[inline] @@ -577,9 +569,8 @@ impl SwapOps for QtumCoin { tx, utxo_common::DEFAULT_FEE_VOUT, validate_fee_args.expected_sender, - validate_fee_args.dex_fee, + validate_fee_args.dex_fee.clone(), validate_fee_args.min_block_number, - validate_fee_args.fee_addr, ) .compat() .await @@ -898,6 +889,8 @@ impl MarketCoinOps for QtumCoin { fn min_trading_vol(&self) -> MmNumber { utxo_common::min_trading_vol(self.as_ref()) } fn is_trezor(&self) -> bool { self.as_ref().priv_key_policy.is_trezor() } + + fn should_burn_dex_fee(&self) -> bool { utxo_common::should_burn_dex_fee() } } #[async_trait] diff --git a/mm2src/coins/utxo/slp.rs b/mm2src/coins/utxo/slp.rs index 695e1c6889..9b9bd5a83e 100644 --- a/mm2src/coins/utxo/slp.rs +++ b/mm2src/coins/utxo/slp.rs @@ -725,7 +725,6 @@ impl SlpToken { &self, tx: UtxoTx, expected_sender: &[u8], - fee_addr: &[u8], amount: BigDecimal, min_block_number: u64, ) -> Result<(), MmError> { @@ -759,9 +758,8 @@ impl SlpToken { tx, SLP_FEE_VOUT, expected_sender, - &DexFee::Standard(self.platform_dust_dec().into()), + DexFee::Standard(self.platform_dust_dec().into()), min_block_number, - fee_addr, ); validate_fut @@ -1214,21 +1212,17 @@ impl MarketCoinOps for SlpToken { fn min_trading_vol(&self) -> MmNumber { big_decimal_from_sat_unsigned(1, self.decimals()).into() } + fn should_burn_dex_fee(&self) -> bool { false } + fn is_trezor(&self) -> bool { self.as_ref().priv_key_policy.is_trezor() } } #[async_trait] impl SwapOps for SlpToken { - async fn send_taker_fee( - &self, - fee_addr: &[u8], - dex_fee: DexFee, - _uuid: &[u8], - _expire_at: u64, - ) -> TransactionResult { - let fee_pubkey = try_tx_s!(Public::from_slice(fee_addr)); + async fn send_taker_fee(&self, dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionResult { + let fee_pubkey = try_tx_s!(Public::from_slice(self.dex_pubkey())); let script_pubkey = ScriptBuilder::build_p2pkh(&fee_pubkey.address_hash().into()).into(); - let amount = try_tx_s!(dex_fee.fee_uamount(self.decimals())); + let amount = try_tx_s!(dex_fee.fee_amount_as_u64(self.decimals())); let slp_out = SlpOutput { amount, script_pubkey }; let (preimage, recently_spent) = try_tx_s!(self.generate_slp_tx_preimage(vec![slp_out]).await); @@ -1354,7 +1348,6 @@ impl SwapOps for SlpToken { self.validate_dex_fee( tx, validate_fee_args.expected_sender, - validate_fee_args.fee_addr, amount.into(), validate_fee_args.min_block_number, ) diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index 2722dfcdeb..1a867791f0 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -13,13 +13,13 @@ use crate::utxo::utxo_hd_wallet::UtxoHDAddress; use crate::utxo::utxo_withdraw::{InitUtxoWithdraw, StandardUtxoWithdraw, UtxoWithdraw}; use crate::watcher_common::validate_watcher_reward; use crate::{scan_for_new_addresses_impl, CanRefundHtlc, CoinBalance, CoinWithDerivationMethod, ConfirmPaymentInput, - DexFee, GenPreimageResult, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, GetWithdrawSenderAddress, - RawTransactionError, RawTransactionRequest, RawTransactionRes, RawTransactionResult, - RefundFundingSecretArgs, RefundMakerPaymentSecretArgs, RefundPaymentArgs, RewardTarget, - SearchForSwapTxSpendInput, SendMakerPaymentArgs, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, - SendTakerFundingArgs, SignRawTransactionEnum, SignRawTransactionRequest, SignUtxoTransactionParams, - SignatureError, SignatureResult, SpendMakerPaymentArgs, SpendPaymentArgs, SwapOps, - SwapTxTypeWithSecretHash, TradePreimageValue, TransactionData, TransactionFut, TransactionResult, + DexFee, DexFeeBurnDestination, GenPreimageResult, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, + GetWithdrawSenderAddress, RawTransactionError, RawTransactionRequest, RawTransactionRes, + RawTransactionResult, RefundFundingSecretArgs, RefundMakerPaymentSecretArgs, RefundPaymentArgs, + RewardTarget, SearchForSwapTxSpendInput, SendMakerPaymentArgs, SendMakerPaymentSpendPreimageInput, + SendPaymentArgs, SendTakerFundingArgs, SignRawTransactionEnum, SignRawTransactionRequest, + SignUtxoTransactionParams, SignatureError, SignatureResult, SpendMakerPaymentArgs, SpendPaymentArgs, + SwapOps, SwapTxTypeWithSecretHash, TradePreimageValue, TransactionData, TransactionFut, TransactionResult, TxFeeDetails, TxGenError, TxMarshalingErr, TxPreimageWithSig, ValidateAddressResult, ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, ValidateSwapV2TxError, ValidateSwapV2TxResult, ValidateTakerFundingArgs, ValidateTakerFundingSpendPreimageError, @@ -53,6 +53,7 @@ use mm2_number::bigdecimal_custom::CheckedDivision; use mm2_number::{BigDecimal, MmNumber}; use primitives::hash::H512; use rpc::v1::types::{Bytes as BytesJson, ToTxHash, TransactionInputEnum, H256 as H256Json}; +#[cfg(test)] use rpc_clients::NativeClientImpl; use script::{Builder, Opcode, Script, ScriptAddress, TransactionInputSigner, UnsignedTransactionInput}; use secp256k1::{PublicKey, Signature as SecpSignature}; use serde_json::{self as json}; @@ -63,6 +64,8 @@ use std::collections::hash_map::{Entry, HashMap}; use std::convert::TryFrom; use std::str::FromStr; use std::sync::atomic::Ordering as AtomicOrdering; +#[cfg(test)] +use utxo_common_tests::{utxo_coin_fields_for_test, utxo_coin_from_fields}; use utxo_signer::with_key_pair::{calc_and_sign_sighash, p2sh_spend, signature_hash_to_sign, SIGHASH_ALL, SIGHASH_SINGLE}; use utxo_signer::UtxoSignerOps; @@ -70,7 +73,7 @@ use utxo_signer::UtxoSignerOps; pub mod utxo_tx_history_v2_common; pub const DEFAULT_FEE_VOUT: usize = 0; -pub const DEFAULT_SWAP_TX_SPEND_SIZE: u64 = 305; +pub const DEFAULT_SWAP_TX_SPEND_SIZE: u64 = 496; // TODO: checking with komodo-like tx size, included the burn output pub const DEFAULT_SWAP_VOUT: usize = 0; pub const DEFAULT_SWAP_VIN: usize = 0; const MIN_BTC_TRADING_VOL: &str = "0.00777"; @@ -1239,43 +1242,77 @@ pub async fn sign_and_send_taker_funding_spend( Ok(final_tx) } -async fn gen_taker_payment_spend_preimage( +// Make tx preimage to spend taker payment for swaps V2 +async fn gen_taker_payment_spend_preimage( coin: &T, args: &GenTakerPaymentSpendArgs<'_, T>, n_time: NTimeSetting, ) -> GenPreimageResInner { - let dex_fee_address = address_from_raw_pubkey( - args.dex_fee_pub, - coin.as_ref().conf.address_prefixes.clone(), - coin.as_ref().conf.checksum_type, - coin.as_ref().conf.bech32_hrp.clone(), - coin.addr_format().clone(), - ) - .map_to_mm(|e| TxGenError::AddressDerivation(format!("Failed to derive dex_fee_address: {}", e)))?; - - let mut outputs = generate_taker_fee_tx_outputs(coin.as_ref().decimals, dex_fee_address.hash(), args.dex_fee)?; - if let DexFee::WithBurn { .. } = args.dex_fee { - let script = output_script(args.maker_address).map_to_mm(|e| { - TxGenError::Other(format!( - "Couldn't generate output script for maker address {}, error {}", - args.maker_address, e - )) - })?; - let tx_fee = coin - .get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) - .await?; - let maker_value = args - .taker_tx - .first_output() - .map_to_mm(|e| TxGenError::PrevTxIsNotValid(e.to_string()))? - .value - - outputs[0].value - - outputs[1].value - - tx_fee; - outputs.push(TransactionOutput { - value: maker_value, - script_pubkey: script.to_bytes(), - }) + let mut outputs = generate_taker_fee_tx_outputs(coin, args.dex_fee).map_err(TxGenError::Other)?; + match args.dex_fee { + &DexFee::WithBurn { .. } | &DexFee::NoFee => { + let script = output_script(args.maker_address).map_to_mm(|e| { + TxGenError::Other(format!( + "Couldn't generate output script for maker address {}, error {}", + args.maker_address, e + )) + })?; + let tx_fee = coin + .get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) + .await?; + let dex_fee_value = if matches!(args.dex_fee, &DexFee::WithBurn { .. }) { + outputs[0].value + outputs[1].value + } else { + 0 + }; + let prev_value = args + .taker_tx + .first_output() + .map_to_mm(|e| TxGenError::PrevTxIsNotValid(e.to_string()))? + .value; + let maker_value = prev_value + .checked_sub(dex_fee_value) + .ok_or(TxGenError::PrevOutputTooLow(format!( + "taker value too low: {}", + prev_value + )))? + .checked_sub(tx_fee) + .ok_or(TxGenError::PrevOutputTooLow(format!( + "taker value too low: {}", + prev_value + )))?; + // taker also adds maker output as we can't use SIGHASH_SINGLE with two outputs, dex fee and burn, + // and both the maker and taker sign all outputs: + outputs.push(TransactionOutput { + value: maker_value, + script_pubkey: script.to_bytes(), + }) + }, + &DexFee::Standard(..) => {}, // We do not add maker output here, only the single dex fee output (signed with SIGHASH_SINGLE) is created by the taker or validated by the maker + } + + #[cfg(feature = "run-docker-tests")] + { + match *args.dex_fee { + DexFee::NoFee => { + if args.taker_pub.to_vec().as_slice() != coin.burn_pubkey() { + panic!("taker pubkey must be equal to burn pubkey for DexFee::NoFee"); + } + assert_eq!(outputs.len(), 1); // only the maker output + }, + DexFee::Standard(..) => { + if args.taker_pub.to_vec().as_slice() == coin.burn_pubkey() { + panic!("taker pubkey must NOT be equal to burn pubkey for DexFee::Standard"); + } + assert_eq!(outputs.len(), 1); // only the dex fee output (maker output will be added later) + }, + DexFee::WithBurn { .. } => { + if args.taker_pub.to_vec().as_slice() == coin.burn_pubkey() { + panic!("taker pubkey must NOT be equal to burn pubkey for DexFee::WithBurn"); + } + assert_eq!(outputs.len(), 3); // dex fee, burn and maker outputs + }, + } } p2sh_spending_tx_preimage( @@ -1290,7 +1327,7 @@ async fn gen_taker_payment_spend_preimage( .map_to_mm(TxGenError::Legacy) } -pub async fn gen_and_sign_taker_payment_spend_preimage( +pub async fn gen_and_sign_taker_payment_spend_preimage( coin: &T, args: &GenTakerPaymentSpendArgs<'_, T>, htlc_keypair: &KeyPair, @@ -1307,7 +1344,7 @@ pub async fn gen_and_sign_taker_payment_spend_preimage( let sig_hash_type = match args.dex_fee { DexFee::Standard(_) => SIGHASH_SINGLE, - DexFee::WithBurn { .. } => SIGHASH_ALL, + DexFee::WithBurn { .. } | DexFee::NoFee => SIGHASH_ALL, }; let signature = calc_and_sign_sighash( @@ -1350,7 +1387,7 @@ pub async fn validate_taker_payment_spend_preimage( let sig_hash_type = match gen_args.dex_fee { DexFee::Standard(_) => SIGHASH_SINGLE, - DexFee::WithBurn { .. } => SIGHASH_ALL, + DexFee::WithBurn { .. } | DexFee::NoFee => SIGHASH_ALL, }; let sig_hash = signature_hash_to_sign( @@ -1401,6 +1438,8 @@ pub async fn sign_and_broadcast_taker_payment_spend( payment_input.amount = payment_output.value; signer.consensus_branch_id = coin.as_ref().conf.consensus_branch_id; + // Add the maker output if DexFee is Standard (when the single dex fee output is signed with SIGHASH_SINGLE) + // (in other DexFee options the make output is added in gen_taker_payment_spend_preimage fn) if let DexFee::Standard(dex_fee) = gen_args.dex_fee { let dex_fee_sat = try_tx_s!(sat_from_big_decimal(&dex_fee.to_decimal(), coin.as_ref().decimals)); @@ -1434,7 +1473,7 @@ pub async fn sign_and_broadcast_taker_payment_spend( let mut taker_signature_with_sighash = preimage.signature.to_vec(); let taker_sig_hash = match gen_args.dex_fee { DexFee::Standard(_) => (SIGHASH_SINGLE | coin.as_ref().conf.fork_id) as u8, - DexFee::WithBurn { .. } => (SIGHASH_ALL | coin.as_ref().conf.fork_id) as u8, + DexFee::WithBurn { .. } | DexFee::NoFee => (SIGHASH_ALL | coin.as_ref().conf.fork_id) as u8, }; taker_signature_with_sighash.push(taker_sig_hash); @@ -1461,47 +1500,79 @@ pub async fn sign_and_broadcast_taker_payment_spend( Ok(final_tx) } -pub fn send_taker_fee(coin: T, fee_pub_key: &[u8], dex_fee: DexFee) -> TransactionFut +pub fn send_taker_fee(coin: T, dex_fee: DexFee) -> TransactionFut where - T: UtxoCommonOps + GetUtxoListOps, + T: UtxoCommonOps + GetUtxoListOps + SwapOps, { - let address = try_tx_fus!(address_from_raw_pubkey( - fee_pub_key, - coin.as_ref().conf.address_prefixes.clone(), - coin.as_ref().conf.checksum_type, - coin.as_ref().conf.bech32_hrp.clone(), - coin.addr_format().clone(), - )); + let outputs = try_tx_fus!(generate_taker_fee_tx_outputs(&coin, &dex_fee,)); - let outputs = try_tx_fus!(generate_taker_fee_tx_outputs( - coin.as_ref().decimals, - address.hash(), - &dex_fee, - )); + #[cfg(feature = "run-docker-tests")] + { + let taker_pub = coin.derive_htlc_pubkey(&[]); + match dex_fee { + DexFee::NoFee => { + panic!("should not send dex fee for DexFee::NoFee"); + }, + DexFee::Standard(..) => { + if taker_pub.as_slice() == coin.burn_pubkey() { + panic!("taker pubkey must NOT be equal to burn pubkey for DexFee::Standard"); + } + assert_eq!(outputs.len(), 1); + }, + DexFee::WithBurn { .. } => { + if taker_pub.as_slice() == coin.burn_pubkey() { + panic!("taker pubkey must NOT be equal to burn pubkey for DexFee::WithBurn"); + } + assert_eq!(outputs.len(), 2); + }, + } + } send_outputs_from_my_address(coin, outputs) } -fn generate_taker_fee_tx_outputs( - decimals: u8, - address_hash: &AddressHashEnum, - dex_fee: &DexFee, -) -> Result, MmError> { - let fee_amount = dex_fee.fee_uamount(decimals)?; - - let mut outputs = vec![TransactionOutput { - value: fee_amount, - script_pubkey: Builder::build_p2pkh(address_hash).to_bytes(), - }]; - - if let Some(burn_amount) = dex_fee.burn_uamount(decimals)? { - outputs.push(TransactionOutput { - value: burn_amount, - script_pubkey: Builder::default().push_opcode(Opcode::OP_RETURN).into_bytes(), - }); +// Create dex fee (burn fee) outputs +fn generate_taker_fee_tx_outputs(coin: &T, dex_fee: &DexFee) -> Result, String> +where + T: UtxoCommonOps + SwapOps, +{ + match dex_fee { + DexFee::NoFee => Ok(vec![]), + // TODO: return an error for DexFee::Standard like 'dex fee must contain burn amount' when nodes upgraded to this code + DexFee::Standard(_) | DexFee::WithBurn { .. } => { + let dex_address = dex_address(coin)?; + let burn_address = burn_address(coin)?; + let fee_amount = dex_fee + .fee_amount_as_u64(coin.as_ref().decimals) + .map_err(|err| err.to_string())?; + + let mut outputs = vec![TransactionOutput { + value: fee_amount, + script_pubkey: Builder::build_p2pkh(dex_address.hash()).to_bytes(), + }]; + + if let DexFee::WithBurn { + fee_amount: _, + burn_amount, + burn_destination, + } = dex_fee + { + let burn_amount_u64 = sat_from_big_decimal(&burn_amount.to_decimal(), coin.as_ref().decimals) + .map_err(|err| err.to_string())?; + match burn_destination { + DexFeeBurnDestination::KmdOpReturn => outputs.push(TransactionOutput { + value: burn_amount_u64, + script_pubkey: Builder::default().push_opcode(Opcode::OP_RETURN).into_bytes(), + }), + DexFeeBurnDestination::PreBurnAccount => outputs.push(TransactionOutput { + value: burn_amount_u64, + script_pubkey: Builder::build_p2pkh(burn_address.hash()).to_bytes(), + }), + }; + } + Ok(outputs) + }, } - - Ok(outputs) } pub fn send_maker_payment(coin: T, args: SendPaymentArgs) -> TransactionFut @@ -2037,7 +2108,7 @@ pub fn check_all_utxo_inputs_signed_by_pub( Ok(true) } -pub fn watcher_validate_taker_fee( +pub fn watcher_validate_taker_fee( coin: &T, input: WatcherValidateTakerFeeInput, output_index: usize, @@ -2046,8 +2117,6 @@ pub fn watcher_validate_taker_fee( let sender_pubkey = input.sender_pubkey.clone(); let min_block_number = input.min_block_number; let lock_duration = input.lock_duration; - let fee_addr = input.fee_addr.to_vec(); - let fut = async move { let taker_fee_hash_len = input.taker_fee_hash.len(); let taker_fee_hash_array: [u8; 32] = input.taker_fee_hash.try_into().map_to_mm(|_| { @@ -2104,18 +2173,10 @@ pub fn watcher_validate_taker_fee( ))); } - let address = address_from_raw_pubkey( - &fee_addr, - coin.as_ref().conf.address_prefixes.clone(), - coin.as_ref().conf.checksum_type, - coin.as_ref().conf.bech32_hrp.clone(), - coin.addr_format().clone(), - ) - .map_to_mm(ValidatePaymentError::TxDeserializationError)?; - + let dex_address = dex_address(&coin).map_to_mm(ValidatePaymentError::TxDeserializationError)?; match taker_fee_tx.outputs.get(output_index) { Some(out) => { - let expected_script_pubkey = Builder::build_p2pkh(address.hash()).to_bytes(); + let expected_script_pubkey = Builder::build_p2pkh(dex_address.hash()).to_bytes(); if out.script_pubkey != expected_script_pubkey { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "{}: Provided dex fee tx output script_pubkey doesn't match expected {:?} {:?}", @@ -2137,24 +2198,88 @@ pub fn watcher_validate_taker_fee( Box::new(fut.boxed().compat()) } -pub fn validate_fee( +/// Helper fn to validate taker tx output to dex address +fn validate_dex_output( + coin: &T, + tx: &UtxoTx, + output_index: usize, + dex_address: &Address, + fee_amount: &MmNumber, +) -> MmResult<(), ValidatePaymentError> { + let fee_amount_u64 = sat_from_big_decimal(&fee_amount.to_decimal(), coin.as_ref().decimals)?; + match tx.outputs.get(output_index) { + Some(out) => { + let expected_script_pubkey = Builder::build_p2pkh(dex_address.hash()).to_bytes(); + if out.script_pubkey != expected_script_pubkey { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "{}: Provided dex fee tx output script_pubkey doesn't match expected {:?} {:?}", + INVALID_RECEIVER_ERR_LOG, out.script_pubkey, expected_script_pubkey + ))); + } + if out.value < fee_amount_u64 { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Provided dex fee tx output value is less than expected {:?} {:?}", + out.value, fee_amount_u64 + ))); + } + }, + None => { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Provided dex fee tx {:?} does not have output {}", + tx, output_index + ))) + }, + } + Ok(()) +} + +/// Helper fn to validate taker tx output burning coins +fn validate_burn_output( + coin: &T, + tx: &UtxoTx, + output_index: usize, + burn_script_pubkey: &Script, + burn_amount: &MmNumber, +) -> MmResult<(), ValidatePaymentError> { + let burn_amount_u64 = sat_from_big_decimal(&burn_amount.to_decimal(), coin.as_ref().decimals)?; + match tx.outputs.get(output_index) { + Some(out) => { + if out.script_pubkey != burn_script_pubkey.to_bytes() { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "{}: Provided burn tx output script_pubkey {:?} doesn't match expected {:?}", + INVALID_RECEIVER_ERR_LOG, + out.script_pubkey, + burn_script_pubkey.to_bytes() + ))); + } + + if out.value < burn_amount_u64 { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Provided burn tx output value {:?} is less than expected: {:?}", + out.value, burn_amount + ))); + } + }, + None => { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Provided burn tx {:?} does not have output {}", + tx, output_index + ))) + }, + } + Ok(()) +} + +pub fn validate_fee( coin: T, tx: UtxoTx, output_index: usize, sender_pubkey: &[u8], - dex_amount: &DexFee, + dex_fee: DexFee, min_block_number: u64, - fee_addr: &[u8], ) -> ValidatePaymentFut<()> { - let address = try_f!(address_from_raw_pubkey( - fee_addr, - coin.as_ref().conf.address_prefixes.clone(), - coin.as_ref().conf.checksum_type, - coin.as_ref().conf.bech32_hrp.clone(), - coin.addr_format().clone(), - ) - .map_to_mm(ValidatePaymentError::TxDeserializationError)); - + let dex_address = try_f!(dex_address(&coin).map_to_mm(ValidatePaymentError::InternalError)); + let burn_address = try_f!(burn_address(&coin).map_to_mm(ValidatePaymentError::InternalError)); let inputs_signed_by_pub = try_f!(check_all_utxo_inputs_signed_by_pub(&tx, sender_pubkey)); if !inputs_signed_by_pub { return Box::new(futures01::future::err( @@ -2165,9 +2290,6 @@ pub fn validate_fee( )); } - let fee_amount = try_f!(dex_amount.fee_uamount(coin.as_ref().decimals)); - let burn_amount = try_f!(dex_amount.burn_uamount(coin.as_ref().decimals)); - let fut = async move { let tx_from_rpc = coin .as_ref() @@ -2195,58 +2317,28 @@ pub fn validate_fee( ))); } - match tx.outputs.get(output_index) { - Some(out) => { - let expected_script_pubkey = Builder::build_p2pkh(address.hash()).to_bytes(); - if out.script_pubkey != expected_script_pubkey { - return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "{}: Provided dex fee tx output script_pubkey doesn't match expected {:?} {:?}", - INVALID_RECEIVER_ERR_LOG, out.script_pubkey, expected_script_pubkey - ))); - } - if out.value < fee_amount { - return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "Provided dex fee tx output value is less than expected {:?} {:?}", - out.value, fee_amount - ))); - } - }, - None => { - return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "Provided dex fee tx {:?} does not have output {}", - tx, output_index - ))) + match dex_fee { + DexFee::NoFee => {}, + DexFee::Standard(fee_amount) => { + validate_dex_output(&coin, &tx, output_index, &dex_address, &fee_amount)?; }, - } - - if let Some(burn_amount) = burn_amount { - match tx.outputs.get(output_index + 1) { - Some(out) => { - let expected_script_pubkey = Builder::default().push_opcode(Opcode::OP_RETURN).into_bytes(); - - if out.script_pubkey != expected_script_pubkey { - return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "{}: Provided burn tx output script_pubkey doesn't match expected {:?} {:?}", - INVALID_RECEIVER_ERR_LOG, out.script_pubkey, expected_script_pubkey - ))); - } - - if out.value < burn_amount { - return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "Provided burn tx output value is less than expected {:?} {:?}", - out.value, burn_amount - ))); - } + DexFee::WithBurn { + fee_amount, + burn_amount, + burn_destination, + } => match burn_destination { + DexFeeBurnDestination::KmdOpReturn => { + validate_dex_output(&coin, &tx, output_index, &dex_address, &fee_amount)?; + let burn_script_pubkey = Builder::default().push_opcode(Opcode::OP_RETURN).into_script(); + validate_burn_output(&coin, &tx, output_index + 1, &burn_script_pubkey, &burn_amount)?; }, - None => { - return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "Provided burn tx output {:?} does not have output {}", - tx, output_index - ))) + DexFeeBurnDestination::PreBurnAccount => { + let burn_script_pubkey = Builder::build_p2pkh(burn_address.hash()); + validate_dex_output(&coin, &tx, output_index, &dex_address, &fee_amount)?; + validate_burn_output(&coin, &tx, output_index + 1, &burn_script_pubkey, &burn_amount)?; }, - } - } - + }, + }; Ok(()) }; Box::new(fut.boxed().compat()) @@ -2625,6 +2717,26 @@ pub fn my_address(coin: &T) -> MmResult(coin: &T) -> Result { + address_from_raw_pubkey( + coin.dex_pubkey(), + coin.as_ref().conf.address_prefixes.clone(), + coin.as_ref().conf.checksum_type, + coin.as_ref().conf.bech32_hrp.clone(), + coin.addr_format().clone(), + ) +} + +pub fn burn_address(coin: &T) -> Result { + address_from_raw_pubkey( + coin.burn_pubkey(), + coin.as_ref().conf.address_prefixes.clone(), + coin.as_ref().conf.checksum_type, + coin.as_ref().conf.bech32_hrp.clone(), + coin.addr_format().clone(), + ) +} + /// Hash message for signature using Bitcoin's message signing format. /// sha256(sha256(PREFIX_LENGTH + PREFIX + MESSAGE_LENGTH + MESSAGE)) pub fn sign_message_hash(coin: &UtxoCoinFields, message: &str) -> Option<[u8; 32]> { @@ -2988,6 +3100,8 @@ pub fn min_trading_vol(coin: &UtxoCoinFields) -> MmNumber { pub fn is_asset_chain(coin: &UtxoCoinFields) -> bool { coin.conf.asset_chain } +pub fn should_burn_dex_fee() -> bool { true } + pub async fn get_raw_transaction(coin: &UtxoCoinFields, req: RawTransactionRequest) -> RawTransactionResult { let hash = H256Json::from_str(&req.tx_hash).map_to_mm(|e| RawTransactionError::InvalidHashError(e.to_string()))?; let hex = coin @@ -3995,11 +4109,9 @@ pub async fn get_fee_to_send_taker_fee( stage: FeeApproxStage, ) -> TradePreimageResult where - T: MarketCoinOps + UtxoCommonOps, + T: MarketCoinOps + UtxoCommonOps + SwapOps, { - let decimals = coin.as_ref().decimals; - - let outputs = generate_taker_fee_tx_outputs(decimals, &AddressHashEnum::default_address_hash(), &dex_fee)?; + let outputs = generate_taker_fee_tx_outputs(coin, &dex_fee).map_err(TradePreimageError::InternalError)?; let gas_fee = None; let fee_amount = coin @@ -5210,42 +5322,104 @@ fn test_tx_v_size() { } #[test] -fn test_generate_taker_fee_tx_outputs() { - let amount = BigDecimal::from(6150); - let fee_amount = sat_from_big_decimal(&amount, 8).unwrap(); +fn test_generate_taker_fee_tx_outputs_with_standard_dex_fee() { + let client = UtxoRpcClientEnum::Native(NativeClient(Arc::new(NativeClientImpl::default()))); + let mut fields = utxo_coin_fields_for_test(client, None, false); + fields.conf.ticker = "MYCOIN1".to_owned(); + let coin = utxo_coin_from_fields(fields); + let fee_amount = BigDecimal::from(6150); + let fee_uamount = sat_from_big_decimal(&fee_amount, 8).unwrap(); + + // TODO: replace with error result ('dex fee must contain burn amount') when nodes are upgraded let outputs = generate_taker_fee_tx_outputs( - 8, - &AddressHashEnum::default_address_hash(), - &DexFee::Standard(amount.into()), + &coin, + &DexFee::create_from_fields(fee_amount.into(), 0.into(), "MYCOIN1"), ) .unwrap(); - assert_eq!(outputs.len(), 1); + let dex_address = dex_address(&coin).unwrap(); - assert_eq!(outputs[0].value, fee_amount); + assert_eq!(outputs.len(), 1); + assert_eq!(outputs[0].value, fee_uamount); + assert_eq!( + outputs[0].script_pubkey, + Builder::build_p2pkh(dex_address.hash()).to_bytes() + ); } #[test] -fn test_generate_taker_fee_tx_outputs_with_burn() { +fn test_generate_taker_fee_tx_outputs_with_non_kmd_burn() { + let client = UtxoRpcClientEnum::Native(NativeClient(Arc::new(NativeClientImpl::default()))); + let mut fields = utxo_coin_fields_for_test(client, None, false); + fields.conf.ticker = "MYCOIN1".to_owned(); + let coin = utxo_coin_from_fields(fields); + let fee_amount = BigDecimal::from(6150); let burn_amount = &(&fee_amount / &BigDecimal::from_str("0.75").unwrap()) - &fee_amount; - let fee_uamount = sat_from_big_decimal(&fee_amount, 8).unwrap(); let burn_uamount = sat_from_big_decimal(&burn_amount, 8).unwrap(); let outputs = generate_taker_fee_tx_outputs( - 8, - &AddressHashEnum::default_address_hash(), - &DexFee::with_burn(fee_amount.into(), burn_amount.into()), + &coin, + &DexFee::create_from_fields(fee_amount.into(), burn_amount.into(), "MYCOIN1"), ) .unwrap(); - assert_eq!(outputs.len(), 2); + let dex_address = dex_address(&coin).unwrap(); + let burn_address = burn_address(&coin).unwrap(); + assert_eq!(outputs.len(), 2); assert_eq!(outputs[0].value, fee_uamount); + assert_eq!( + outputs[0].script_pubkey, + Builder::build_p2pkh(dex_address.hash()).to_bytes() + ); + assert_eq!(outputs[1].value, burn_uamount); + assert_eq!( + outputs[1].script_pubkey, + Builder::build_p2pkh(burn_address.hash()).to_bytes() + ); +} +#[test] +fn test_generate_taker_fee_tx_outputs_with_kmd_burn() { + let client = UtxoRpcClientEnum::Native(NativeClient(Arc::new(NativeClientImpl::default()))); + let mut fields = utxo_coin_fields_for_test(client, None, false); + fields.conf.ticker = "KMD".to_owned(); + let coin = utxo_coin_from_fields(fields); + + let fee_amount = BigDecimal::from(6150); + let burn_amount = &(&fee_amount / &BigDecimal::from_str("0.75").unwrap()) - &fee_amount; + let fee_uamount = sat_from_big_decimal(&fee_amount, 8).unwrap(); + let burn_uamount = sat_from_big_decimal(&burn_amount, 8).unwrap(); + + let outputs = generate_taker_fee_tx_outputs( + &coin, + &DexFee::create_from_fields(fee_amount.into(), burn_amount.into(), "KMD"), + ) + .unwrap(); + + let dex_address = address_from_raw_pubkey( + coin.dex_pubkey(), + coin.as_ref().conf.address_prefixes.clone(), + coin.as_ref().conf.checksum_type, + coin.as_ref().conf.bech32_hrp.clone(), + coin.addr_format().clone(), + ) + .unwrap(); + + assert_eq!(outputs.len(), 2); + assert_eq!(outputs[0].value, fee_uamount); + assert_eq!( + outputs[0].script_pubkey, + Builder::build_p2pkh(dex_address.hash()).to_bytes() + ); assert_eq!(outputs[1].value, burn_uamount); + assert_eq!( + outputs[1].script_pubkey, + Builder::default().push_opcode(Opcode::OP_RETURN).into_bytes() + ); } #[test] diff --git a/mm2src/coins/utxo/utxo_common/utxo_tx_history_v2_common.rs b/mm2src/coins/utxo/utxo_common/utxo_tx_history_v2_common.rs index 32577de810..cd37636e79 100644 --- a/mm2src/coins/utxo/utxo_common/utxo_tx_history_v2_common.rs +++ b/mm2src/coins/utxo/utxo_common/utxo_tx_history_v2_common.rs @@ -51,10 +51,6 @@ where (DerivationMethod::HDWallet(hd_wallet), MyTxHistoryTarget::AddressId(hd_address_id)) => { get_tx_history_filters_for_hd_address(coin, hd_wallet, hd_address_id).await }, - (DerivationMethod::HDWallet(hd_wallet), MyTxHistoryTarget::AddressDerivationPath(derivation_path)) => { - let hd_address_id = HDPathAccountToAddressId::from(derivation_path); - get_tx_history_filters_for_hd_address(coin, hd_wallet, hd_address_id).await - }, (DerivationMethod::HDWallet(_), target) => MmError::err(MyTxHistoryErrorV2::with_expected_target( target, "an HD account/address", diff --git a/mm2src/coins/utxo/utxo_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index 401be98080..17dd2c9af8 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -44,6 +44,7 @@ use common::executor::{AbortableSystem, AbortedError}; use futures::{FutureExt, TryFutureExt}; use mm2_metrics::MetricsArc; use mm2_number::MmNumber; +#[cfg(test)] use mocktopus::macros::*; use script::Opcode; use utxo_signer::UtxoSignerOps; @@ -301,18 +302,11 @@ impl UtxoStandardOps for UtxoStandardCoin { } #[async_trait] +#[cfg_attr(test, mockable)] impl SwapOps for UtxoStandardCoin { #[inline] - async fn send_taker_fee( - &self, - fee_addr: &[u8], - dex_fee: DexFee, - _uuid: &[u8], - _expire_at: u64, - ) -> TransactionResult { - utxo_common::send_taker_fee(self.clone(), fee_addr, dex_fee) - .compat() - .await + async fn send_taker_fee(&self, dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionResult { + utxo_common::send_taker_fee(self.clone(), dex_fee).compat().await } #[inline] @@ -370,9 +364,8 @@ impl SwapOps for UtxoStandardCoin { tx, utxo_common::DEFAULT_FEE_VOUT, validate_fee_args.expected_sender, - validate_fee_args.dex_fee, + validate_fee_args.dex_fee.clone(), validate_fee_args.min_block_number, - validate_fee_args.fee_addr, ) .compat() .await @@ -894,6 +887,12 @@ impl CommonSwapOpsV2 for UtxoStandardCoin { fn derive_htlc_pubkey_v2_bytes(&self, swap_unique_data: &[u8]) -> Vec { self.derive_htlc_pubkey_v2(swap_unique_data).to_bytes() } + + #[inline(always)] + fn taker_pubkey_bytes(&self) -> Option> { + let dummy_unique_data = []; // not used for non-private coins + Some(self.derive_htlc_pubkey_v2(&dummy_unique_data).to_bytes()) + } } #[async_trait] @@ -970,6 +969,10 @@ impl MarketCoinOps for UtxoStandardCoin { fn min_trading_vol(&self) -> MmNumber { utxo_common::min_trading_vol(self.as_ref()) } + fn is_kmd(&self) -> bool { &self.utxo_arc.conf.ticker == "KMD" } + + fn should_burn_dex_fee(&self) -> bool { utxo_common::should_burn_dex_fee() } + fn is_trezor(&self) -> bool { self.as_ref().priv_key_policy.is_trezor() } } diff --git a/mm2src/coins/utxo/utxo_tests.rs b/mm2src/coins/utxo/utxo_tests.rs index 188ce519d4..eaaad68edf 100644 --- a/mm2src/coins/utxo/utxo_tests.rs +++ b/mm2src/coins/utxo/utxo_tests.rs @@ -45,6 +45,7 @@ use keys::prefixes::*; use mm2_core::mm_ctx::MmCtxBuilder; use mm2_event_stream::StreamingManager; use mm2_number::bigdecimal::{BigDecimal, Signed}; +use mm2_number::MmNumber; use mm2_test_helpers::electrums::doc_electrums; use mm2_test_helpers::for_tests::{electrum_servers_rpc, mm_ctx_with_custom_db, DOC_ELECTRUM_ADDRS, MARTY_ELECTRUM_ADDRS, T_BCH_ELECTRUMS}; @@ -2652,6 +2653,28 @@ fn test_get_sender_trade_fee_dynamic_tx_fee() { assert_eq!(fee1, fee3); } +// validate an old tx with no output with the burn account +// TODO: remove when we disable such old style txns +#[test] +fn test_validate_old_fee_tx() { + let rpc_client = electrum_client_for_test(MARTY_ELECTRUM_ADDRS); + let coin = utxo_coin_for_test(UtxoRpcClientEnum::Electrum(rpc_client), None, false); + let tx_bytes = hex::decode("0400008085202f8901033aedb3c3c02fc76c15b393c7b1f638cfa6b4a1d502e00d57ad5b5305f12221000000006a473044022074879aabf38ef943eba7e4ce54c444d2d6aa93ac3e60ea1d7d288d7f17231c5002205e1671a62d8c031ac15e0e8456357e54865b7acbf49c7ebcba78058fd886b4bd012103242d9cb2168968d785f6914c494c303ff1c27ba0ad882dbc3c15cfa773ea953cffffffff0210270000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ac4802d913000000001976a914902053231ef0541a7628c11acac40d30f2a127bd88ac008e3765000000000000000000000000000000").unwrap(); + let taker_fee_tx = coin.tx_enum_from_bytes(&tx_bytes).unwrap(); + let amount: MmNumber = "0.0001".parse::().unwrap().into(); + let dex_fee = DexFee::Standard(amount); + let validate_fee_args = ValidateFeeArgs { + fee_tx: &taker_fee_tx, + expected_sender: &hex::decode("03242d9cb2168968d785f6914c494c303ff1c27ba0ad882dbc3c15cfa773ea953c").unwrap(), + dex_fee: &dex_fee, + min_block_number: 0, + uuid: &[], + }; + let result = block_on(coin.validate_fee(validate_fee_args)); + log!("result: {:?}", result); + assert!(result.is_ok()); +} + #[test] fn test_validate_fee_wrong_sender() { let rpc_client = electrum_client_for_test(MARTY_ELECTRUM_ADDRS); @@ -2663,7 +2686,6 @@ fn test_validate_fee_wrong_sender() { let validate_fee_args = ValidateFeeArgs { fee_tx: &taker_fee_tx, expected_sender: &DEX_FEE_ADDR_RAW_PUBKEY, - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &DexFee::Standard(amount.into()), min_block_number: 0, uuid: &[], @@ -2688,7 +2710,6 @@ fn test_validate_fee_min_block() { let validate_fee_args = ValidateFeeArgs { fee_tx: &taker_fee_tx, expected_sender: &sender_pub, - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &DexFee::Standard(amount.into()), min_block_number: 278455, uuid: &[], @@ -2717,7 +2738,6 @@ fn test_validate_fee_bch_70_bytes_signature() { let validate_fee_args = ValidateFeeArgs { fee_tx: &taker_fee_tx, expected_sender: &sender_pub, - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &DexFee::Standard(amount.into()), min_block_number: 0, uuid: &[], diff --git a/mm2src/coins/z_coin.rs b/mm2src/coins/z_coin.rs index b2b9f34e72..7a492cfd6f 100644 --- a/mm2src/coins/z_coin.rs +++ b/mm2src/coins/z_coin.rs @@ -88,7 +88,7 @@ use zcash_primitives::memo::MemoBytes; use zcash_primitives::sapling::keys::OutgoingViewingKey; use zcash_primitives::sapling::note_encryption::try_sapling_output_recovery; use zcash_primitives::transaction::builder::Builder as ZTxBuilder; -use zcash_primitives::transaction::components::{Amount, TxOut}; +use zcash_primitives::transaction::components::{Amount, OutputDescription, TxOut}; use zcash_primitives::transaction::Transaction as ZTransaction; use zcash_primitives::zip32::ChildIndex as Zip32Child; use zcash_primitives::{constants::mainnet as z_mainnet_constants, sapling::PaymentAddress, @@ -135,6 +135,7 @@ macro_rules! try_ztx_s { const DEX_FEE_OVK: OutgoingViewingKey = OutgoingViewingKey([7; 32]); const DEX_FEE_Z_ADDR: &str = "zs1rp6426e9r6jkq2nsanl66tkd34enewrmr0uvj0zelhkcwmsy0uvxz2fhm9eu9rl3ukxvgzy2v9f"; +const DEX_BURN_Z_ADDR: &str = "zs1hq65fswcur3uxe385cxxgynf37qz4jpfcj52sj9ndvfhc569qwd39alfv9k82e0zftp3xc2jfgj"; // TODO: fix to actual burn z address cfg_native!( const SAPLING_OUTPUT_NAME: &str = "sapling-output.params"; const SAPLING_SPEND_NAME: &str = "sapling-spend.params"; @@ -205,6 +206,7 @@ impl Parameters for ZcoinConsensusParams { #[allow(unused)] pub struct ZCoinFields { dex_fee_addr: PaymentAddress, + dex_burn_addr: PaymentAddress, my_z_addr: PaymentAddress, my_z_addr_encoded: String, z_spending_key: ExtendedSpendingKey, @@ -667,6 +669,33 @@ impl ZCoin { paging_options: request.paging_options, }) } + + /// Validates dex fee output or burn output + /// Returns true if the output valid or error if not valid. Returns false if could not decrypt output (some other output) + fn validate_dex_fee_output( + &self, + shielded_out: &OutputDescription, + ovk: &OutgoingViewingKey, + expected_address: &PaymentAddress, + block_height: BlockHeight, + amount_sat: u64, + expected_memo: &MemoBytes, + ) -> Result { + if let Some((note, address, memo)) = + try_sapling_output_recovery(self.consensus_params_ref(), block_height, ovk, shielded_out) + { + if &address == expected_address { + if note.value != amount_sat { + return Err(format!("invalid amount {}, expected {}", note.value, amount_sat)); + } + if &memo != expected_memo { + return Err(format!("invalid memo {:?}, expected {:?}", memo, expected_memo)); + } + return Ok(true); + } + } + Ok(false) + } } impl AsRef for ZCoin { @@ -850,6 +879,7 @@ pub struct ZCoinBuilder<'a> { z_coin_params: &'a ZcoinActivationParams, utxo_params: UtxoActivationParams, priv_key_policy: PrivKeyBuildPolicy, + #[cfg_attr(target_arch = "wasm32", allow(unused))] db_dir_path: PathBuf, /// `Some` if `ZCoin` should be initialized with a forced spending key. z_spending_key: Option, @@ -905,6 +935,13 @@ impl<'a> UtxoCoinBuilder for ZCoinBuilder<'a> { .expect("DEX_FEE_Z_ADDR is a valid z-address") .expect("DEX_FEE_Z_ADDR is a valid z-address"); + let dex_burn_addr = decode_payment_address( + self.protocol_info.consensus_params.hrp_sapling_payment_address(), + DEX_BURN_Z_ADDR, + ) + .expect("DEX_BURN_Z_ADDR is a valid z-address") + .expect("DEX_BURN_Z_ADDR is a valid z-address"); + let z_tx_prover = self.z_tx_prover().await?; let my_z_addr_encoded = encode_payment_address( self.protocol_info.consensus_params.hrp_sapling_payment_address(), @@ -938,6 +975,7 @@ impl<'a> UtxoCoinBuilder for ZCoinBuilder<'a> { let z_fields = Arc::new(ZCoinFields { dex_fee_addr, + dex_burn_addr, my_z_addr, my_z_addr_encoded, evk: ExtendedFullViewingKey::from(&z_spending_key), @@ -1062,6 +1100,8 @@ impl<'a> ZCoinBuilder<'a> { } /// Initialize `ZCoin` with a forced `z_spending_key`. +/// db_dir_path is where ZOMBIE_wallet.db located +/// Note that ZOMBIE_cache.db (db where blocks are downloaded to create ZOMBIE_wallet.db) is created in-memory (see BlockDbImpl::new fn) #[cfg(all(test, feature = "zhtlc-native-tests"))] #[allow(clippy::too_many_arguments)] async fn z_coin_from_conf_and_params_with_z_key( @@ -1084,6 +1124,9 @@ async fn z_coin_from_conf_and_params_with_z_key( Some(z_spending_key), protocol_info, ); + + println!("ZOMBIE_wallet.db will be synch'ed with the chain, this may take a while for the first time."); + println!("You may also run prepare_zombie_sapling_cache test to update ZOMBIE_wallet.db before running tests."); builder.build().await } @@ -1203,20 +1246,16 @@ impl MarketCoinOps for ZCoin { fn is_privacy(&self) -> bool { true } + fn should_burn_dex_fee(&self) -> bool { false } // TODO: enable when burn z_address fixed + fn is_trezor(&self) -> bool { self.as_ref().priv_key_policy.is_trezor() } } #[async_trait] impl SwapOps for ZCoin { - async fn send_taker_fee( - &self, - _fee_addr: &[u8], - dex_fee: DexFee, - uuid: &[u8], - _expire_at: u64, - ) -> TransactionResult { + async fn send_taker_fee(&self, dex_fee: DexFee, uuid: &[u8], _expire_at: u64) -> TransactionResult { let uuid = uuid.to_owned(); - let tx = try_tx_s!(z_send_dex_fee(self, dex_fee.fee_amount().into(), &uuid).await); + let tx = try_tx_s!(z_send_dex_fee(self, dex_fee, &uuid).await); Ok(tx.into()) } @@ -1371,6 +1410,8 @@ impl SwapOps for ZCoin { Ok(tx.into()) } + /// Currently validates both Standard and WithBurn options for DexFee + /// TODO: when all mm2 nodes upgrade to support the burn account then disable validation of the Standard option async fn validate_fee(&self, validate_fee_args: ValidateFeeArgs<'_>) -> ValidatePaymentResult<()> { let z_tx = match validate_fee_args.fee_tx { TransactionEnum::ZTransaction(t) => t.clone(), @@ -1381,7 +1422,8 @@ impl SwapOps for ZCoin { ))) }, }; - let amount_sat = validate_fee_args.dex_fee.fee_uamount(self.utxo_arc.decimals)?; + let fee_amount_sat = validate_fee_args.dex_fee.fee_amount_as_u64(self.utxo_arc.decimals)?; + let burn_amount_sat = validate_fee_args.dex_fee.burn_amount_as_u64(self.utxo_arc.decimals)?; let expected_memo = MemoBytes::from_bytes(validate_fee_args.uuid).expect("Uuid length < 512"); let tx_hash = H256::from(z_tx.txid().0).reversed(); @@ -1415,40 +1457,53 @@ impl SwapOps for ZCoin { None => H0, }; + let mut fee_output_valid = false; + let mut burn_output_valid = false; for shielded_out in z_tx.shielded_outputs.iter() { - if let Some((note, address, memo)) = - try_sapling_output_recovery(self.consensus_params_ref(), block_height, &DEX_FEE_OVK, shielded_out) + if self + .validate_dex_fee_output( + shielded_out, + &DEX_FEE_OVK, + &self.z_fields.dex_fee_addr, + block_height, + fee_amount_sat, + &expected_memo, + ) + .map_err(|err| { + MmError::new(ValidatePaymentError::WrongPaymentTx(format!( + "Bad dex fee output: {}", + err + ))) + })? { - if address != self.z_fields.dex_fee_addr { - let encoded = encode_payment_address(z_mainnet_constants::HRP_SAPLING_PAYMENT_ADDRESS, &address); - let expected = encode_payment_address( - z_mainnet_constants::HRP_SAPLING_PAYMENT_ADDRESS, - &self.z_fields.dex_fee_addr, - ); - return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "Dex fee was sent to the invalid address {}, expected {}", - encoded, expected - ))); - } - - if note.value != amount_sat { - return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "Dex fee has invalid amount {}, expected {}", - note.value, amount_sat - ))); - } - - if memo != expected_memo { - return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "Dex fee has invalid memo {:?}, expected {:?}", - memo, expected_memo - ))); + fee_output_valid = true; + } + if let Some(burn_amount_sat) = burn_amount_sat { + if self + .validate_dex_fee_output( + shielded_out, + &DEX_FEE_OVK, + &self.z_fields.dex_burn_addr, + block_height, + burn_amount_sat, + &expected_memo, + ) + .map_err(|err| { + MmError::new(ValidatePaymentError::WrongPaymentTx(format!( + "Bad burn output: {}", + err + ))) + })? + { + burn_output_valid = true; } - - return Ok(()); } } + if fee_output_valid && (burn_amount_sat.is_none() || burn_output_valid) { + return Ok(()); + } + MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "The dex fee tx {:?} has no shielded outputs or outputs decryption failed", z_tx diff --git a/mm2src/coins/z_coin/tx_streaming_tests/native.rs b/mm2src/coins/z_coin/tx_streaming_tests/native.rs index cc5ecc5812..b2b9983661 100644 --- a/mm2src/coins/z_coin/tx_streaming_tests/native.rs +++ b/mm2src/coins/z_coin/tx_streaming_tests/native.rs @@ -8,7 +8,7 @@ use super::light_zcoin_activation_params; use crate::z_coin::tx_history_events::ZCoinTxHistoryEventStreamer; use crate::z_coin::z_coin_from_conf_and_params; use crate::z_coin::z_htlc::z_send_dex_fee; -use crate::{CoinProtocol, MarketCoinOps, MmCoin, PrivKeyBuildPolicy}; +use crate::{CoinProtocol, DexFee, MarketCoinOps, MmCoin, PrivKeyBuildPolicy}; #[test] #[ignore] // Ignored because we don't have zcash params in CI. TODO: Why not download them on demand like how we do in wasm (see download_and_save_params). @@ -52,7 +52,7 @@ fn test_zcoin_tx_streaming() { block_on(ctx.event_stream_manager.add(client_id, streamer, coin.spawner())).unwrap(); // Send a tx to have it in the tx history. - let tx = block_on(z_send_dex_fee(&coin, "0.0001".parse().unwrap(), &[1; 16])).unwrap(); + let tx = block_on(z_send_dex_fee(&coin, DexFee::Standard("0.0001".into()), &[1; 16])).unwrap(); // Wait for the tx history event (should be streamed next block). let event = block_on(Box::pin(event_receiver.recv()).timeout_secs(120.)) diff --git a/mm2src/coins/z_coin/tx_streaming_tests/wasm.rs b/mm2src/coins/z_coin/tx_streaming_tests/wasm.rs index 192db3c7a9..a983371a7f 100644 --- a/mm2src/coins/z_coin/tx_streaming_tests/wasm.rs +++ b/mm2src/coins/z_coin/tx_streaming_tests/wasm.rs @@ -9,7 +9,7 @@ use crate::z_coin::tx_history_events::ZCoinTxHistoryEventStreamer; use crate::z_coin::z_coin_from_conf_and_params; use crate::z_coin::z_htlc::z_send_dex_fee; use crate::PrivKeyBuildPolicy; -use crate::{CoinProtocol, MarketCoinOps, MmCoin}; +use crate::{CoinProtocol, DexFee, MarketCoinOps, MmCoin}; #[wasm_bindgen_test] async fn test_zcoin_tx_streaming() { @@ -49,7 +49,7 @@ async fn test_zcoin_tx_streaming() { .unwrap(); // Send a tx to have it in the tx history. - let tx = z_send_dex_fee(&coin, "0.0001".parse().unwrap(), &[1; 16]) + let tx = z_send_dex_fee(&coin, DexFee::Standard("0.0001".into()), &[1; 16]) .await .unwrap(); diff --git a/mm2src/coins/z_coin/z_coin_native_tests.rs b/mm2src/coins/z_coin/z_coin_native_tests.rs index 0cde681ee8..4e5ffc4325 100644 --- a/mm2src/coins/z_coin/z_coin_native_tests.rs +++ b/mm2src/coins/z_coin/z_coin_native_tests.rs @@ -1,3 +1,27 @@ +//! Native tests for zcoin +//! +//! To run zcoin tests in this source you need `--features zhtlc-native-tests` +//! ZOMBIE chain must be running for zcoin tests: +//! komodod -ac_name=ZOMBIE -ac_supply=0 -ac_reward=25600000000 -ac_halving=388885 -ac_private=1 -ac_sapling=1 -testnode=1 -addnode=65.21.51.116 -addnode=116.203.120.163 -addnode=168.119.236.239 -addnode=65.109.1.121 -addnode=159.69.125.84 -addnode=159.69.10.44 +//! Also check the test z_key (spending key) has balance: +//! `komodo-cli -ac_name=ZOMBIE z_getbalance zs10hvyxf3ajm82e4gvxem3zjlf9xf3yxhjww9fvz3mfqza9zwumvluzy735e29c3x5aj2nu0ua6n0` +//! If no balance, you may mine some transparent coins and send to the test z_key. +//! When tests are run for the first time (or have not been run for a long) synching to fill ZOMBIE_wallet.db is started which may take hours. +//! So it is recommended to run prepare_zombie_sapling_cache to sync ZOMBIE_wallet.db before running zcoin tests: +//! cargo test -p coins --features zhtlc-native-tests -- --nocapture prepare_zombie_sapling_cache +//! If you did not run prepare_zombie_sapling_cache waiting for ZOMBIE_wallet.db sync will be done in the first call to ZCoin::gen_tx. +//! In tests, for ZOMBIE_wallet.db to be filled, another database ZOMBIE_cache.db is created in memory, +//! so if db sync in tests is cancelled and restarted this would cause restarting of building ZOMBIE_cache.db in memory +//! +//! Note that during the ZOMBIE_wallet.db sync an error may be reported: +//! 'error trying to connect: tcp connect error: Can't assign requested address (os error 49)'. +//! Also during the sync other apps like ssh or komodo-cli may return same error or even crash. TODO: fix this problem, maybe it is due to too much load on TCP stack +//! Errors like `No one seems interested in SyncStatus: send failed because channel is full` in the debug log may be ignored (means that update status is temporarily not watched) +//! +//! To monitor sync status in logs you may add logging support into the beginning of prepare_zombie_sapling_cache test (or other tests): +//! common::log::UnifiedLoggerBuilder::default().init(); +//! and run cargo test with var RUST_LOG=debug + use bitcrypto::dhash160; use common::{block_on, now_sec}; use mm2_core::mm_ctx::MmCtxBuilder; @@ -9,8 +33,8 @@ use zcash_client_backend::encoding::decode_extended_spending_key; use super::{z_coin_from_conf_and_params_with_z_key, z_mainnet_constants, PrivKeyBuildPolicy, RefundPaymentArgs, SendPaymentArgs, SpendPaymentArgs, SwapOps, ValidateFeeArgs, ValidatePaymentError, ZTransaction}; use crate::z_coin::{z_htlc::z_send_dex_fee, ZcoinActivationParams, ZcoinRpcMode}; -use crate::DexFee; use crate::{CoinProtocol, SwapTxTypeWithSecretHash}; +use crate::{DexFee, DexFeeBurnDestination}; use mm2_number::MmNumber; fn native_zcoin_activation_params() -> ZcoinActivationParams { @@ -20,8 +44,8 @@ fn native_zcoin_activation_params() -> ZcoinActivationParams { } } -#[test] -fn zombie_coin_send_and_refund_maker_payment() { +#[tokio::test(flavor = "multi_thread")] +async fn zombie_coin_send_and_refund_maker_payment() { let ctx = MmCtxBuilder::default().into_mm_arc(); let mut conf = zombie_conf(); let params = native_zcoin_activation_params(); @@ -33,7 +57,7 @@ fn zombie_coin_send_and_refund_maker_payment() { other_protocol => panic!("Failed to get protocol from config: {:?}", other_protocol), }; - let coin = block_on(z_coin_from_conf_and_params_with_z_key( + let coin = z_coin_from_conf_and_params_with_z_key( &ctx, "ZOMBIE", &conf, @@ -42,11 +66,17 @@ fn zombie_coin_send_and_refund_maker_payment() { db_dir, z_key, protocol_info, - )) + ) + .await .unwrap(); let time_lock = now_sec() - 3600; - let taker_pub = coin.utxo_arc.priv_key_policy.activated_key_or_err().unwrap().public(); + let maker_uniq_data = [3; 32]; + + let taker_uniq_data = [5; 32]; + let taker_key_pair = coin.derive_htlc_key_pair(taker_uniq_data.as_slice()); + let taker_pub = taker_key_pair.public(); + let secret_hash = [0; 20]; let args = SendPaymentArgs { @@ -56,12 +86,12 @@ fn zombie_coin_send_and_refund_maker_payment() { secret_hash: &secret_hash, amount: "0.01".parse().unwrap(), swap_contract_address: &None, - swap_unique_data: &[], + swap_unique_data: maker_uniq_data.as_slice(), payment_instructions: &None, watcher_reward: None, wait_for_confirmation_until: 0, }; - let tx = block_on(coin.send_maker_payment(args)).unwrap(); + let tx = coin.send_maker_payment(args).await.unwrap(); log!("swap tx {}", hex::encode(tx.tx_hash_as_bytes().0)); let refund_args = RefundPaymentArgs { @@ -72,15 +102,15 @@ fn zombie_coin_send_and_refund_maker_payment() { maker_secret_hash: &secret_hash, }, swap_contract_address: &None, - swap_unique_data: pk_data.as_slice(), + swap_unique_data: maker_uniq_data.as_slice(), watcher_reward: false, }; - let refund_tx = block_on(coin.send_maker_refunds_payment(refund_args)).unwrap(); + let refund_tx = coin.send_maker_refunds_payment(refund_args).await.unwrap(); log!("refund tx {}", hex::encode(refund_tx.tx_hash_as_bytes().0)); } -#[test] -fn zombie_coin_send_and_spend_maker_payment() { +#[tokio::test(flavor = "multi_thread")] +async fn zombie_coin_send_and_spend_maker_payment() { let ctx = MmCtxBuilder::default().into_mm_arc(); let mut conf = zombie_conf(); let params = native_zcoin_activation_params(); @@ -92,7 +122,7 @@ fn zombie_coin_send_and_spend_maker_payment() { other_protocol => panic!("Failed to get protocol from config: {:?}", other_protocol), }; - let coin = block_on(z_coin_from_conf_and_params_with_z_key( + let coin = z_coin_from_conf_and_params_with_z_key( &ctx, "ZOMBIE", &conf, @@ -101,11 +131,20 @@ fn zombie_coin_send_and_spend_maker_payment() { db_dir, z_key, protocol_info, - )) + ) + .await .unwrap(); let lock_time = now_sec() - 1000; - let taker_pub = coin.utxo_arc.priv_key_policy.activated_key_or_err().unwrap().public(); + + let maker_uniq_data = [3; 32]; + let maker_key_pair = coin.derive_htlc_key_pair(maker_uniq_data.as_slice()); + let maker_pub = maker_key_pair.public(); + + let taker_uniq_data = [5; 32]; + let taker_key_pair = coin.derive_htlc_key_pair(taker_uniq_data.as_slice()); + let taker_pub = taker_key_pair.public(); + let secret = [0; 32]; let secret_hash = dhash160(&secret); @@ -116,33 +155,31 @@ fn zombie_coin_send_and_spend_maker_payment() { secret_hash: secret_hash.as_slice(), amount: "0.01".parse().unwrap(), swap_contract_address: &None, - swap_unique_data: &[], + swap_unique_data: maker_uniq_data.as_slice(), payment_instructions: &None, watcher_reward: None, wait_for_confirmation_until: 0, }; - let tx = block_on(coin.send_maker_payment(maker_payment_args)).unwrap(); + let tx = coin.send_maker_payment(maker_payment_args).await.unwrap(); log!("swap tx {}", hex::encode(tx.tx_hash_as_bytes().0)); - let maker_pub = taker_pub; - let spends_payment_args = SpendPaymentArgs { other_payment_tx: &tx.tx_hex(), time_lock: lock_time, other_pubkey: maker_pub, secret: &secret, - secret_hash: &[], + secret_hash: secret_hash.as_slice(), swap_contract_address: &None, - swap_unique_data: pk_data.as_slice(), + swap_unique_data: taker_uniq_data.as_slice(), watcher_reward: false, }; - let spend_tx = block_on(coin.send_taker_spends_maker_payment(spends_payment_args)).unwrap(); + let spend_tx = coin.send_taker_spends_maker_payment(spends_payment_args).await.unwrap(); log!("spend tx {}", hex::encode(spend_tx.tx_hash_as_bytes().0)); } -#[test] -fn zombie_coin_send_dex_fee() { +#[tokio::test(flavor = "multi_thread")] +async fn zombie_coin_send_dex_fee() { let ctx = MmCtxBuilder::default().into_mm_arc(); let mut conf = zombie_conf(); let params = native_zcoin_activation_params(); @@ -154,22 +191,44 @@ fn zombie_coin_send_dex_fee() { other_protocol => panic!("Failed to get protocol from config: {:?}", other_protocol), }; - let coin = block_on(z_coin_from_conf_and_params_with_z_key( - &ctx, - "ZOMBIE", - &conf, - ¶ms, - priv_key, - db_dir, - z_key, - protocol_info, - )) - .unwrap(); + let coin = + z_coin_from_conf_and_params_with_z_key(&ctx, "ZOMBIE", &conf, ¶ms, priv_key, db_dir, z_key, protocol_info) + .await + .unwrap(); + + let dex_fee = DexFee::WithBurn { + fee_amount: "0.0075".into(), + burn_amount: "0.0025".into(), + burn_destination: DexFeeBurnDestination::PreBurnAccount, + }; + let tx = z_send_dex_fee(&coin, dex_fee, &[1; 16]).await.unwrap(); + log!("dex fee tx {}", tx.txid()); +} + +#[tokio::test(flavor = "multi_thread")] +async fn zombie_coin_send_standard_dex_fee() { + let ctx = MmCtxBuilder::default().into_mm_arc(); + let mut conf = zombie_conf(); + let params = native_zcoin_activation_params(); + let priv_key = PrivKeyBuildPolicy::IguanaPrivKey([1; 32].into()); + let db_dir = PathBuf::from("./for_tests"); + let z_key = decode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, "secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").unwrap().unwrap(); + let protocol_info = match serde_json::from_value::(conf["protocol"].take()).unwrap() { + CoinProtocol::ZHTLC(protocol_info) => protocol_info, + other_protocol => panic!("Failed to get protocol from config: {:?}", other_protocol), + }; - let tx = block_on(z_send_dex_fee(&coin, "0.01".parse().unwrap(), &[1; 16])).unwrap(); + let coin = + z_coin_from_conf_and_params_with_z_key(&ctx, "ZOMBIE", &conf, ¶ms, priv_key, db_dir, z_key, protocol_info) + .await + .unwrap(); + + let dex_fee = DexFee::Standard("0.01".into()); + let tx = z_send_dex_fee(&coin, dex_fee, &[1; 16]).await.unwrap(); log!("dex fee tx {}", tx.txid()); } +/// Use to create ZOMBIE_wallet.db #[test] fn prepare_zombie_sapling_cache() { let ctx = MmCtxBuilder::default().into_mm_arc(); @@ -200,8 +259,8 @@ fn prepare_zombie_sapling_cache() { } } -#[test] -fn zombie_coin_validate_dex_fee() { +#[tokio::test(flavor = "multi_thread")] +async fn zombie_coin_validate_dex_fee() { let ctx = MmCtxBuilder::default().into_mm_arc(); let mut conf = zombie_conf(); let params = native_zcoin_activation_params(); @@ -213,36 +272,34 @@ fn zombie_coin_validate_dex_fee() { other_protocol => panic!("Failed to get protocol from config: {:?}", other_protocol), }; - let coin = block_on(z_coin_from_conf_and_params_with_z_key( - &ctx, - "ZOMBIE", - &conf, - ¶ms, - priv_key, - db_dir, - z_key, - protocol_info, - )) - .unwrap(); + let coin = + z_coin_from_conf_and_params_with_z_key(&ctx, "ZOMBIE", &conf, ¶ms, priv_key, db_dir, z_key, protocol_info) + .await + .unwrap(); - // https://zombie.explorer.lordofthechains.com/tx/ec620194c33eba004904f34c93f4f005a7544988771af1c5a527f65c08e4a4aa - let tx_hex = "0400008085202f89000000000000af330000e803000000000000015c3fc69c0eb25dc2b75593464af5b937da35816a2ffeb9b79f3da865c2187083a0b143011810109ab0ed410896aff77bcfbc8a8f5b9bfe0d273716095cfe401cbd97c66a999384aa12a571abc39508b113de0ad0816630fea67f18d68572c52be4364f812f9796e1084ee6c28d1419dac4767d12a7a33662536c2c1ffa7e221d843c9f2bf2601f34cc71a1e1c42041fab87e617ae00b796aa070280060e9cdc30e69e80367e6105e792bbefcd93f00c48ce8278c4eb36c8846cb94d5adcb273ce91decf79196461f7969d6a7031878c6c8e81edd4532a5c57bbaeeea4ed5f4440cef90f19020079c69e05325e63350e9cb9eac44a3d4937111a3c6dc00c79d4dfe72c1e73a6e00ad0aa1aded83f0b778ab92319fcdae19c2946c50c370d243fe6dfa4f92803dcec1992af0d91f0cda8ccbee2a5321f708fc0156d29b51a015b3fb70f543c7713b8547d24e6916caefca17edf1f4109099177498cb30f9305b5169ab1f2e3e4a83e789b5687f3f5f5013d917e2e6babc8ca4507cb349d1e5a30602f557bcbd6574c7fcb5779ce286bdd10fe5db58abadcacf5eaa9e5d3575e30e439d0c62494bc045456e7b6b03f5304a8ff8878f01883f8c473e066f8159bdc111a03d96670f4b29acd919d8b9674897e056c7ac6ef4da155ce7d923f2bedcd51f2198c2be360e03ef2373df94d1e63ba507effc2f9b2f1ccfed09f2f26b8c619415d4a90f556e4b9350099f58fb10a33986945a1512879fdae66e9ef94671764ecdc558ed2d760f7bd3ce2dedfdb4fc7e3aa26903288e16f34214632d8727f68d47389ff687f681b3b285896d3214b9eb60271d87f3223f20e4ddf39513c07fe3420eefa9e7372fff51c83468161d9ffe745533b02917e4ccf87a213c884042938511bb7ccbe6b54392897b1ba111d127ec2c16ba167bb5a65d7819295ceedc5b8faf493c71ed722b72578c62be7d59449bd218196e1f43c3a8bb4875c3bcce1adcb6c4afa6398a7276583c60dbe609c9819bf66385e6cff4b27090aa1dccd0a2f86ca3b3871f2077db44c17d57bba98f9809e6000676600ad70560cbf285354f979d24a5de6e8b0c65ee1a89e28f58f430d20988cae8b0a9690cf79519efc227d54ca739ce3dcde73ac6e624c00b120d6955b40b854b00b1b53dc18cc35cd4792716f3e0bc6552bf0ba4616d1b22900cebede31fbe4b722de1f11c0577abe2ca0614c9d6f24cb56e2b4c840b8573c503ca1d4bf9e671a583b04dd51af10cfc709e89965c5150d7fb6b8c924812e6c9d31025d30e8367defb39e71fda095a16c0e1a70b528799d8c4852b3adb700b113bf5de1d6ec6c7742a1ef678228930ec767e406b36a55fe4a8108236cf0487901e35b50312facad257fd9ba2be154fbc674b33240fffaffc149f26238c5b188107df049cc615289ab8ee6f12a868379f6e362b059ba7c3dde3f02a91a08316c194ad7e556d390d38e6442212502f84cb22fc7dbab262984d2155ebeee3e4109033e57e761e9ab701512cf2635fe92f12d42953ce33f020ad4606125477318f88f673517831f43e548c5ef1d6d4aef7d850fdc0d35bc38a69ac02ccc7436eb711c6303cd306b34931bf1a4cbaed6940ede588e2abf7835718e4afed606d71cdb48146598db31d024347ba9eb289f714bfa7a3670392b3a5e35b6359f6626ed07cca451f0389e4423bb531baf409c48279df489d0073ccf17676eb5c5caa732b104894c2bcf311774f1f8c0b8b6fa313437bc1209f29ee64ccb40a07bb0cf928c77ca6b6a4fe287b1dc6df678a32b8dda35876211d5f929f90a6cc772bd171d15f50da9de8f11a241be98d205b2c53a78a5ba1bce0e782ee88512c3fc815fe843c6b5ffae1b80f1bdd5132b84a813e5157d3096034011fec2f0543f9c30a119d87e8b66e9a857d833d45fe55352871f68aaf8757c03f3b82f1cbd13c56d1843b9d2ebf7fe42f41ab0493dc9491813456fd1e0466bfdfb87a684cba8944df2fd8d3703617383137613a853a3725b366079c3760bbce60f2a88fa2cc579a6ddc9813185cb26873e6c09e43b6db73e4a44d30eebdae38bdaae9f6f1c38941b342ba67822b039f35878e54aadc4b1861df8803494f739d07b0d8b7815d1b55932bcdda80f612f97e0a0c288a7daf3aee1eb0db33fa030082b439a6d0c8d1043a718747acc398913f89e09cb0c95be96fdc9b8aa01f8eba0bd543528035fb7442ce9c6fa5e5539d4dfe29f2a9400d2d122d61037b9df584c5738b851a0d8f6bb6cf553efbdeefc3db3718681a75cb90398fa54c8dd1e696de8dba5ec977c4e2909f4977fde39847f2c0d8f9f9927e9a6cc9466b90d7745e678baa32100cb1ca7d2969c6ec9f35b222f3f4126a7965c40e5da75f183f73d33d325f25a371f5767c6b5bca141c30ed409ffce5f8e073bfb0a85512d0594c96b80cd5d7b73ac3dca494aa9dc7085ad594b46eb28fd1df84afc8a71dd63bc5d23eaf21238706a205d643bd238fe01b32dcd50c93047498ed54bb01cf2108d326f7e3c0538a9e6cc79090ccee6cf47e7fd3cc5cf41aad6905c5d099cea22effdcd4bb7b8d85ba3e3d703c34863d2540936976c774e5c4cb020873873a186c3bab67b1a47c4029f2880cbadd1cd7d82a6c649b073aa0c938b5f28e9173a64c72c81745bc8df6706bf6e320b5e96820970322f21d633a2c28b23d79b8edbc8a13eafa2a5241d7bb59b341779fe6f5db2994567caefaec23b7b7c55a73dbc6614bb958bc1d62838c56197a3eceefeb1dc4f505645548f2dd8848e4046aca421548235f1945725f82f03b0ba5c774ddea6f9524cdcc302ee4712ef7d4bc1c16d7aa578d8fd8ceb680c16fcc6ca6a40afdcef6f89e81bd92f7d1f6e39c9c57f3239a1fcb23d649f8757348214572e53bc2c2c7ff8bce6d48df6e3c53ab7014a55c9296d05998a0d1b53749d9561541eb0cf6e1bfa65141ce9b6c30fe4f68cd8e869feba82675ec43bf953ab2994533d6d1af1705130243d9b9ee4088b635d6b4db5603b8784f4fe77d4b0d8a7935c06198d12fa0fc6e1ad2ddef96e7f9ab6103a2a29739ca3af9fe1736cdf49162e77d6f17d063f04dc2e1358af3da993fb3824e59575a9f15c7c429efd059477429be0c2a5b126078a8f8b1088d35aae59eac0897dfa4d45179947bad401c7417df2fac46f8782a2069f83cc18eda4d0070167878ad72f5d255e300a6368e0d390d3d0206aba68772b1e9d73c97406a0a5d80b7b8360502a9e7cb471fb5bd49ce9eee3a16f82aadca47327ccaa00a0575ed7191ffb710dd1ab7f801"; + // https://zombie.explorer.lordofthechains.com/tx/9390a26810342151f48f455b09e5d087a5429cbba08f2381b02c43b76f813e29 + let tx_hex = "0400008085202f8900000000000001030c00e8030000000000000169e7017fbd969be53da2c1b8812002baaf59ce98b230a9c1001397ba7f4db8676bd77e8ea644b67067d1f996d8d81c279961343f00a10095bccbddc341c98539287c900cf969688ddc574786e0e34bd6d3ec2ffaab5e2d472848781b116906669786c14c5c608b20dc23c9566fd46861f6a258b5ffc6de73495b56f4823e098c8664eab895d5cd31c013428ae2cbe940dc236ca40465ea2b912ce6c36555b2affb1f38b99b28dc593d865b0b948d567f9315df666d2e65e666d829b9823154bae0410bd885582b4a8a6eb4b9ae214b59ffd9b1167b7cd48f48a11cbd67c08f4e01ed4fd78fc91d0c9e70baa4f25761ef6c78cd7268b307aaa6ece2b443937eb4beac2c8843279a8879adbe0b381e65d0b674f2feeb54b78f80b377f66baab72c4cf9f10dde48f343c001df91a1a6d252ad8eca26eea0fdee49ad7024b505e55b4e082e94616794ddd7c2b852594b4b7af2292f0aa9e34f38322f548f1a21c015e92dbfd239ce18144f3b8045e9efa3de6b4c6b338f01d0adeb26a088a3c8c00503b67b2980b7663e97541e2944e4ad3588554966b6a930d2dc01d9fc7f8a846583fcf3b721f979705eff5bb9bb1fb0cad9ad941ceb3f581710efd8c50713a53751a0a196322ef8618bf1e097383666e91b5133ba81645d2b542181476eba2326cd02fb29a9f09edc46ea04b32ed9243597318d23b955a2570d78cbfb46cc26c1807eddd1de4785b6e752f859f7e25fc67f9e8a00feafac6fd7781eb72a663d9b80c10e9c387abc4d41294b3573785fd53bc56ccac2edf5c7bbb99cb3bcf87161fa893d2e1aabfee75754767cef07a12e44bb707720e727e585a258356cc797ecee8263c0f61cfc8ffa0360c758f1348ac44c186e12ce0f4faad43b4638abd4a0bc9fd4a6fa4352c20cc771241f95c26f1671ca95c8f4a63a8318dc43299f54e8a899df78ccfd3112a0d5ea637847dd2e3b05be8c0658dd0d7d814473fa5369957c00e84df600df23faaee5faa17b9ededad4731e5e9c1099dfddf5264756800dcfcad4b006b736d1d47c59a019acde4dc22249fc40846b77b43294e32a21db745e1bec790324c3d505edc79388a6e44b02841b26306ed48cfce1e941642c30792315016dba03797c8e4e279eec5b78aad602620471f24c25aea3aaa57509aa9eef2057f11bc95bad708918f2f0df74ac179d7dffc772b2c603dd89e7aea0e8f94f1a8bab4a4fba10bf05c88fbe4b021b3faff3d558e32e4bc20be4bed62d653674ce697390e098e590a3e354cb4a1e703474de8aab30cd76cf7e237f2e66bf486c4fc6c22028764e95adf7d8fa018f44b51ae6acfa3bf80f14c45c06623b916d79649abe0a2b229f96e60e421f6e734160da37f01e915cf73d1cacd1eb7f06c26c33b4d8e4dde264f3cfe84bada0601d1c03aa31c5938750ca0b852f3177883cae9f285d582a4eb38c05f8ef6e5cff5be0745e1ec66e20752bfd5bd5a1590fa280ace3e9786e0022e7ae3c48bcca14e9c5513bc8b57e15820a685f8348159862be0579a35d8ac9d1abaf36d9274c7e750fd9ad265c0d8f08c95ed9ce69eef3a55aef05f2d5d601f80f472689f3428e4f0095829a459813d5dace7e6137a752ae5567982e67b2092afeba99561fbe4e716f67bd1b4e8de1f376dec30eed27371bcc42d7de2ea0f4288054618e9afa002a2d1996b7a70a9683229f28bab811b67629dad527f325c0f12e19d92bac51e5924f27048fa118673b52b296b3642ec946d9915ded0ae84e1a2236da65f672bdad75a22cc0ea751c07e56d2ec22caa41afc98ec6b37a8c1b6a5378a81f2cdb2228f4efb8d7f35c0086a955e1b04bd09bd7e056c949fab1805f733a8b2061adad0c2b7fae33d21363de911e517b21a1539dfa1b3cbb1ea0dbfa3ffff23bbac01183f852de41e798fca5a278b711893175aeaded90873574d8de30b360f39ea239492c630eda4a811d3bb7a125054d5ca74bb6698aeea1a417ad19415ca0e5ca36abc2f96725986f73bcbe3113e391010d08f58f05979c7cef26ff92506c5d1eb2a2f6f5689e9a39957f0723bef3262f5190de996234d4f00b73ed74d78fdf1e6bf31161e16bd083bc6fbddc4eba85c17067e15f08019e5ed943de8e23a974d516abc641e85e641b03779816c30b3449a16b142417c1ff93ab7fa8f96a175e9ef73b3f06ac76788c27889d426efa78d5b8ce35be4591902f7766fe579a0aa28229235a920d26264c09625dea807f619a040f08931d6e1fe57ff0c48ea476be93a16d1fc8de3617984eeebcf14b63c839b41f8f9305402d1288c8e481a4fa5c3302bb1f83e3f0dc8ff9550f9bacb44bccb58f3de152abef5d578afed1c29dc89495b9e54a0c6d00f1dba45a2cf68c9512d9a9ff0b2531e58e47428a99cb246ca23f867b660dc71785b57407cc292f735634c602409792c4640831809f1f1e51903273b623aa0ae0cdd335c7b9db360b0bceb0d15f2313e1944800f30f82ed5bb07cfa1c4740c2bf2806539a4afac1f79d779b923ad8dc2493ebb2d2fce9aea58a009d64e7d1b71ca6893b076e41f7e88a4b51b5402e3fa6c60fa65a686adea229f0164318c9fa1b6d2d2218e5ada710daffecb6b7dd8bf7447658795c4c7a0ad710c4f02fd19017a0575f9467600cdca019793f2f49d197dbfc937828e5790b90929e5ca16037ec79734b64feec36b36c220a2979c45dd51e24c9fb21d8634471aac20c6f179f90c0d61c7b3d89826d146b157bedd8f6b66f6edfabfe04b49f2f2d999fc2e578a440bafd524c82ae614dc8017e379cf926e042f4fbd6f0628fde52de18d764ba8385b77569eda30d5a3617fb0a0c7fd26c821308c3ae98498d33b974cb318a04af3ea3fbcb13fc62fc952aaef095423da9ec7bdc7b77adbd403931189ddc98fe19a06711415b40a9a68812bb7c5453b7b2377910c7b89c99b379e038a7940487c0fd2405456ee55ab6ead3ef25a8a5b1abcae479c24f5e6869057e0bdabcdf352b4a64a3e385171a6e14c8102b2a187034e21705e3a457167fe0dc0d63d6e8d489c9a18c9d84b541504d36b086c2c63cc1a34c0080122c5d60ca33ab60289d16f21e1ded753607267c2093b1c587b89da9df65584fbe3ff9eb7f91d64e33912b8e91adc27191d22f8e835be6bb24546f21488f7abcb29339c34058d4f4093096144b17b8ab76a346275b7e7c80bca59d20e0bb482bb2a9cc3c9515cc1b5be17348c65c73e9fb1ed77d423c509f7cff0e355a34d080d310f3b848dbc209bbba6b6b109fb8d9556dca0fab086e197327ab423d5d762b68961244d8d22c30a8a3a116770bb15b5a0a347091a843b68d6a8e0f1c79f12523a7561c1233cd44db90f6cd3c1ce5fc13f8382177b5522aae028379269b71ae2a42f41dff7374ed7e83c89566f57297b82478b04359a2c199ce8f842112b7450cc1e2e2e394cda4c67e0b2302e21f6af997607ceefd067f77be8900bb3ecb3e30782477aa76861b286b9ddc9e36fcebb50f04f9516e02da31e6219bb5bcb81ee673d95be14c1bd2be4909556d6dbca0365292c582dedcafcc60b255ab7bcd9d977a4139f394ca1da81040e784fd8e7534f230bc5201e7f1db47eadc30f37609d5bbaba624157d98d65029bbab766b6c23c3049a32b894c0cfcb40913ba1cd2d5acda7d2acc920fd01c36f28fc6b7ffd01a37b17fc3235d0dbe9b8098530bed6894b288604b8689f4aafc22cdf211fb95ef5c90cae62a250234e6f790e9a15012acac88305dc4f91fd564a9ab8bb27c057ec5dd46fe952a7be557caea9b7b1d6118aa42df79b8c207e2bae6c34d67dc32b4360ad20b3e609e9caeb7f432ad51cfce139f2d4eb9ed219f4323acd5685e0e0409939eb662175a83fa083f500516dbcb091a3448cb24c3198c8fc547fbda3cb0894edeceef7ccb4ad746aa06f4038b63ab4095a9c390656520561ba3763b1057b3af7cb548342a2bfc2ab725b01b12a7adfc30d7d9632acafd2595cde406b8637a911b7c86f7b09b11f58acec3f1a1bd7cf6853331b48d7907ed699d91fbdbcab8001e3d8d3a26b491b6e2d98c5e149847a07a2b7faa1f567cd4bc9c83ad553339632f3dcacb890c5222656b3349ddd5c8eacaa490ac0b2b38f8a26da9ce7789f5601769a7f10b93125cb93b589bda4ddb4e8795817b60cc149af7c0699b2bbbf655f2f5ec170d6af51213e8c725e699d181923ecf10c6f1069f46e6bc89c7a29d2ebe133b5c0c4b67826a93add7d4824e60b4c5f0cee358abedb50c54a59e95185d7a80081f2dddba5c7c7c637b2dfe8575ddaa71306a2725c9ec17b8e4e1f271a442f6798cc21bbd55c2d69819ddde37a8e8d6a812c41a3e58719b7c96e9375155c4a873ed698ad37144ef32e3fe41cce9c48bbe31441dbbeec7b97734769063d6d04cd8d4963f09f7101bf57cb97a83452cc5de873c5ac0ce001c471c9fcd3275d90a118dd4c25a525d9fb358ff85104b98136850786b387fa17cc1a1d128bc5f7c365ec7920ea677e4c8023071a958647d9fbd27e29d7d099b4dfbbac086ac2af00407fd12092ef1f4847bf8988d839e49a6b5b42482c3dde77022ace66e1ca15b46f2df88d053c1bc3623110b3be74b08749eba6d22f87a44cf7cc1997e7e45d0e"; let tx_bytes = hex::decode(tx_hex).unwrap(); let tx = ZTransaction::read(tx_bytes.as_slice()).unwrap(); let tx = tx.into(); + let expected_fee = DexFee::WithBurn { + fee_amount: "0.0075".into(), + burn_amount: "0.0025".into(), + burn_destination: DexFeeBurnDestination::PreBurnAccount, + }; + let validate_fee_args = ValidateFeeArgs { fee_tx: &tx, expected_sender: &[], - fee_addr: &[], dex_fee: &DexFee::Standard(MmNumber::from("0.001")), min_block_number: 12000, uuid: &[1; 16], }; // Invalid amount should return an error - let err = block_on(coin.validate_fee(validate_fee_args)).unwrap_err().into_inner(); + let err = coin.validate_fee(validate_fee_args).await.unwrap_err().into_inner(); match err { - ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("Dex fee has invalid amount")), + ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("invalid amount")), _ => panic!("Expected `WrongPaymentTx`: {:?}", err), } @@ -250,40 +307,73 @@ fn zombie_coin_validate_dex_fee() { let validate_fee_args = ValidateFeeArgs { fee_tx: &tx, expected_sender: &[], - fee_addr: &[], - dex_fee: &DexFee::Standard(MmNumber::from("0.01")), + dex_fee: &expected_fee, min_block_number: 12000, uuid: &[2; 16], }; - let err = block_on(coin.validate_fee(validate_fee_args)).unwrap_err().into_inner(); + let err = coin.validate_fee(validate_fee_args).await.unwrap_err().into_inner(); match err { - ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("Dex fee has invalid memo")), + ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("invalid memo")), _ => panic!("Expected `WrongPaymentTx`: {:?}", err), } + /* Fix realtime min_block_number to run this test: // Confirmed before min block + let min_block_number = 451208; let validate_fee_args = ValidateFeeArgs { fee_tx: &tx, expected_sender: &[], - fee_addr: &[], - dex_fee: &DexFee::Standard(MmNumber::from("0.01")), - min_block_number: 14000, + dex_fee: &expected_fee, + min_block_number: , uuid: &[1; 16], }; - let err = block_on(coin.validate_fee(validate_fee_args)).unwrap_err().into_inner(); + let err = coin.validate_fee(validate_fee_args).await.unwrap_err().into_inner(); match err { ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("confirmed before min block")), _ => panic!("Expected `WrongPaymentTx`: {:?}", err), - } + } */ // Success validation let validate_fee_args = ValidateFeeArgs { fee_tx: &tx, expected_sender: &[], - fee_addr: &[], - dex_fee: &DexFee::Standard(MmNumber::from("0.01")), + dex_fee: &expected_fee, + min_block_number: 12000, + uuid: &[1; 16], + }; + coin.validate_fee(validate_fee_args).await.unwrap(); + + // Test old standard dex fee with no burn output + // TODO: disable when the upgrade transition period ends + + // https://zombie.explorer.lordofthechains.com/tx/9eb7fc697b280499df33e5838af6e67540d436fd8f565f47a7f03e6013e8342c + let tx_2_hex = "0400008085202f8900000000000006030c00e80300000000000001c167d6e78e09dfbac2973bfd8acac75fc603f6ffb481377e3ec790f1cc812a8a3979ecfb8a0c7c3a966d90675261568550f9363f9384a21390d7f58bde6f7b03270d88e1fa61d739c27d7f585c9bbc81a3d522fbb88fe8dc8567e27a048d475ce14fdfd11455fd54c577538438decbf6954f1ffba86c78896178ce514c5f1762a7de9e83552533eb4c558c4f9950b1806f266b25d6437f5aac08048d6f48100d49ecb2253e85c3b555a7cd84c9628ae58e5d68ddad61e69edfcdc0fa12170dd80340c417bff9e1711bf6e9728a6a52c42598d7ffd00c35679b1555cab075e54b134901d02ca9b07bb20c5719b2728faa020fb844c183c2ae649034a5476c4d129c3f97cd00a87be1ca7e73d027188cdab57fbb34b5addb7432f51454299b8cf47b389f98bad8abd42d82a2f8c2d11312e39272d44409540bcfa4c6b445e8e6dc63cc2fd5db1448875adb055ea8665c863bd07bf3aa8eb210f638287789957c96c54819061ee215eb7ba7b6048591a57f097a3e5da06b6359325d830d5b74c20c025996a113e4bb9fd2c853b7360d4961396cd99c23a13de972097eede3a955a5d5d8c8695a7290581a248fc03ea87606e71564d8e8fb00ebb8d5c10fc8fefe1660171524264060d15363fc2dc0ac0ab21fcbae1dc53786873cb9e8716f3ada651e79c3306ad49adeeb354213cc37499e217fa1c0f219e85bd22cf493f5e76f053543dd3b36bd180b1dcf17f781e35d6955c33c06426a885138f1e21b78ee87a27624f33b6567bfa6a0fe43e2d623578f6917d300a408c4dd48683213ffad453de1003e120fbfa74a6db4628af9d446e26492fde67bf52d034fcaf2b9b959472404fd631ef599815c6f190807b75f638e134148a5813424ba6cf59cf86ce515a14b95f7b8f80b1aa1b3cbbc091fa2a686277a9cc613e48b2c227aed7b4b093ac8b12a238bc99f9983c8bac21bb0f897eada35bf0e01b1436cf6d44b959595bdcdfd4676e28b500b9ad6b8a5825c3d3c0c38a4a5a2c3ded205584439621eaa7ee639b09aca1f533bb4892b29d761d94887fa78f605b9b8f5b3ab44ea578d9329bd78d7a6ae903f1960e16007a924be79ab31ea6ed7466485488b5c71eb02d6b99f345f2f61cb3cd994045c502d19f615233b3ebb263981de26674de082d384cc04c09a309567780f7f24298847fc2dff5f22082074684aa9efa260b8aaf4357bff2e9d32f8918b16876051b5459136dcc8788aba7b2ead435c3bf662f9f1acddd4a8a71b593e99ed50e158028946195ee991666bf88f4cf4d30a04c877ce8a9e6d224aed662e85a32f5cb9029a3dd4ba663b6f6314ef58fbce623171946d01d1ff456f90131159e5209cb41329061a0dd8a5fc35576108681e783fb173f67dda33134a9b1f07494a1d6273810fd77a25c92f7444d6226738d5c7161b7b198be069ac65d50a22d728292e95d1859e0c646db62aa3f401e55026a551b1edfe8fd5eca8e4c6836bd09429b5e22f64a09db4c6935b6febcbac6430f66dc0280c9be046133795f1f59ec32cbf4511749984f7b2ba131588f86f82322901ee7d709550ecadb5b915d5cfb2e950d2a8c5eda57da49d2ac9562b851f81e70a32178989e83807f04a6324cf7320a26a91b41e31a06c706431794ffb8b9ce5f3d853fb9106c8a98ea3b2948356948bfbbd63eb30e3cb68d7e373df80221d1b1211c717afe8b7b0b46a3208859254d9ae3517b8e031f413178c0fd408e76ccdc580a9a19edf4b3c70c273f4c8c626fad225e5aeee890c65328437b8bf316066e54a4741d8ac8ab9b5555f09b89b79165f9aa08a59be8f10c121b1b425bd5e3a64b6e4db3e1cacb00a5867fd05b454b75ff1eb8560770f21af7680107560a2209373d2999eb21bed2a10bafe1eaf5a31c18e69c63cce9b8c6cddcfc1088f956bcf3c9adeb77ef0589ab6405f0a9ba5650819a48fb42597fcd2f4ad67bdc89870d82eaa0d8dbd298a59ff552576dedb539834de725638e0f68307d4ac203d8e2e4649e31abc4e8748251c8fb6df3459300d1badfc19ad4d2f680f466b02680bb3e5a13c0c8a5db3665bc9fc2093c4d38acb176754db556ebd1663c23f284bec95279957b112131f8aa09af15ff26eebea3215c96b9df43c9fc9134d9db4e588aff293f3084db13e1d92bc33ca07a1b534b4a4e5fcbf098be7d26f9312db7f9d6b160318a4562c3c3b0c87688c59f402e0032242324339ef33713bf39c2110e7eb155bf926888385fe4b18bf3ef13dc2601b76def3d763f5b2ddea363f7e3697112194fb6332be96540a53a86e1e34fd70429dcfc39c5e2f68fa72e0045fe4ef12b965f0827c5bee9cd4f0c9b4cf6468316384fe33df5703c7742f9b409b9a508e94faa8be3c27ad75d21f85ee31753c96deb909221befd62bae084885c890d89f775dc0eee940ffbcad0aa65c08a71d09e234ad150e82610ba03deb608d44e9019d8579f9e9351daa6f3bcbbc8ec170c8b700bcb495c333b32136721f6417a3f3b12500641eb7af9e5813fafd27794a7b2476320fde18f3019302d49d77c3536af214e6c8357a36029a37a07011d1cdbe0db3fe7443a6908f5d3b6e08d61f33bad2a0bfbc9db86022d4f91b0ba6ef1b5ec30f0187f4c540eeb117c4d3d78659e46540df4b9301c6fce031d7e438abeb13a747be6ce9c0a33a2bd6f6092d0a26d5ba138bb6f2c3113ea6cff868853dacfc5df0433049a59d2b365e9a87ee6a6203e52121d60bc709feb1c1a30e95fbc600f648dfa5fadc8cf324a4c5d91e1f80501661aa51a518b381933932a1367e4369e07943f291012f5a9394692d9984fc2dc55c0ec4fe3d18a4a0b9f9d7c9d3f57b2e2a0c31f08f17ffe7355fec963b8ae364ed8cff046aa8220dc813f2dc78405069c707afadb77cfc8d64803a25eab7ebc74c738b41f9b3f2d881f1e2b77d37f38c1b5991daf5c911c04947891909f9c3e50e1314884207f0ea99d9310c9cfe93fea53fb57c93efbd412702e283e61196b9158de774333893b51c768ae48ec086e47b105d0b21357bd14f85b9f145fbfd63c0e998d6e54900915c8ffaf1234fa910ede3035e5e47ee9b22559459d0ea2b0f3242c5ec2782d09a7b477b560b1ecfd14d82f24600334d2c85dc2def0f457ea199e266c52fb9a596de02da05a9df8e4731cf941e1ada11c66d0954742745d5ef1b36dc7628614ed28ba9358ab38c2d007aa90147906270ab35ae26fa3473ec5881f8e6ed04c592a403386c4061becc70b5735531f8d249abb079317f43f111de58c6678e62a6d2dc83193acef928c906"; + let tx_2_bytes = hex::decode(tx_2_hex).unwrap(); + let tx_2 = ZTransaction::read(tx_2_bytes.as_slice()).unwrap(); + let tx_2 = tx_2.into(); + + // Success validation + let validate_fee_args = ValidateFeeArgs { + fee_tx: &tx_2, + expected_sender: &[], + dex_fee: &DexFee::Standard("0.00999999".into()), + min_block_number: 12000, + uuid: &[1; 16], + }; + let err = coin.validate_fee(validate_fee_args).await.unwrap_err().into_inner(); + match err { + ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("invalid amount")), + _ => panic!("Expected `WrongPaymentTx`: {:?}", err), + } + + // Success validation + let expected_std_fee = DexFee::Standard("0.01".into()); + let validate_fee_args = ValidateFeeArgs { + fee_tx: &tx_2, + expected_sender: &[], + dex_fee: &expected_std_fee, min_block_number: 12000, uuid: &[1; 16], }; - block_on(coin.validate_fee(validate_fee_args)).unwrap(); + coin.validate_fee(validate_fee_args).await.unwrap(); } diff --git a/mm2src/coins/z_coin/z_htlc.rs b/mm2src/coins/z_coin/z_htlc.rs index bd32389f51..13093b80b0 100644 --- a/mm2src/coins/z_coin/z_htlc.rs +++ b/mm2src/coins/z_coin/z_htlc.rs @@ -12,7 +12,7 @@ use crate::utxo::utxo_common::payment_script; use crate::utxo::{sat_from_big_decimal, UtxoAddressFormat}; use crate::z_coin::SendOutputsErr; use crate::z_coin::{ZOutput, DEX_FEE_OVK}; -use crate::NumConversError; +use crate::{DexFee, NumConversError}; use crate::{PrivKeyPolicyNotAllowed, TransactionEnum}; use bitcrypto::dhash160; use derive_more::Display; @@ -86,19 +86,36 @@ pub async fn z_send_htlc( /// Sends HTLC output from the coin's my_z_addr pub async fn z_send_dex_fee( coin: &ZCoin, - amount: BigDecimal, + dex_fee: DexFee, uuid: &[u8], ) -> Result> { - let dex_fee_amount = sat_from_big_decimal(&amount, coin.utxo_arc.decimals)?; + if matches!(dex_fee, DexFee::NoFee) { + return MmError::err(SendOutputsErr::InternalError("unexpected DexFee::NoFee".to_string())); + } + let dex_fee_amount_sat = sat_from_big_decimal(&dex_fee.fee_amount().to_decimal(), coin.utxo_arc.decimals)?; + // add dex fee output let dex_fee_out = ZOutput { to_addr: coin.z_fields.dex_fee_addr.clone(), - amount: Amount::from_u64(dex_fee_amount).map_err(|_| NumConversError::new("Invalid ZCash amount".into()))?, + amount: Amount::from_u64(dex_fee_amount_sat) + .map_err(|_| NumConversError::new("Invalid ZCash amount".into()))?, viewing_key: Some(DEX_FEE_OVK), memo: Some(MemoBytes::from_bytes(uuid).expect("uuid length < 512")), }; + let mut outputs = vec![dex_fee_out]; + if let Some(dex_burn_amount) = dex_fee.burn_amount() { + let dex_burn_amount_sat = sat_from_big_decimal(&dex_burn_amount.to_decimal(), coin.utxo_arc.decimals)?; + // add output to the dex burn address: + let dex_burn_out = ZOutput { + to_addr: coin.z_fields.dex_burn_addr.clone(), + amount: Amount::from_u64(dex_burn_amount_sat) + .map_err(|_| NumConversError::new("Invalid ZCash amount".into()))?, + viewing_key: Some(DEX_FEE_OVK), + memo: Some(MemoBytes::from_bytes(uuid).expect("uuid length < 512")), + }; + outputs.push(dex_burn_out); + } - let tx = coin.send_outputs(vec![], vec![dex_fee_out]).await?; - + let tx = coin.send_outputs(vec![], outputs).await?; Ok(tx) } diff --git a/mm2src/coins/z_coin/z_rpc.rs b/mm2src/coins/z_coin/z_rpc.rs index 7bfd299bdc..91d897d126 100644 --- a/mm2src/coins/z_coin/z_rpc.rs +++ b/mm2src/coins/z_coin/z_rpc.rs @@ -831,10 +831,14 @@ impl SaplingSyncLoopHandle { if max_in_wallet >= current_block { break; } else { + debug!("Updating wallet.db from block {} to {}", max_in_wallet, current_block); self.notify_building_wallet_db(max_in_wallet.into(), current_block.into()); } }, - None => self.notify_building_wallet_db(0, current_block.into()), + None => { + debug!("Updating wallet.db from block {} to {}", 0, current_block); + self.notify_building_wallet_db(0, current_block.into()) + }, } let scan = DataConnStmtCacheWrapper::new(wallet_ops.clone()); diff --git a/mm2src/common/common.rs b/mm2src/common/common.rs index d9b984a118..1e3bca0f45 100644 --- a/mm2src/common/common.rs +++ b/mm2src/common/common.rs @@ -183,6 +183,7 @@ cfg_native! { use findshlibs::{IterationControl, Segment, SharedLibrary, TargetSharedLibrary}; use std::env; use std::sync::Mutex; + use std::str::FromStr; } cfg_wasm32! { @@ -203,13 +204,18 @@ pub const APPLICATION_GRPC_WEB_TEXT_PROTO: &str = "application/grpc-web-text+pro pub const SATOSHIS: u64 = 100_000_000; +/// Dex fee public key for chains where SECP256K1 is supported pub const DEX_FEE_ADDR_PUBKEY: &str = "03bc2c7ba671bae4a6fc835244c9762b41647b9827d4780a89a949b984a8ddcc06"; +/// Public key to collect the burn part of dex fee, for chains where SECP256K1 is supported +pub const DEX_BURN_ADDR_PUBKEY: &str = "0369aa10c061cd9e085f4adb7399375ba001b54136145cb748eb4c48657be13153"; pub const PROXY_REQUEST_EXPIRATION_SEC: i64 = 15; lazy_static! { pub static ref DEX_FEE_ADDR_RAW_PUBKEY: Vec = hex::decode(DEX_FEE_ADDR_PUBKEY).expect("DEX_FEE_ADDR_PUBKEY is expected to be a hexadecimal string"); + pub static ref DEX_BURN_ADDR_RAW_PUBKEY: Vec = + hex::decode(DEX_BURN_ADDR_PUBKEY).expect("DEX_BURN_ADDR_PUBKEY is expected to be a hexadecimal string"); } #[cfg(not(target_arch = "wasm32"))] @@ -618,6 +624,17 @@ pub fn var(name: &str) -> Result { } } +#[cfg(not(target_arch = "wasm32"))] +pub fn env_var_as_bool(name: &str) -> bool { + match env::var(name) { + Ok(v) => FromStr::from_str(&v).unwrap_or_default(), + Err(_err) => false, + } +} + +#[cfg(target_arch = "wasm32")] +pub fn env_var_as_bool(_name: &str) -> bool { false } + /// TODO make it wasm32 only #[cfg(target_arch = "wasm32")] pub fn var(_name: &str) -> Result { ERR!("Environment variable not supported in WASM") } diff --git a/mm2src/mm2_main/Cargo.toml b/mm2src/mm2_main/Cargo.toml index 856cd244ac..7b06a9d9c4 100644 --- a/mm2src/mm2_main/Cargo.toml +++ b/mm2src/mm2_main/Cargo.toml @@ -129,7 +129,7 @@ coins = { path = "../coins", features = ["for-tests"] } coins_activation = { path = "../coins_activation", features = ["for-tests"] } common = { path = "../common", features = ["for-tests"] } mm2_test_helpers = { path = "../mm2_test_helpers" } -trading_api = { path = "../trading_api", features = ["mocktopus"] } +trading_api = { path = "../trading_api", features = ["for-tests"] } mocktopus = "0.8.0" testcontainers = "0.15.0" web3 = { git = "https://github.com/KomodoPlatform/rust-web3", tag = "v0.20.0", default-features = false, features = ["http-rustls-tls"] } @@ -145,3 +145,4 @@ chrono = "0.4" gstuff = { version = "0.7", features = ["nightly"] } prost-build = { version = "0.12", default-features = false } regex = "1" + diff --git a/mm2src/mm2_main/src/database/my_swaps.rs b/mm2src/mm2_main/src/database/my_swaps.rs index 2fe1a85890..74214b2449 100644 --- a/mm2src/mm2_main/src/database/my_swaps.rs +++ b/mm2src/mm2_main/src/database/my_swaps.rs @@ -296,6 +296,7 @@ pub fn select_unfinished_swaps_uuids(conn: &Connection, swap_type: u8) -> SqlRes /// The SQL query selecting upgraded swap data and send it to user through RPC API /// It omits sensitive data (swap secret, p2p privkey, etc) for security reasons +/// TODO: should we add burn amount for rpc? pub const SELECT_MY_SWAP_V2_FOR_RPC_BY_UUID: &str = r#"SELECT my_coin, other_coin, @@ -317,6 +318,7 @@ WHERE uuid = :uuid; "#; /// The SQL query selecting upgraded swap data required to re-initialize the swap e.g., on restart. +/// NOTE: for maker v2 swap the dex_fee is stored as default (the real one could be no fee if taker is the dex pubkey) pub const SELECT_MY_SWAP_V2_BY_UUID: &str = r#"SELECT my_coin, other_coin, diff --git a/mm2src/mm2_main/src/ext_api.rs b/mm2src/mm2_main/src/ext_api.rs deleted file mode 100644 index f1b92c145f..0000000000 --- a/mm2src/mm2_main/src/ext_api.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! RPCs for integration with external third party trading APIs. - -pub mod one_inch; diff --git a/mm2src/mm2_main/src/lp_ordermatch.rs b/mm2src/mm2_main/src/lp_ordermatch.rs index de75e4ed3b..b36a6fa79f 100644 --- a/mm2src/mm2_main/src/lp_ordermatch.rs +++ b/mm2src/mm2_main/src/lp_ordermatch.rs @@ -25,7 +25,7 @@ use blake2::digest::{Update, VariableOutput}; use blake2::Blake2bVar; use coins::utxo::{compressed_pub_key_from_priv_raw, ChecksumType, UtxoAddressFormat}; use coins::{coin_conf, find_pair, lp_coinfind, BalanceTradeFeeUpdatedHandler, CoinProtocol, CoinsContext, - FeeApproxStage, MarketCoinOps, MmCoinEnum}; + FeeApproxStage, MmCoinEnum}; use common::executor::{simple_map::AbortableSimpleMap, AbortSettings, AbortableSystem, AbortedError, SpawnAbortable, SpawnFuture, Timer}; use common::log::{error, warn, LogOnError}; @@ -77,8 +77,8 @@ use crate::lp_network::{broadcast_p2p_msg, request_any_relay, request_one_peer, use crate::lp_swap::maker_swap_v2::{self, MakerSwapStateMachine, MakerSwapStorage}; use crate::lp_swap::taker_swap_v2::{self, TakerSwapStateMachine, TakerSwapStorage}; use crate::lp_swap::{calc_max_maker_vol, check_balance_for_maker_swap, check_balance_for_taker_swap, - check_other_coin_balance_for_swap, detect_secret_hash_algo, dex_fee_amount_from_taker_coin, - generate_secret, get_max_maker_vol, insert_new_swap_to_db, is_pubkey_banned, lp_atomic_locktime, + check_other_coin_balance_for_swap, detect_secret_hash_algo, generate_secret, get_max_maker_vol, + insert_new_swap_to_db, is_pubkey_banned, lp_atomic_locktime, p2p_keypair_and_peer_id_to_broadcast, p2p_private_and_peer_id_to_broadcast, run_maker_swap, run_taker_swap, swap_v2_topic, AtomicLocktimeVersion, CheckBalanceError, CheckBalanceResult, CoinVolumeInfo, MakerSwap, RunMakerSwapInput, RunTakerSwapInput, SwapConfirmationsSettings, @@ -3048,7 +3048,6 @@ fn lp_connect_start_bob(ctx: MmArc, maker_match: MakerMatch, maker_order: MakerO maker_volume: maker_amount, secret, taker_coin: t.clone(), - dex_fee: dex_fee_amount_from_taker_coin(&t, m.ticker(), &taker_amount), taker_volume: taker_amount, taker_premium: Default::default(), conf_settings: my_conf_settings, @@ -3206,7 +3205,6 @@ fn lp_connected_alice(ctx: MmArc, taker_order: TakerOrder, taker_match: TakerMat maker_coin: m.clone(), maker_volume: maker_amount, taker_coin: t.clone(), - dex_fee: dex_fee_amount_from_taker_coin(&t, maker_coin_ticker, &taker_amount), taker_volume: taker_amount, taker_premium: Default::default(), secret_hash_algo, diff --git a/mm2src/mm2_main/src/lp_swap.rs b/mm2src/mm2_main/src/lp_swap.rs index f2486f03e5..1bca1979c9 100644 --- a/mm2src/mm2_main/src/lp_swap.rs +++ b/mm2src/mm2_main/src/lp_swap.rs @@ -62,20 +62,20 @@ use crate::lp_network::{broadcast_p2p_msg, Libp2pPeerId, P2PProcessError, P2PPro use crate::lp_swap::maker_swap_v2::{MakerSwapStateMachine, MakerSwapStorage}; use crate::lp_swap::taker_swap_v2::{TakerSwapStateMachine, TakerSwapStorage}; use bitcrypto::{dhash160, sha256}; -use coins::{lp_coinfind, lp_coinfind_or_err, CoinFindError, DexFee, MmCoin, MmCoinEnum, TradeFee, TransactionEnum}; +use coins::{lp_coinfind, lp_coinfind_or_err, CoinFindError, MmCoinEnum, TradeFee, TransactionEnum}; use common::log::{debug, warn}; use common::now_sec; use common::{bits256, calc_total_pages, executor::{spawn_abortable, AbortOnDropHandle, SpawnFuture, Timer}, log::{error, info}, - var, HttpStatusCode, PagingOptions, StatusCode}; + HttpStatusCode, PagingOptions, StatusCode}; use derive_more::Display; use http::Response; use mm2_core::mm_ctx::{from_ctx, MmArc}; use mm2_err_handle::prelude::*; use mm2_libp2p::behaviours::atomicdex::MAX_TIME_GAP_FOR_CONNECTED_PEER; use mm2_libp2p::{decode_signed, encode_and_sign, pub_sub_topic, PeerId, TopicPrefix}; -use mm2_number::{BigDecimal, BigRational, MmNumber, MmNumberMultiRepr}; +use mm2_number::{BigDecimal, MmNumber, MmNumberMultiRepr}; use mm2_state_machine::storable_state_machine::StateMachineStorage; use parking_lot::Mutex as PaMutex; use rpc::v1::types::{Bytes as BytesJson, H256 as H256Json, H264}; @@ -161,6 +161,12 @@ const NEGOTIATE_SEND_INTERVAL: f64 = 30.; /// If a certain P2P message is not received, swap will be aborted after this time expires. const NEGOTIATION_TIMEOUT_SEC: u64 = 90; +/// Add refund fee to calculate maximum available balance for a swap (including possible refund) +pub(crate) const INCLUDE_REFUND_FEE: bool = true; + +/// Do not add refund fee to calculate fee needed only to make a successful swap +pub(crate) const NO_REFUND_FEE: bool = false; + const MAX_STARTED_AT_DIFF: u64 = MAX_TIME_GAP_FOR_CONNECTED_PEER * 3; cfg_wasm32! { @@ -328,6 +334,19 @@ pub fn broadcast_p2p_tx_msg(ctx: &MmArc, topic: String, msg: &TransactionEnum, p broadcast_p2p_msg(ctx, topic, encoded_msg, from); } +impl SwapMsg { + fn swap_msg_to_store(self, msg_store: &mut SwapMsgStore) { + match self { + SwapMsg::Negotiation(data) => msg_store.negotiation = Some(data), + SwapMsg::NegotiationReply(data) => msg_store.negotiation_reply = Some(data), + SwapMsg::Negotiated(negotiated) => msg_store.negotiated = Some(negotiated), + SwapMsg::TakerFee(data) => msg_store.taker_fee = Some(data), + SwapMsg::MakerPayment(data) => msg_store.maker_payment = Some(data), + SwapMsg::TakerPayment(taker_payment) => msg_store.taker_payment = Some(taker_payment), + } + } +} + pub async fn process_swap_msg(ctx: MmArc, topic: &str, msg: &[u8]) -> P2PRequestResult<()> { let uuid = Uuid::from_str(topic).map_to_mm(|e| P2PRequestError::DecodeError(e.to_string()))?; @@ -365,14 +384,7 @@ pub async fn process_swap_msg(ctx: MmArc, topic: &str, msg: &[u8]) -> P2PRequest let mut msgs = swap_ctx.swap_msgs.lock().unwrap(); if let Some(msg_store) = msgs.get_mut(&uuid) { if msg_store.accept_only_from.bytes == msg.2.unprefixed() { - match msg.0 { - SwapMsg::Negotiation(data) => msg_store.negotiation = Some(data), - SwapMsg::NegotiationReply(data) => msg_store.negotiation_reply = Some(data), - SwapMsg::Negotiated(negotiated) => msg_store.negotiated = Some(negotiated), - SwapMsg::TakerFee(data) => msg_store.taker_fee = Some(data), - SwapMsg::MakerPayment(data) => msg_store.maker_payment = Some(data), - SwapMsg::TakerPayment(taker_payment) => msg_store.taker_payment = Some(taker_payment), - } + msg.0.swap_msg_to_store(msg_store); } else { warn!("Received message from unexpected sender for swap {}", uuid); } @@ -785,62 +797,6 @@ pub fn lp_atomic_locktime(maker_coin: &str, taker_coin: &str, version: AtomicLoc } } -fn dex_fee_rate(base: &str, rel: &str) -> MmNumber { - let fee_discount_tickers: &[&str] = if var("MYCOIN_FEE_DISCOUNT").is_ok() { - &["KMD", "MYCOIN"] - } else { - &["KMD"] - }; - if fee_discount_tickers.contains(&base) || fee_discount_tickers.contains(&rel) { - // 1/777 - 10% - BigRational::new(9.into(), 7770.into()).into() - } else { - BigRational::new(1.into(), 777.into()).into() - } -} - -pub fn dex_fee_amount(base: &str, rel: &str, trade_amount: &MmNumber, min_tx_amount: &MmNumber) -> DexFee { - let rate = dex_fee_rate(base, rel); - let fee = trade_amount * &rate; - - if &fee <= min_tx_amount { - return DexFee::Standard(min_tx_amount.clone()); - } - - if base == "KMD" { - // Drop the fee by 25%, which will be burned during the taker fee payment. - // - // This cut will be dropped before return if the final amount is less than - // the minimum transaction amount. - - // Fee with 25% cut - let new_fee = &fee * &MmNumber::from("0.75"); - - let (fee, burn) = if &new_fee >= min_tx_amount { - // Use the max burn value, which is 25%. - let burn_amount = &fee - &new_fee; - - (new_fee, burn_amount) - } else { - // Burn only the exceed amount because fee after 25% cut is less - // than `min_tx_amount`. - let burn_amount = &fee - min_tx_amount; - - (min_tx_amount.clone(), burn_amount) - }; - - return DexFee::with_burn(fee, burn); - } - - DexFee::Standard(fee) -} - -/// Calculates DEX fee with a threshold based on min tx amount of the taker coin. -pub fn dex_fee_amount_from_taker_coin(taker_coin: &dyn MmCoin, maker_coin: &str, trade_amount: &MmNumber) -> DexFee { - let min_tx_amount = MmNumber::from(taker_coin.min_tx_amount()); - dex_fee_amount(taker_coin.ticker(), maker_coin, trade_amount, &min_tx_amount) -} - #[derive(Clone, Debug, Eq, Deserialize, PartialEq, Serialize)] pub struct NegotiationDataV1 { started_at: u64, @@ -1821,11 +1777,6 @@ pub fn generate_secret() -> Result<[u8; 32], rand::Error> { Ok(sec) } -/// Add refund fee to calculate maximum available balance for a swap (including possible refund) -pub(crate) const INCLUDE_REFUND_FEE: bool = true; -/// Do not add refund fee to calculate fee needed only to make a successful swap -pub(crate) const NO_REFUND_FEE: bool = false; - #[cfg(all(test, not(target_arch = "wasm32")))] mod lp_swap_tests { use super::*; @@ -1834,58 +1785,12 @@ mod lp_swap_tests { use coins::utxo::rpc_clients::ElectrumConnectionSettings; use coins::utxo::utxo_standard::utxo_standard_coin_with_priv_key; use coins::utxo::{UtxoActivationParams, UtxoRpcMode}; - use coins::MarketCoinOps; use coins::PrivKeyActivationPolicy; + use coins::{DexFee, MarketCoinOps, TestCoin}; use common::{block_on, new_uuid}; use mm2_core::mm_ctx::MmCtxBuilder; use mm2_test_helpers::for_tests::{morty_conf, rick_conf, MORTY_ELECTRUM_ADDRS, RICK_ELECTRUM_ADDRS}; - - #[test] - fn test_dex_fee_amount() { - let min_tx_amount = MmNumber::from("0.0001"); - - let base = "BTC"; - let rel = "ETH"; - let amount = 1.into(); - let actual_fee = dex_fee_amount(base, rel, &amount, &min_tx_amount); - let expected_fee = DexFee::Standard(amount / 777u64.into()); - assert_eq!(expected_fee, actual_fee); - - let base = "KMD"; - let rel = "ETH"; - let amount = 1.into(); - let actual_fee = dex_fee_amount(base, rel, &amount, &min_tx_amount); - let expected_fee = amount.clone() * (9, 7770).into() * MmNumber::from("0.75"); - let expected_burn_amount = amount * (9, 7770).into() * MmNumber::from("0.25"); - assert_eq!(DexFee::with_burn(expected_fee, expected_burn_amount), actual_fee); - - // check the case when KMD taker fee is close to dust - let base = "KMD"; - let rel = "BTC"; - let amount = (1001 * 777, 90000000).into(); - let min_tx_amount = "0.00001".into(); - let actual_fee = dex_fee_amount(base, rel, &amount, &min_tx_amount); - assert_eq!( - DexFee::WithBurn { - fee_amount: "0.00001".into(), - burn_amount: "0.00000001".into() - }, - actual_fee - ); - - let base = "BTC"; - let rel = "KMD"; - let amount = 1.into(); - let actual_fee = dex_fee_amount(base, rel, &amount, &min_tx_amount); - let expected_fee = DexFee::Standard(amount * (9, 7770).into()); - assert_eq!(expected_fee, actual_fee); - - let base = "BTC"; - let rel = "KMD"; - let amount: MmNumber = "0.001".parse::().unwrap().into(); - let actual_fee = dex_fee_amount(base, rel, &amount, &min_tx_amount); - assert_eq!(DexFee::Standard(min_tx_amount), actual_fee); - } + use mocktopus::mocking::*; #[test] fn test_lp_atomic_locktime() { @@ -2404,49 +2309,78 @@ mod lp_swap_tests { std::env::set_var("MYCOIN_FEE_DISCOUNT", ""); let kmd = coins::TestCoin::new("KMD"); - let (kmd_taker_fee, kmd_burn_amount) = match dex_fee_amount_from_taker_coin(&kmd, "", &MmNumber::from(6150)) { - DexFee::Standard(_) => panic!("Wrong variant returned for KMD from `dex_fee_amount_from_taker_coin`."), - DexFee::WithBurn { - fee_amount, - burn_amount, - } => (fee_amount, burn_amount), - }; + TestCoin::should_burn_dex_fee.mock_safe(|_| MockResult::Return(true)); + let (kmd_fee_amount, kmd_burn_amount) = + match DexFee::new_from_taker_coin(&kmd, "ETH", &MmNumber::from(6150), None) { + DexFee::Standard(_) | DexFee::NoFee => { + panic!("Wrong variant returned for KMD from `DexFee::new_from_taker_coin`.") + }, + DexFee::WithBurn { + fee_amount, + burn_amount, + .. + } => (fee_amount, burn_amount), + }; + TestCoin::should_burn_dex_fee.clear_mock(); let mycoin = coins::TestCoin::new("MYCOIN"); - let mycoin_taker_fee = match dex_fee_amount_from_taker_coin(&mycoin, "", &MmNumber::from(6150)) { - DexFee::Standard(t) => t, - DexFee::WithBurn { .. } => { - panic!("Wrong variant returned for MYCOIN from `dex_fee_amount_from_taker_coin`.") - }, - }; + TestCoin::should_burn_dex_fee.mock_safe(|_| MockResult::Return(true)); + let (mycoin_fee_amount, mycoin_burn_amount) = + match DexFee::new_from_taker_coin(&mycoin, "ETH", &MmNumber::from(6150), None) { + DexFee::Standard(_) | DexFee::NoFee => { + panic!("Wrong variant returned for MYCOIN from `DexFee::new_from_taker_coin`.") + }, + DexFee::WithBurn { + fee_amount, + burn_amount, + .. + } => (fee_amount, burn_amount), + }; + TestCoin::should_burn_dex_fee.clear_mock(); - let expected_mycoin_taker_fee = &kmd_taker_fee / &MmNumber::from("0.75"); - let expected_kmd_burn_amount = &mycoin_taker_fee - &kmd_taker_fee; + let expected_mycoin_total_fee = &kmd_fee_amount / &MmNumber::from("0.75"); + let expected_kmd_burn_amount = &expected_mycoin_total_fee - &kmd_fee_amount; - assert_eq!(expected_mycoin_taker_fee, mycoin_taker_fee); + assert_eq!(kmd_fee_amount, mycoin_fee_amount); assert_eq!(expected_kmd_burn_amount, kmd_burn_amount); + // assuming for TestCoin dust is zero + assert_eq!(mycoin_burn_amount, kmd_burn_amount); } #[test] - fn test_dex_fee_amount_from_taker_coin_discount() { + fn test_dex_fee_from_taker_coin_discount() { std::env::set_var("MYCOIN_FEE_DISCOUNT", ""); let mycoin = coins::TestCoin::new("MYCOIN"); - let mycoin_taker_fee = match dex_fee_amount_from_taker_coin(&mycoin, "", &MmNumber::from(6150)) { - DexFee::Standard(t) => t, - DexFee::WithBurn { .. } => { - panic!("Wrong variant returned for MYCOIN from `dex_fee_amount_from_taker_coin`.") - }, - }; + TestCoin::should_burn_dex_fee.mock_safe(|_| MockResult::Return(true)); + let (mycoin_taker_fee, mycoin_burn_amount) = + match DexFee::new_from_taker_coin(&mycoin, "", &MmNumber::from(6150), None) { + DexFee::Standard(_) | DexFee::NoFee => { + panic!("Wrong variant returned for MYCOIN from `DexFee::new_from_taker_coin`.") + }, + DexFee::WithBurn { + fee_amount, + burn_amount, + .. + } => (fee_amount, burn_amount), + }; + TestCoin::should_burn_dex_fee.clear_mock(); let testcoin = coins::TestCoin::default(); - let testcoin_taker_fee = match dex_fee_amount_from_taker_coin(&testcoin, "", &MmNumber::from(6150)) { - DexFee::Standard(t) => t, - DexFee::WithBurn { .. } => { - panic!("Wrong variant returned for TEST coin from `dex_fee_amount_from_taker_coin`.") - }, - }; - + TestCoin::should_burn_dex_fee.mock_safe(|_| MockResult::Return(true)); + let (testcoin_taker_fee, testcoin_burn_amount) = + match DexFee::new_from_taker_coin(&testcoin, "", &MmNumber::from(6150), None) { + DexFee::Standard(_) | DexFee::NoFee => { + panic!("Wrong variant returned for TEST coin from `DexFee::new_from_taker_coin`.") + }, + DexFee::WithBurn { + fee_amount, + burn_amount, + .. + } => (fee_amount, burn_amount), + }; + TestCoin::should_burn_dex_fee.clear_mock(); assert_eq!(testcoin_taker_fee * MmNumber::from("0.90"), mycoin_taker_fee); + assert_eq!(testcoin_burn_amount * MmNumber::from("0.90"), mycoin_burn_amount); } } diff --git a/mm2src/mm2_main/src/lp_swap/maker_swap.rs b/mm2src/mm2_main/src/lp_swap/maker_swap.rs index d5c8bf2582..b467f5f0bf 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap.rs @@ -4,13 +4,13 @@ use super::pubkey_banning::ban_pubkey_on_failed_swap; use super::swap_lock::{SwapLock, SwapLockOps}; use super::trade_preimage::{TradePreimageRequest, TradePreimageRpcError, TradePreimageRpcResult}; use super::{broadcast_my_swap_status, broadcast_p2p_tx_msg, broadcast_swap_msg_every, - check_other_coin_balance_for_swap, detect_secret_hash_algo, dex_fee_amount_from_taker_coin, - get_locked_amount, recv_swap_msg, swap_topic, taker_payment_spend_deadline, tx_helper_topic, - wait_for_maker_payment_conf_until, AtomicSwap, LockedAmount, MySwapInfo, NegotiationDataMsg, - NegotiationDataV2, NegotiationDataV3, RecoveredSwap, RecoveredSwapAction, SavedSwap, SavedSwapIo, - SavedTradeFee, SecretHashAlgo, SwapConfirmationsSettings, SwapError, SwapMsg, SwapPubkeys, SwapTxDataMsg, - SwapsContext, TransactionIdentifier, INCLUDE_REFUND_FEE, NO_REFUND_FEE, TAKER_FEE_VALIDATION_ATTEMPTS, - TAKER_FEE_VALIDATION_RETRY_DELAY_SECS, WAIT_CONFIRM_INTERVAL_SEC}; + check_other_coin_balance_for_swap, detect_secret_hash_algo, get_locked_amount, recv_swap_msg, swap_topic, + taker_payment_spend_deadline, tx_helper_topic, wait_for_maker_payment_conf_until, AtomicSwap, + LockedAmount, MySwapInfo, NegotiationDataMsg, NegotiationDataV2, NegotiationDataV3, RecoveredSwap, + RecoveredSwapAction, SavedSwap, SavedSwapIo, SavedTradeFee, SecretHashAlgo, SwapConfirmationsSettings, + SwapError, SwapMsg, SwapPubkeys, SwapTxDataMsg, SwapsContext, TransactionIdentifier, INCLUDE_REFUND_FEE, + NO_REFUND_FEE, TAKER_FEE_VALIDATION_ATTEMPTS, TAKER_FEE_VALIDATION_RETRY_DELAY_SECS, + WAIT_CONFIRM_INTERVAL_SEC}; use crate::lp_dispatcher::{DispatcherContext, LpEvents}; use crate::lp_network::subscribe_to_topic; use crate::lp_ordermatch::MakerOrderBuilder; @@ -18,12 +18,14 @@ use crate::lp_swap::swap_events::{SwapStatusEvent, SwapStatusStreamer}; use crate::lp_swap::swap_v2_common::mark_swap_as_finished; use crate::lp_swap::{broadcast_swap_message, taker_payment_spend_duration, MAX_STARTED_AT_DIFF}; use coins::lp_price::fetch_swap_coins_price; -use coins::{CanRefundHtlc, CheckIfMyPaymentSentArgs, ConfirmPaymentInput, FeeApproxStage, FoundSwapTxSpend, MmCoin, - MmCoinEnum, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, RefundPaymentArgs, - SearchForSwapTxSpendInput, SendPaymentArgs, SpendPaymentArgs, SwapTxTypeWithSecretHash, TradeFee, - TradePreimageValue, TransactionEnum, ValidateFeeArgs, ValidatePaymentInput, WatcherReward}; +#[cfg(feature = "run-docker-tests")] +use coins::TEST_BURN_ADDR_RAW_PUBKEY; +use coins::{CanRefundHtlc, CheckIfMyPaymentSentArgs, ConfirmPaymentInput, DexFee, FeeApproxStage, FoundSwapTxSpend, + MmCoin, MmCoinEnum, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, + RefundPaymentArgs, SearchForSwapTxSpendInput, SendPaymentArgs, SpendPaymentArgs, SwapTxTypeWithSecretHash, + TradeFee, TradePreimageValue, TransactionEnum, ValidateFeeArgs, ValidatePaymentInput, WatcherReward}; use common::log::{debug, error, info, warn}; -use common::{bits256, executor::Timer, now_ms, DEX_FEE_ADDR_RAW_PUBKEY}; +use common::{bits256, executor::Timer, now_ms}; use common::{now_sec, wait_until_sec}; use crypto::privkey::SerializableSecp256k1Keypair; use crypto::CryptoCtx; @@ -171,8 +173,10 @@ pub struct MakerSwapData { #[serde(skip_serializing_if = "Option::is_none")] pub taker_coin_swap_contract_address: Option, /// Temporary pubkey used in HTLC redeem script when applicable for maker coin + /// Note: it's temporary for zcoin. For other coins it's currently obtained from iguana key or HD wallet activated key pub maker_coin_htlc_pubkey: Option, /// Temporary pubkey used in HTLC redeem script when applicable for taker coin + /// Note: it's temporary for zcoin. For other coins it's currently obtained from iguana key or HD wallet activated key pub taker_coin_htlc_pubkey: Option, /// Temporary privkey used to sign P2P messages when applicable pub p2p_privkey: Option, @@ -474,6 +478,13 @@ impl MakerSwap { } async fn start(&self) -> Result<(Option, Vec), String> { + #[cfg(feature = "run-docker-tests")] + if let Ok(env_pubkey) = std::env::var("TEST_BURN_ADDR_RAW_PUBKEY") { + unsafe { + TEST_BURN_ADDR_RAW_PUBKEY = Some(hex::decode(env_pubkey).expect("valid hex")); + } + } + // do not use self.r().data here as it is not initialized at this step yet let preimage_value = TradePreimageValue::Exact(self.maker_amount.clone()); let stage = FeeApproxStage::StartSwap; @@ -585,14 +596,15 @@ impl MakerSwap { async fn negotiate(&self) -> Result<(Option, Vec), String> { let negotiation_data = self.get_my_negotiation_data(); - let maker_negotiation_data = SwapMsg::Negotiation(negotiation_data); + let maker_negotiation_msg = SwapMsg::Negotiation(negotiation_data); + const NEGOTIATION_TIMEOUT_SEC: u64 = 90; - debug!("Sending maker negotiation data {:?}", maker_negotiation_data); + debug!("Sending maker negotiation data: {:?}", maker_negotiation_msg); let send_abort_handle = broadcast_swap_msg_every( self.ctx.clone(), swap_topic(&self.uuid), - maker_negotiation_data, + maker_negotiation_msg, NEGOTIATION_TIMEOUT_SEC as f64 / 6., self.p2p_privkey, ); @@ -612,6 +624,7 @@ impl MakerSwap { }, }; drop(send_abort_handle); + let time_dif = self.r().data.started_at.abs_diff(taker_data.started_at()); if time_dif > MAX_STARTED_AT_DIFF { self.broadcast_negotiated_false(); @@ -747,6 +760,29 @@ impl MakerSwap { }; swap_events.push(MakerSwapEvent::MakerPaymentInstructionsReceived(instructions)); + let taker_amount = MmNumber::from(self.taker_amount.clone()); + let dex_fee = DexFee::new_from_taker_coin( + self.taker_coin.deref(), + &self.r().data.maker_coin, + &taker_amount, + Some(self.r().other_taker_coin_htlc_pub.to_vec().as_ref()), + ); + debug!( + "MakerSwap::wait_taker_fee dex_fee={:?} my_taker_coin_htlc_pub={}", + dex_fee, + hex::encode(self.my_taker_coin_htlc_pub().0) + ); + + if matches!(dex_fee, DexFee::NoFee) { + info!("Taker fee is not expected for dex taker"); + let fee_ident = TransactionIdentifier { + tx_hex: BytesJson::from(vec![]), + tx_hash: BytesJson::from(vec![]), + }; + swap_events.push(MakerSwapEvent::TakerFeeValidated(fee_ident)); + return Ok((Some(MakerSwapCommand::SendPayment), swap_events)); + } + let taker_fee = match self.taker_coin.tx_enum_from_bytes(payload.data()) { Ok(tx) => tx, Err(e) => { @@ -759,8 +795,6 @@ impl MakerSwap { let hash = taker_fee.tx_hash_as_bytes(); info!("Taker fee tx {:02x}", hash); - let taker_amount = MmNumber::from(self.taker_amount.clone()); - let dex_fee = dex_fee_amount_from_taker_coin(self.taker_coin.deref(), &self.r().data.maker_coin, &taker_amount); let other_taker_coin_htlc_pub = self.r().other_taker_coin_htlc_pub; let taker_coin_start_block = self.r().data.taker_coin_start_block; @@ -771,7 +805,6 @@ impl MakerSwap { .validate_fee(ValidateFeeArgs { fee_tx: &taker_fee, expected_sender: &*other_taker_coin_htlc_pub, - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &dex_fee, min_block_number: taker_coin_start_block, uuid: self.uuid.as_bytes(), diff --git a/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs b/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs index 8b2a05f935..2120169f48 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs @@ -9,6 +9,8 @@ use crate::lp_swap::{broadcast_swap_v2_msg_every, check_balance_for_maker_swap, use crate::lp_swap::{swap_v2_pb::*, NO_REFUND_FEE}; use async_trait::async_trait; use bitcrypto::{dhash160, sha256}; +#[cfg(feature = "run-docker-tests")] +use coins::TEST_BURN_ADDR_RAW_PUBKEY; use coins::{CanRefundHtlc, ConfirmPaymentInput, DexFee, FeeApproxStage, FundingTxSpend, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, MakerCoinSwapOpsV2, MmCoin, ParseCoinAssocTypes, RefundMakerPaymentSecretArgs, RefundMakerPaymentTimelockArgs, SearchForFundingSpendErr, SendMakerPaymentArgs, SwapTxTypeWithSecretHash, @@ -16,7 +18,7 @@ use coins::{CanRefundHtlc, ConfirmPaymentInput, DexFee, FeeApproxStage, FundingT use common::executor::abortable_queue::AbortableQueue; use common::executor::{AbortableSystem, Timer}; use common::log::{debug, error, info, warn}; -use common::{now_sec, Future01CompatExt, DEX_FEE_ADDR_RAW_PUBKEY}; +use common::{now_sec, Future01CompatExt}; use crypto::privkey::SerializableSecp256k1Keypair; use keys::KeyPair; use mm2_core::mm_ctx::MmArc; @@ -374,8 +376,6 @@ pub struct MakerSwapStateMachine Vec { self.secret_hash() } + + fn dex_fee(&self, taker_pub: Option<&[u8]>) -> DexFee { + DexFee::new_from_taker_coin( + &self.taker_coin, + self.maker_coin.ticker(), + &self.taker_volume, + taker_pub, + ) + } } #[async_trait] @@ -437,8 +446,8 @@ impl Result<(RestoredMachine, Box>), Self::RecreateError> { + #[cfg(feature = "run-docker-tests")] + if let Ok(env_pubkey) = std::env::var("TEST_BURN_ADDR_RAW_PUBKEY") { + unsafe { + TEST_BURN_ADDR_RAW_PUBKEY = Some(hex::decode(env_pubkey).expect("valid hex")); + } + } + if repr.events.is_empty() { return MmError::err(SwapRecreateError::ReprEventsEmpty); } @@ -617,12 +633,6 @@ impl MmNumber::default() { - DexFee::with_burn(repr.dex_fee_amount, repr.dex_fee_burn) - } else { - DexFee::Standard(repr.dex_fee_amount) - }; - let machine = MakerSwapStateMachine { ctx: storage.ctx.clone(), abortable_system: storage @@ -640,7 +650,6 @@ impl; async fn on_changed(self: Box, state_machine: &mut Self::StateMachine) -> StateResult { + #[cfg(feature = "run-docker-tests")] + if let Ok(env_pubkey) = std::env::var("TEST_BURN_ADDR_RAW_PUBKEY") { + unsafe { + TEST_BURN_ADDR_RAW_PUBKEY = Some(hex::decode(env_pubkey).expect("valid hex")); + } + } + let maker_coin_start_block = match state_machine.maker_coin.current_block().compat().await { Ok(b) => b, Err(e) => { @@ -1162,7 +1178,6 @@ impl, state_machine: &mut Self::StateMachine) -> StateResult { let unique_data = state_machine.unique_data(); - let validation_args = ValidateTakerFundingArgs { funding_tx: &self.taker_funding, payment_time_lock: self.negotiation_data.taker_payment_locktime, @@ -1170,7 +1185,7 @@ impl, /// Temporary pubkey used in HTLC redeem script when applicable for maker coin + /// Note: it's temporary for zcoin. For other coins it's currently obtained from iguana key or HD wallet activated key pub maker_coin_htlc_pubkey: Option, /// Temporary pubkey used in HTLC redeem script when applicable for taker coin + /// Note: it's temporary for zcoin. For other coins it's currently obtained from iguana key or HD wallet activated key pub taker_coin_htlc_pubkey: Option, /// Temporary privkey used to sign P2P messages when applicable pub p2p_privkey: Option, @@ -979,7 +983,6 @@ impl TakerSwap { let equal = r.data.maker_coin_htlc_pubkey == r.data.taker_coin_htlc_pubkey; let same_as_persistent = r.data.maker_coin_htlc_pubkey == Some(r.data.my_persistent_pub); - if equal && same_as_persistent { NegotiationDataMsg::V2(NegotiationDataV2 { started_at: r.data.started_at, @@ -1038,20 +1041,39 @@ impl TakerSwap { } async fn start(&self) -> Result<(Option, Vec), String> { + #[cfg(feature = "run-docker-tests")] + if let Ok(env_pubkey) = std::env::var("TEST_BURN_ADDR_RAW_PUBKEY") { + unsafe { + TEST_BURN_ADDR_RAW_PUBKEY = Some(hex::decode(env_pubkey).expect("valid hex")); + } + } + // do not use self.r().data here as it is not initialized at this step yet let stage = FeeApproxStage::StartSwap; - let dex_fee = - dex_fee_amount_from_taker_coin(self.taker_coin.deref(), self.maker_coin.ticker(), &self.taker_amount); + let dex_fee = DexFee::new_from_taker_coin( + self.taker_coin.deref(), + self.maker_coin.ticker(), + &self.taker_amount, + Some(&self.my_taker_coin_htlc_pub().0), + ); let preimage_value = TradePreimageValue::Exact(self.taker_amount.to_decimal()); - let fee_to_send_dex_fee_fut = self.taker_coin.get_fee_to_send_taker_fee(dex_fee.clone(), stage); - let fee_to_send_dex_fee = match fee_to_send_dex_fee_fut.await { - Ok(fee) => fee, - Err(e) => { - return Ok((Some(TakerSwapCommand::Finish), vec![TakerSwapEvent::StartFailed( - ERRL!("!taker_coin.get_fee_to_send_taker_fee {}", e).into(), - )])) - }, + let fee_to_send_dex_fee = if matches!(dex_fee, DexFee::NoFee) { + TradeFee { + coin: self.taker_coin.ticker().to_owned(), + amount: MmNumber::from(0), + paid_from_trading_vol: false, + } + } else { + let fee_to_send_dex_fee_fut = self.taker_coin.get_fee_to_send_taker_fee(dex_fee.clone(), stage); + match fee_to_send_dex_fee_fut.await { + Ok(fee) => fee, + Err(e) => { + return Ok((Some(TakerSwapCommand::Finish), vec![TakerSwapEvent::StartFailed( + ERRL!("!taker_coin.get_fee_to_send_taker_fee {}", e).into(), + )])) + }, + } }; let get_sender_trade_fee_fut = self .taker_coin @@ -1267,15 +1289,17 @@ impl TakerSwap { taker_coin_swap_contract_bytes, ); - let taker_data = SwapMsg::NegotiationReply(my_negotiation_data); + let (topic, taker_data) = (swap_topic(&self.uuid), SwapMsg::NegotiationReply(my_negotiation_data)); + debug!("Sending taker negotiation data {:?}", taker_data); let send_abort_handle = broadcast_swap_msg_every( self.ctx.clone(), - swap_topic(&self.uuid), + topic, taker_data, NEGOTIATE_TIMEOUT_SEC as f64 / 6., self.p2p_privkey, ); + let recv_fut = recv_swap_msg( self.ctx.clone(), |store| store.negotiated.take(), @@ -1321,12 +1345,25 @@ impl TakerSwap { TakerSwapEvent::TakerFeeSendFailed(ERRL!("Timeout {} > {}", now, expire_at).into()), ])); } - - let fee_amount = - dex_fee_amount_from_taker_coin(self.taker_coin.deref(), &self.r().data.maker_coin, &self.taker_amount); + let dex_fee = DexFee::new_from_taker_coin( + self.taker_coin.deref(), + &self.r().data.maker_coin, + &self.taker_amount, + Some(&self.my_taker_coin_htlc_pub().0), + ); + if matches!(dex_fee, DexFee::NoFee) { + info!("Taker fee tx not sent for dex taker"); + let empty_tx_ident = TransactionIdentifier { + tx_hex: BytesJson::from(vec![]), + tx_hash: BytesJson::from(vec![]), + }; + return Ok((Some(TakerSwapCommand::WaitForMakerPayment), vec![ + TakerSwapEvent::TakerFeeSent(empty_tx_ident), + ])); + } let fee_tx = self .taker_coin - .send_taker_fee(&DEX_FEE_ADDR_RAW_PUBKEY, fee_amount, self.uuid.as_bytes(), expire_at) + .send_taker_fee(dex_fee, self.uuid.as_bytes(), expire_at) .await; let transaction = match fee_tx { Ok(t) => t, @@ -2410,13 +2447,17 @@ impl AtomicSwap for TakerSwap { let mut result = Vec::new(); // if taker fee is not sent yet it must be virtually locked - let taker_fee_amount = - dex_fee_amount_from_taker_coin(self.taker_coin.deref(), &self.r().data.maker_coin, &self.taker_amount); + let taker_fee = DexFee::new_from_taker_coin( + self.taker_coin.deref(), + &self.r().data.maker_coin, + &self.taker_amount, + Some(&self.my_taker_coin_htlc_pub().0), + ); let trade_fee = self.r().data.fee_to_send_taker_fee.clone().map(TradeFee::from); if self.r().taker_fee.is_none() { result.push(LockedAmount { coin: self.taker_coin.ticker().to_owned(), - amount: taker_fee_amount.total_spend_amount(), + amount: taker_fee.total_spend_amount(), trade_fee, }); } @@ -2479,7 +2520,8 @@ pub async fn check_balance_for_taker_swap( let params = match prepared_params { Some(params) => params, None => { - let dex_fee = dex_fee_amount_from_taker_coin(my_coin, other_coin.ticker(), &volume); + // Use None as taker_pubkey is okay because we just need to calculate max swap amount + let dex_fee = DexFee::new_from_taker_coin(my_coin, other_coin.ticker(), &volume, None); let fee_to_send_dex_fee = my_coin .get_fee_to_send_taker_fee(dex_fee.clone(), stage) .await @@ -2567,15 +2609,21 @@ pub async fn taker_swap_trade_preimage( TakerAction::Buy => rel_amount.clone(), }; - let dex_amount = dex_fee_amount_from_taker_coin(my_coin.deref(), other_coin_ticker, &my_coin_volume); + let dummy_unique_data = vec![]; + let dex_fee = DexFee::new_from_taker_coin( + my_coin.deref(), + other_coin_ticker, + &my_coin_volume, + Some(&my_coin.derive_htlc_pubkey(&dummy_unique_data)), // use dummy_unique_data because we need only the permanent pubkey here (not derived from the unique data) + ); let taker_fee = TradeFee { coin: my_coin_ticker.to_owned(), - amount: dex_amount.total_spend_amount(), + amount: dex_fee.total_spend_amount(), paid_from_trading_vol: false, }; let fee_to_send_taker_fee = my_coin - .get_fee_to_send_taker_fee(dex_amount.clone(), stage) + .get_fee_to_send_taker_fee(dex_fee.clone(), stage) .await .mm_err(|e| TradePreimageRpcError::from_trade_preimage_error(e, my_coin_ticker))?; @@ -2591,7 +2639,7 @@ pub async fn taker_swap_trade_preimage( .mm_err(|e| TradePreimageRpcError::from_trade_preimage_error(e, other_coin_ticker))?; let prepared_params = TakerSwapPreparedParams { - dex_fee: dex_amount.total_spend_amount(), + dex_fee: dex_fee.total_spend_amount(), fee_to_send_dex_fee: fee_to_send_taker_fee.clone(), taker_payment_trade_fee: my_coin_trade_fee.clone(), maker_payment_spend_trade_fee: other_coin_trade_fee.clone(), @@ -2713,7 +2761,8 @@ pub async fn calc_max_taker_vol( let max_vol = if my_coin == max_trade_fee.coin { // second case let max_possible_2 = &max_possible - &max_trade_fee.amount; - let max_dex_fee = dex_fee_amount_from_taker_coin(coin.deref(), other_coin, &max_possible_2); + // Use None as taker_pubkey is we need just to calc max volume + let max_dex_fee = DexFee::new_from_taker_coin(coin.deref(), other_coin, &max_possible_2, None); let max_fee_to_send_taker_fee = coin .get_fee_to_send_taker_fee(max_dex_fee.clone(), stage) .await @@ -2758,7 +2807,7 @@ pub fn max_taker_vol_from_available( rel: &str, min_tx_amount: &MmNumber, ) -> Result> { - let dex_fee_rate = dex_fee_rate(base, rel); + let dex_fee_rate = DexFee::dex_fee_rate(base, rel); let threshold_coef = &(&MmNumber::from(1) + &dex_fee_rate) / &dex_fee_rate; let max_vol = if available > min_tx_amount * &threshold_coef { available / (MmNumber::from(1) + dex_fee_rate) @@ -2778,7 +2827,7 @@ pub fn max_taker_vol_from_available( #[cfg(all(test, not(target_arch = "wasm32")))] mod taker_swap_tests { use super::*; - use crate::lp_swap::{dex_fee_amount, get_locked_amount_by_other_swaps}; + use crate::lp_swap::get_locked_amount_by_other_swaps; use coins::eth::{addr_from_str, signed_eth_tx_from_bytes, SignedEthTx}; use coins::utxo::UtxoTx; use coins::{FoundSwapTxSpend, MarketCoinOps, MmCoin, SwapOps, TestCoin}; @@ -3203,7 +3252,10 @@ mod taker_swap_tests { let max_taker_vol = max_taker_vol_from_available(available.clone(), "RICK", "MORTY", &min_tx_amount) .expect("!max_taker_vol_from_available"); - let dex_fee = dex_fee_amount(base, "MORTY", &max_taker_vol, &min_tx_amount).fee_amount(); + let coin = TestCoin::new(base); + let mock_min_tx_amount = min_tx_amount.clone(); + TestCoin::min_tx_amount.mock_safe(move |_| MockResult::Return(mock_min_tx_amount.clone().into())); + let dex_fee = DexFee::new_from_taker_coin(&coin, "MORTY", &max_taker_vol, None).total_spend_amount(); assert!(min_tx_amount < dex_fee); assert!(min_tx_amount <= max_taker_vol); assert_eq!(max_taker_vol + dex_fee, available); @@ -3223,7 +3275,11 @@ mod taker_swap_tests { let base = if is_kmd { "KMD" } else { "RICK" }; let max_taker_vol = max_taker_vol_from_available(available.clone(), base, "MORTY", &min_tx_amount) .expect("!max_taker_vol_from_available"); - let dex_fee = dex_fee_amount(base, "MORTY", &max_taker_vol, &min_tx_amount).fee_amount(); + + let coin = TestCoin::new(base); + let mock_min_tx_amount = min_tx_amount.clone(); + TestCoin::min_tx_amount.mock_safe(move |_| MockResult::Return(mock_min_tx_amount.clone().into())); + let dex_fee = DexFee::new_from_taker_coin(&coin, "MORTY", &max_taker_vol, None).fee_amount(); // returns Standard dex_fee (default for TestCoin) println!( "available={:?} max_taker_vol={:?} dex_fee={:?}", available.to_decimal(), diff --git a/mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs b/mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs index 8801d32cfc..d7ccd57178 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs @@ -9,6 +9,8 @@ use crate::lp_swap::{broadcast_swap_v2_msg_every, check_balance_for_taker_swap, use crate::lp_swap::{swap_v2_pb::*, NO_REFUND_FEE}; use async_trait::async_trait; use bitcrypto::{dhash160, sha256}; +#[cfg(feature = "run-docker-tests")] +use coins::TEST_BURN_ADDR_RAW_PUBKEY; use coins::{CanRefundHtlc, ConfirmPaymentInput, DexFee, FeeApproxStage, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, MakerCoinSwapOpsV2, MmCoin, ParseCoinAssocTypes, RefundFundingSecretArgs, RefundTakerPaymentArgs, SendTakerFundingArgs, SpendMakerPaymentArgs, SwapTxTypeWithSecretHash, @@ -17,7 +19,7 @@ use coins::{CanRefundHtlc, ConfirmPaymentInput, DexFee, FeeApproxStage, GenTaker use common::executor::abortable_queue::AbortableQueue; use common::executor::{AbortableSystem, Timer}; use common::log::{debug, error, info, warn}; -use common::{Future01CompatExt, DEX_FEE_ADDR_RAW_PUBKEY}; +use common::Future01CompatExt; use crypto::privkey::SerializableSecp256k1Keypair; use keys::KeyPair; use mm2_core::mm_ctx::MmArc; @@ -400,8 +402,6 @@ pub struct TakerSwapStateMachine sha256(self.taker_secret.as_slice()).take().into(), } } + + fn dex_fee(&self) -> DexFee { + let taker_pub = self.taker_coin.taker_pubkey_bytes(); // for dex fee calculation we need only permanent (non-derived for HTLC) taker pubkey here + DexFee::new_from_taker_coin( + &self.taker_coin, + self.maker_coin.ticker(), + &self.taker_volume, + taker_pub.as_deref(), + ) + } } #[async_trait] @@ -467,13 +477,13 @@ impl Result<(RestoredMachine, Box>), Self::RecreateError> { + #[cfg(feature = "run-docker-tests")] + if let Ok(env_pubkey) = std::env::var("TEST_BURN_ADDR_RAW_PUBKEY") { + unsafe { + TEST_BURN_ADDR_RAW_PUBKEY = Some(hex::decode(env_pubkey).expect("valid hex")); + } + } + if repr.events.is_empty() { return MmError::err(SwapRecreateError::ReprEventsEmpty); } @@ -733,12 +750,6 @@ impl MmNumber::default() { - DexFee::with_burn(repr.dex_fee_amount, repr.dex_fee_burn) - } else { - DexFee::Standard(repr.dex_fee_amount) - }; - let machine = TakerSwapStateMachine { ctx: storage.ctx.clone(), abortable_system: storage @@ -753,7 +764,6 @@ impl; async fn on_changed(self: Box, state_machine: &mut Self::StateMachine) -> StateResult { + #[cfg(feature = "run-docker-tests")] + if let Ok(env_pubkey) = std::env::var("TEST_BURN_ADDR_RAW_PUBKEY") { + unsafe { + TEST_BURN_ADDR_RAW_PUBKEY = Some(hex::decode(env_pubkey).expect("valid hex")); + } + } + let maker_coin_start_block = match state_machine.maker_coin.current_block().compat().await { Ok(b) => b, Err(e) => { @@ -931,8 +948,8 @@ impl = Cell::new(false); +} + pub const UTXO_ASSET_DOCKER_IMAGE: &str = "docker.io/artempikulin/testblockchain"; pub const UTXO_ASSET_DOCKER_IMAGE_WITH_TAG: &str = "docker.io/artempikulin/testblockchain:multiarch"; pub const GETH_DOCKER_IMAGE: &str = "docker.io/ethereum/client-go"; @@ -880,7 +887,17 @@ pub fn trade_base_rel((base, rel): (&str, &str)) { let bob_priv_key = generate_and_fill_priv_key(base); let alice_priv_key = generate_and_fill_priv_key(rel); + let alice_pubkey_str = hex::encode( + key_pair_from_secret(&alice_priv_key) + .expect("valid test key pair") + .public() + .to_vec(), + ); + let mut envs = vec![]; + if SET_BURN_PUBKEY_TO_ALICE.get() { + envs.push(("TEST_BURN_ADDR_RAW_PUBKEY", alice_pubkey_str.as_str())); + } let confpath = unsafe { QTUM_CONF_PATH.as_ref().expect("Qtum config is not set yet") }; let coins = json! ([ eth_dev_conf(), @@ -891,12 +908,12 @@ pub fn trade_base_rel((base, rel): (&str, &str)) { {"coin":"MYCOIN1","asset":"MYCOIN1","required_confirmations":0,"txversion":4,"overwintered":1,"txfee":1000,"protocol":{"type":"UTXO"}}, // TODO: check if we should fix protocol "type":"UTXO" to "QTUM" for this and other QTUM coin tests. // Maybe we should use a different coin for "UTXO" protocol and make new tests for "QTUM" protocol - {"coin":"QTUM","asset":"QTUM","required_confirmations":0,"decimals":8,"pubtype":120,"p2shtype":110,"wiftype":128,"segwit":true,"txfee":0,"txfee_volatility_percent":0.1, + {"coin":"QTUM","asset":"QTUM","required_confirmations":0,"decimals":8,"pubtype":120,"p2shtype":110,"wiftype":128,"segwit":true,"txfee":0,"txfee_volatility_percent":0.1, "dust":72800, "mm2":1,"network":"regtest","confpath":confpath,"protocol":{"type":"UTXO"},"bech32_hrp":"qcrt","address_format":{"format":"segwit"}}, {"coin":"FORSLP","asset":"FORSLP","required_confirmations":0,"txversion":4,"overwintered":1,"txfee":1000,"protocol":{"type":"BCH","protocol_data":{"slp_prefix":"slptest"}}}, {"coin":"ADEXSLP","protocol":{"type":"SLPTOKEN","protocol_data":{"decimals":8,"token_id":get_slp_token_id(),"platform":"FORSLP"}}} ]); - let mut mm_bob = MarketMakerIt::start( + let mut mm_bob = block_on(MarketMakerIt::start_with_envs( json! ({ "gui": "nogui", "netid": 9000, @@ -908,12 +925,13 @@ pub fn trade_base_rel((base, rel): (&str, &str)) { }), "pass".to_string(), None, - ) + envs.as_slice(), + )) .unwrap(); let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); block_on(mm_bob.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); - let mut mm_alice = MarketMakerIt::start( + let mut mm_alice = block_on(MarketMakerIt::start_with_envs( json! ({ "gui": "nogui", "netid": 9000, @@ -925,7 +943,8 @@ pub fn trade_base_rel((base, rel): (&str, &str)) { }), "pass".to_string(), None, - ) + envs.as_slice(), + )) .unwrap(); let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); block_on(mm_alice.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); diff --git a/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs b/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs index b4f074857f..ff7e6415fb 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs @@ -1,4 +1,5 @@ -use crate::docker_tests::docker_tests_common::{generate_utxo_coin_with_privkey, trade_base_rel, GETH_RPC_URL, MM_CTX}; +use crate::docker_tests::docker_tests_common::{generate_utxo_coin_with_privkey, trade_base_rel, GETH_RPC_URL, MM_CTX, + SET_BURN_PUBKEY_TO_ALICE}; use crate::docker_tests::eth_docker_tests::{erc20_coin_with_random_privkey, erc20_contract_checksum, fill_eth_erc20_with_private_key, swap_contract}; use crate::integration_tests_common::*; @@ -3892,6 +3893,13 @@ fn test_trade_base_rel_eth_erc20_coins() { trade_base_rel(("ETH", "ERC20DEV")); #[test] fn test_trade_base_rel_mycoin_mycoin1_coins() { trade_base_rel(("MYCOIN", "MYCOIN1")); } +// run swap with burn pubkey set to alice (no dex fee) +#[test] +fn test_trade_base_rel_mycoin_mycoin1_coins_burnkey_as_alice() { + SET_BURN_PUBKEY_TO_ALICE.set(true); + trade_base_rel(("MYCOIN", "MYCOIN1")); +} + fn withdraw_and_send( mm: &MarketMakerIt, coin: &str, diff --git a/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs b/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs index 5844459b9f..a503f758b2 100644 --- a/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs @@ -1753,7 +1753,6 @@ fn taker_send_approve_and_spend_eth() { }, }; - let dex_fee_pub = sepolia_taker_swap_v2(); let spend_args = GenTakerPaymentSpendArgs { taker_tx: &taker_approve_tx, time_lock: payment_time_lock, @@ -1761,7 +1760,6 @@ fn taker_send_approve_and_spend_eth() { maker_pub, maker_address: &maker_address, taker_pub, - dex_fee_pub: dex_fee_pub.as_bytes(), dex_fee, premium_amount: Default::default(), trading_amount, @@ -1864,7 +1862,6 @@ fn taker_send_approve_and_spend_erc20() { }, }; - let dex_fee_pub = sepolia_taker_swap_v2(); let spend_args = GenTakerPaymentSpendArgs { taker_tx: &taker_approve_tx, time_lock: payment_time_lock, @@ -1872,7 +1869,6 @@ fn taker_send_approve_and_spend_erc20() { maker_pub, maker_address: &maker_address, taker_pub, - dex_fee_pub: dex_fee_pub.as_bytes(), dex_fee, premium_amount: Default::default(), trading_amount, diff --git a/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs b/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs index cfbd2df664..a4953852b9 100644 --- a/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs @@ -6,17 +6,16 @@ use coins::utxo::qtum::{qtum_coin_with_priv_key, QtumCoin}; use coins::utxo::rpc_clients::UtxoRpcClientEnum; use coins::utxo::utxo_common::big_decimal_from_sat; use coins::utxo::{UtxoActivationParams, UtxoCommonOps}; -use coins::{CheckIfMyPaymentSentArgs, ConfirmPaymentInput, DexFee, FeeApproxStage, FoundSwapTxSpend, MarketCoinOps, - MmCoin, RefundPaymentArgs, SearchForSwapTxSpendInput, SendPaymentArgs, SpendPaymentArgs, SwapOps, - SwapTxTypeWithSecretHash, TradePreimageValue, TransactionEnum, ValidateFeeArgs, ValidatePaymentInput, - WaitForHTLCTxSpendArgs}; -use common::log::debug; +use coins::{CheckIfMyPaymentSentArgs, ConfirmPaymentInput, DexFee, DexFeeBurnDestination, FeeApproxStage, + FoundSwapTxSpend, MarketCoinOps, MmCoin, RefundPaymentArgs, SearchForSwapTxSpendInput, SendPaymentArgs, + SpendPaymentArgs, SwapOps, SwapTxTypeWithSecretHash, TradePreimageValue, TransactionEnum, ValidateFeeArgs, + ValidatePaymentInput, WaitForHTLCTxSpendArgs}; use common::{block_on_f01, temp_dir, DEX_FEE_ADDR_RAW_PUBKEY}; use crypto::Secp256k1Secret; use ethereum_types::H160; use http::StatusCode; use mm2_core::mm_ctx::{MmArc, MmCtxBuilder}; -use mm2_main::lp_swap::{dex_fee_amount, max_taker_vol_from_available}; +use mm2_main::lp_swap::max_taker_vol_from_available; use mm2_number::BigDecimal; use mm2_rpc::data::legacy::{CoinInitResponse, OrderbookResponse}; use mm2_test_helpers::structs::{trade_preimage_error, RpcErrorResponse, RpcSuccessResponse, TransactionDetails}; @@ -977,7 +976,7 @@ fn test_get_max_taker_vol_and_trade_with_dynamic_trade_fee(coin: QtumCoin, priv_ let coins = json! ([ {"coin":"MYCOIN","asset":"MYCOIN","txversion":4,"overwintered":1,"txfee":1000,"protocol":{"type":"UTXO"}}, {"coin":"QTUM","decimals":8,"pubtype":120,"p2shtype":110,"wiftype":128,"txfee":0,"txfee_volatility_percent":0.1, - "mm2":1,"mature_confirmations":500,"network":"regtest","confpath":confpath,"protocol":{"type":"UTXO"}}, + "mm2":1,"mature_confirmations":500,"network":"regtest","confpath":confpath,"protocol":{"type":"UTXO"}, "dust": 72800}, ]); let mut mm = MarketMakerIt::start( json! ({ @@ -1013,13 +1012,13 @@ fn test_get_max_taker_vol_and_trade_with_dynamic_trade_fee(coin: QtumCoin, priv_ )) .expect("!get_sender_trade_fee"); let max_trade_fee = max_trade_fee.amount.to_decimal(); - debug!("max_trade_fee: {}", max_trade_fee); + log!("max_trade_fee: {}", max_trade_fee); // - `max_possible_2 = balance - locked_amount - max_trade_fee`, where `locked_amount = 0` let max_possible_2 = &qtum_balance - &max_trade_fee; // - `max_dex_fee = dex_fee(max_possible_2)` - let max_dex_fee = dex_fee_amount("QTUM", "MYCOIN", &MmNumber::from(max_possible_2), &qtum_min_tx_amount); - debug!("max_dex_fee: {:?}", max_dex_fee.fee_amount().to_fraction()); + let max_dex_fee = DexFee::new_from_taker_coin(&coin, "MYCOIN", &MmNumber::from(max_possible_2), None); + log!("max_dex_fee: {:?}", max_dex_fee.fee_amount().to_fraction()); // - `max_fee_to_send_taker_fee = fee_to_send_taker_fee(max_dex_fee)` // `taker_fee` is sent using general withdraw, and the fee get be obtained from withdraw result @@ -1027,19 +1026,17 @@ fn test_get_max_taker_vol_and_trade_with_dynamic_trade_fee(coin: QtumCoin, priv_ block_on(coin.get_fee_to_send_taker_fee(max_dex_fee, FeeApproxStage::TradePreimage)) .expect("!get_fee_to_send_taker_fee"); let max_fee_to_send_taker_fee = max_fee_to_send_taker_fee.amount.to_decimal(); - debug!("max_fee_to_send_taker_fee: {}", max_fee_to_send_taker_fee); + log!("max_fee_to_send_taker_fee: {}", max_fee_to_send_taker_fee); // and then calculate `min_max_val = balance - locked_amount - max_trade_fee - max_fee_to_send_taker_fee - dex_fee(max_val)` using `max_taker_vol_from_available()` // where `available = balance - locked_amount - max_trade_fee - max_fee_to_send_taker_fee` let available = &qtum_balance - &max_trade_fee - &max_fee_to_send_taker_fee; - debug!("total_available: {}", available); - #[allow(clippy::redundant_clone)] // This is a false-possitive bug from clippy - let min_tx_amount = qtum_min_tx_amount.clone(); + log!("total_available: {}", available); let expected_max_taker_vol = - max_taker_vol_from_available(MmNumber::from(available), "QTUM", "MYCOIN", &min_tx_amount) + max_taker_vol_from_available(MmNumber::from(available), "QTUM", "MYCOIN", &qtum_min_tx_amount) .expect("max_taker_vol_from_available"); - let real_dex_fee = dex_fee_amount("QTUM", "MYCOIN", &expected_max_taker_vol, &qtum_min_tx_amount).fee_amount(); - debug!("real_max_dex_fee: {:?}", real_dex_fee.to_fraction()); + let real_dex_fee = DexFee::new_from_taker_coin(&coin, "MYCOIN", &expected_max_taker_vol, None).fee_amount(); + log!("real_max_dex_fee: {:?}", real_dex_fee.to_fraction()); // check if the actual max_taker_vol equals to the expected let rc = block_on(mm.rpc(&json! ({ @@ -1071,9 +1068,8 @@ fn test_get_max_taker_vol_and_trade_with_dynamic_trade_fee(coin: QtumCoin, priv_ let timelock = now_sec() - 200; let secret_hash = &[0; 20]; - let dex_fee = dex_fee_amount("QTUM", "MYCOIN", &expected_max_taker_vol, &qtum_min_tx_amount); - let _taker_fee_tx = - block_on(coin.send_taker_fee(&DEX_FEE_ADDR_RAW_PUBKEY, dex_fee, &[], timelock)).expect("!send_taker_fee"); + let dex_fee = DexFee::new_from_taker_coin(&coin, "MYCOIN", &expected_max_taker_vol, None); + let _taker_fee_tx = block_on(coin.send_taker_fee(dex_fee, &[], timelock)).expect("!send_taker_fee"); let taker_payment_args = SendPaymentArgs { time_lock_duration: 0, time_lock: timelock, @@ -1100,7 +1096,7 @@ fn test_get_max_taker_vol_and_trade_with_dynamic_trade_fee(coin: QtumCoin, priv_ /// Generate the Qtum coin with a random balance and start the `test_get_max_taker_vol_and_trade_with_dynamic_trade_fee` test. #[test] fn test_max_taker_vol_dynamic_trade_fee() { - wait_for_estimate_smart_fee(30).expect("!wait_for_estimate_smart_fee"); + wait_for_estimate_smart_fee(60).expect("!wait_for_estimate_smart_fee"); // generate QTUM coin with the dynamic fee and fill the wallet by 2 Qtums let (_ctx, coin, priv_key) = generate_qtum_coin_with_random_privkey("QTUM", 2.into(), Some(0)); let my_address = coin.my_address().expect("!my_address"); @@ -1127,7 +1123,7 @@ fn test_max_taker_vol_dynamic_trade_fee() { /// This test checks if the fee returned from `get_sender_trade_fee` should include the change output anyway. #[test] fn test_trade_preimage_fee_includes_change_output_anyway() { - wait_for_estimate_smart_fee(30).expect("!wait_for_estimate_smart_fee"); + wait_for_estimate_smart_fee(60).expect("!wait_for_estimate_smart_fee"); // generate QTUM coin with the dynamic fee and fill the wallet by 2 Qtums let (_ctx, coin, priv_key) = generate_qtum_coin_with_random_privkey("QTUM", 2.into(), Some(0)); let my_address = coin.my_address().expect("!my_address"); @@ -1143,7 +1139,7 @@ fn test_trade_preimage_fee_includes_change_output_anyway() { } #[test] fn test_trade_preimage_not_sufficient_base_coin_balance_for_ticker() { - wait_for_estimate_smart_fee(30).expect("!wait_for_estimate_smart_fee"); + wait_for_estimate_smart_fee(60).expect("!wait_for_estimate_smart_fee"); // generate QRC20 coin(QICK) fill the wallet with 10 QICK // fill QTUM balance with 0.005 QTUM which is will be than expected transaction fee just to get our desired output for this test. let qick_balance = MmNumber::from("10").to_decimal(); @@ -1205,7 +1201,7 @@ fn test_trade_preimage_not_sufficient_base_coin_balance_for_ticker() { #[test] fn test_trade_preimage_dynamic_fee_not_sufficient_balance() { - wait_for_estimate_smart_fee(30).expect("!wait_for_estimate_smart_fee"); + wait_for_estimate_smart_fee(60).expect("!wait_for_estimate_smart_fee"); // generate QTUM coin with the dynamic fee and fill the wallet by 0.5 Qtums let qtum_balance = MmNumber::from("0.5").to_decimal(); let (_ctx, _coin, priv_key) = generate_qtum_coin_with_random_privkey("QTUM", qtum_balance.clone(), Some(0)); @@ -1266,7 +1262,7 @@ fn test_trade_preimage_dynamic_fee_not_sufficient_balance() { /// so we have to receive the `NotSufficientBalance` error. #[test] fn test_trade_preimage_deduct_fee_from_output_failed() { - wait_for_estimate_smart_fee(30).expect("!wait_for_estimate_smart_fee"); + wait_for_estimate_smart_fee(60).expect("!wait_for_estimate_smart_fee"); // generate QTUM coin with the dynamic fee and fill the wallet by 0.00073 Qtums (that is little greater than dust 0.000728) let qtum_balance = MmNumber::from("0.00073").to_decimal(); let (_ctx, _coin, priv_key) = generate_qtum_coin_with_random_privkey("QTUM", qtum_balance.clone(), Some(0)); @@ -1326,7 +1322,7 @@ fn test_trade_preimage_deduct_fee_from_output_failed() { #[test] fn test_segwit_native_balance() { - wait_for_estimate_smart_fee(30).expect("!wait_for_estimate_smart_fee"); + wait_for_estimate_smart_fee(60).expect("!wait_for_estimate_smart_fee"); // generate QTUM coin with the dynamic fee and fill the wallet by 0.5 Qtums let (_ctx, _coin, priv_key) = generate_segwit_qtum_coin_with_random_privkey("QTUM", BigDecimal::try_from(0.5).unwrap(), Some(0)); @@ -1372,7 +1368,7 @@ fn test_segwit_native_balance() { #[test] fn test_withdraw_and_send_from_segwit() { - wait_for_estimate_smart_fee(30).expect("!wait_for_estimate_smart_fee"); + wait_for_estimate_smart_fee(60).expect("!wait_for_estimate_smart_fee"); // generate QTUM coin with the dynamic fee and fill the wallet by 0.7 Qtums let (_ctx, _coin, priv_key) = generate_segwit_qtum_coin_with_random_privkey("QTUM", BigDecimal::try_from(0.7).unwrap(), Some(0)); @@ -1420,7 +1416,7 @@ fn test_withdraw_and_send_from_segwit() { #[test] fn test_withdraw_and_send_legacy_to_segwit() { - wait_for_estimate_smart_fee(30).expect("!wait_for_estimate_smart_fee"); + wait_for_estimate_smart_fee(60).expect("!wait_for_estimate_smart_fee"); // generate QTUM coin with the dynamic fee and fill the wallet by 0.7 Qtums let (_ctx, _coin, priv_key) = generate_qtum_coin_with_random_privkey("QTUM", BigDecimal::try_from(0.7).unwrap(), Some(0)); @@ -1465,7 +1461,7 @@ fn test_withdraw_and_send_legacy_to_segwit() { #[test] fn test_search_for_segwit_swap_tx_spend_native_was_refunded_maker() { - wait_for_estimate_smart_fee(30).expect("!wait_for_estimate_smart_fee"); + wait_for_estimate_smart_fee(60).expect("!wait_for_estimate_smart_fee"); let timeout = wait_until_sec(120); // timeout if test takes more than 120 seconds to run let (_ctx, coin, _) = generate_segwit_qtum_coin_with_random_privkey("QTUM", 1000u64.into(), Some(0)); let my_public_key = coin.my_public_key().unwrap(); @@ -1533,7 +1529,7 @@ fn test_search_for_segwit_swap_tx_spend_native_was_refunded_maker() { #[test] fn test_search_for_segwit_swap_tx_spend_native_was_refunded_taker() { - wait_for_estimate_smart_fee(30).expect("!wait_for_estimate_smart_fee"); + wait_for_estimate_smart_fee(60).expect("!wait_for_estimate_smart_fee"); let timeout = wait_until_sec(120); // timeout if test takes more than 120 seconds to run let (_ctx, coin, _) = generate_segwit_qtum_coin_with_random_privkey("QTUM", 1000u64.into(), Some(0)); let my_public_key = coin.my_public_key().unwrap(); @@ -1619,7 +1615,7 @@ pub async fn enable_native_segwit(mm: &MarketMakerIt, coin: &str) -> Json { #[test] #[ignore] fn segwit_address_in_the_orderbook() { - wait_for_estimate_smart_fee(30).expect("!wait_for_estimate_smart_fee"); + wait_for_estimate_smart_fee(60).expect("!wait_for_estimate_smart_fee"); // generate QTUM coin with the dynamic fee and fill the wallet by 0.5 Qtums let (_ctx, coin, priv_key) = generate_qtum_coin_with_random_privkey("QTUM", BigDecimal::try_from(0.5).unwrap(), Some(0)); @@ -1699,15 +1695,39 @@ fn test_trade_qrc20_utxo() { trade_base_rel(("QICK", "MYCOIN")); } fn test_trade_utxo_qrc20() { trade_base_rel(("MYCOIN", "QICK")); } #[test] -fn test_send_taker_fee_qtum() { +fn test_send_standard_taker_fee_qtum() { // generate QTUM coin with the dynamic fee and fill the wallet by 0.5 Qtums let (_ctx, coin, _priv_key) = generate_segwit_qtum_coin_with_random_privkey("QTUM", BigDecimal::try_from(0.5).unwrap(), Some(0)); let amount = BigDecimal::from_str("0.01").unwrap(); + let tx = block_on(coin.send_taker_fee(DexFee::Standard(amount.clone().into()), &[], 0)).expect("!send_taker_fee"); + assert!(matches!(tx, TransactionEnum::UtxoTx(_)), "Expected UtxoTx"); + + block_on(coin.validate_fee(ValidateFeeArgs { + fee_tx: &tx, + expected_sender: coin.my_public_key().unwrap(), + dex_fee: &DexFee::Standard(amount.into()), + min_block_number: 0, + uuid: &[], + })) + .expect("!validate_fee"); +} + +#[test] +fn test_send_taker_fee_with_burn_qtum() { + // generate QTUM coin with the dynamic fee and fill the wallet by 0.5 Qtums + let (_ctx, coin, _priv_key) = + generate_segwit_qtum_coin_with_random_privkey("QTUM", BigDecimal::try_from(0.5).unwrap(), Some(0)); + + let fee_amount = BigDecimal::from_str("0.0075").unwrap(); + let burn_amount = BigDecimal::from_str("0.0025").unwrap(); let tx = block_on(coin.send_taker_fee( - &DEX_FEE_ADDR_RAW_PUBKEY, - DexFee::Standard(amount.clone().into()), + DexFee::WithBurn { + fee_amount: fee_amount.clone().into(), + burn_amount: burn_amount.clone().into(), + burn_destination: DexFeeBurnDestination::PreBurnAccount, + }, &[], 0, )) @@ -1717,8 +1737,11 @@ fn test_send_taker_fee_qtum() { block_on(coin.validate_fee(ValidateFeeArgs { fee_tx: &tx, expected_sender: coin.my_public_key().unwrap(), - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, - dex_fee: &DexFee::Standard(amount.into()), + dex_fee: &DexFee::WithBurn { + fee_amount: fee_amount.into(), + burn_amount: burn_amount.into(), + burn_destination: DexFeeBurnDestination::PreBurnAccount, + }, min_block_number: 0, uuid: &[], })) @@ -1734,19 +1757,12 @@ fn test_send_taker_fee_qrc20() { ); let amount = BigDecimal::from_str("0.01").unwrap(); - let tx = block_on(coin.send_taker_fee( - &DEX_FEE_ADDR_RAW_PUBKEY, - DexFee::Standard(amount.clone().into()), - &[], - 0, - )) - .expect("!send_taker_fee"); + let tx = block_on(coin.send_taker_fee(DexFee::Standard(amount.clone().into()), &[], 0)).expect("!send_taker_fee"); assert!(matches!(tx, TransactionEnum::UtxoTx(_)), "Expected UtxoTx"); block_on(coin.validate_fee(ValidateFeeArgs { fee_tx: &tx, expected_sender: coin.my_public_key().unwrap(), - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &DexFee::Standard(amount.into()), min_block_number: 0, uuid: &[], diff --git a/mm2src/mm2_main/tests/docker_tests/swap_proto_v2_tests.rs b/mm2src/mm2_main/tests/docker_tests/swap_proto_v2_tests.rs index 304f6f4819..4f4333a92d 100644 --- a/mm2src/mm2_main/tests/docker_tests/swap_proto_v2_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/swap_proto_v2_tests.rs @@ -1,4 +1,4 @@ -use crate::{generate_utxo_coin_with_random_privkey, MYCOIN, MYCOIN1}; +use crate::{generate_utxo_coin_with_random_privkey, MYCOIN, MYCOIN1, SET_BURN_PUBKEY_TO_ALICE}; use bitcrypto::dhash160; use coins::utxo::UtxoCommonOps; use coins::{ConfirmPaymentInput, DexFee, FundingTxSpend, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, @@ -6,7 +6,9 @@ use coins::{ConfirmPaymentInput, DexFee, FundingTxSpend, GenTakerFundingSpendArg RefundMakerPaymentSecretArgs, RefundMakerPaymentTimelockArgs, RefundTakerPaymentArgs, SendMakerPaymentArgs, SendTakerFundingArgs, SwapTxTypeWithSecretHash, TakerCoinSwapOpsV2, Transaction, ValidateMakerPaymentArgs, ValidateTakerFundingArgs}; -use common::{block_on, block_on_f01, now_sec, DEX_FEE_ADDR_RAW_PUBKEY}; +use crypto::privkey::key_pair_from_secret; +//use futures01::Future; +use common::{block_on, block_on_f01, now_sec}; use mm2_number::MmNumber; use mm2_test_helpers::for_tests::{active_swaps, check_recent_swaps, coins_needed_for_kickstart, disable_coin, disable_coin_err, enable_native, get_locked_amount, mm_dump, my_swap_status, @@ -280,7 +282,7 @@ fn send_and_spend_taker_funding() { } #[test] -fn send_and_spend_taker_payment_dex_fee_burn() { +fn send_and_spend_taker_payment_dex_fee_burn_kmd() { let (_mm_arc, taker_coin, _privkey) = generate_utxo_coin_with_random_privkey(MYCOIN, 1000.into()); let (_mm_arc, maker_coin, _privkey) = generate_utxo_coin_with_random_privkey(MYCOIN, 1000.into()); @@ -294,7 +296,7 @@ fn send_and_spend_taker_payment_dex_fee_burn() { let taker_pub = taker_coin.my_public_key().unwrap(); let maker_pub = maker_coin.my_public_key().unwrap(); - let dex_fee = &DexFee::with_burn("0.75".into(), "0.25".into()); + let dex_fee = &DexFee::create_from_fields("0.75".into(), "0.25".into(), "KMD"); let send_args = SendTakerFundingArgs { funding_time_lock, @@ -357,7 +359,6 @@ fn send_and_spend_taker_payment_dex_fee_burn() { maker_pub, maker_address: &block_on(maker_coin.my_addr()), taker_pub, - dex_fee_pub: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee, premium_amount: 0.into(), trading_amount: 777.into(), @@ -387,7 +388,7 @@ fn send_and_spend_taker_payment_dex_fee_burn() { } #[test] -fn send_and_spend_taker_payment_standard_dex_fee() { +fn send_and_spend_taker_payment_dex_fee_burn_non_kmd() { let (_mm_arc, taker_coin, _privkey) = generate_utxo_coin_with_random_privkey(MYCOIN, 1000.into()); let (_mm_arc, maker_coin, _privkey) = generate_utxo_coin_with_random_privkey(MYCOIN, 1000.into()); @@ -401,7 +402,7 @@ fn send_and_spend_taker_payment_standard_dex_fee() { let taker_pub = taker_coin.my_public_key().unwrap(); let maker_pub = maker_coin.my_public_key().unwrap(); - let dex_fee = &DexFee::Standard(1.into()); + let dex_fee = &DexFee::create_from_fields("0.75".into(), "0.25".into(), "MYCOIN"); let send_args = SendTakerFundingArgs { funding_time_lock, @@ -464,7 +465,6 @@ fn send_and_spend_taker_payment_standard_dex_fee() { maker_pub, maker_address: &block_on(maker_coin.my_addr()), taker_pub, - dex_fee_pub: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee, premium_amount: 0.into(), trading_amount: 777.into(), @@ -472,9 +472,12 @@ fn send_and_spend_taker_payment_standard_dex_fee() { let taker_payment_spend_preimage = block_on(taker_coin.gen_taker_payment_spend_preimage(&gen_taker_payment_spend_args, &[])).unwrap(); - // tx must have 1 output: dex fee - assert_eq!(taker_payment_spend_preimage.preimage.outputs.len(), 1); - assert_eq!(taker_payment_spend_preimage.preimage.outputs[0].value, 100000000); + // tx must have 3 outputs: dex fee, burn (for non-kmd too), and maker amount + // because of the burn output we can't use SIGHASH_SINGLE and taker must add the maker output + assert_eq!(taker_payment_spend_preimage.preimage.outputs.len(), 3); + assert_eq!(taker_payment_spend_preimage.preimage.outputs[0].value, 75_000_000); + assert_eq!(taker_payment_spend_preimage.preimage.outputs[1].value, 25_000_000); + assert_eq!(taker_payment_spend_preimage.preimage.outputs[2].value, 77699998000); block_on( maker_coin.validate_taker_payment_spend_preimage(&gen_taker_payment_spend_args, &taker_payment_spend_preimage), @@ -619,13 +622,39 @@ fn send_and_refund_maker_payment_taker_secret() { } #[test] -fn test_v2_swap_utxo_utxo() { +fn test_v2_swap_utxo_utxo() { test_v2_swap_utxo_utxo_impl(); } + +// test a swap when taker is burn pubkey (no dex fee should be paid) +#[test] +fn test_v2_swap_utxo_utxo_burnkey_as_alice() { + SET_BURN_PUBKEY_TO_ALICE.set(true); + test_v2_swap_utxo_utxo_impl(); +} + +fn test_v2_swap_utxo_utxo_impl() { let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey(MYCOIN, 1000.into()); let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey(MYCOIN1, 1000.into()); let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let alice_pubkey_str = hex::encode( + key_pair_from_secret(&alice_priv_key) + .expect("valid test key pair") + .public() + .to_vec(), + ); + let mut envs = vec![]; + if SET_BURN_PUBKEY_TO_ALICE.get() { + envs.push(("TEST_BURN_ADDR_RAW_PUBKEY", alice_pubkey_str.as_str())); + } + let bob_conf = Mm2TestConf::seednode_trade_v2(&format!("0x{}", hex::encode(bob_priv_key)), &coins); - let mut mm_bob = MarketMakerIt::start(bob_conf.conf, bob_conf.rpc_password, None).unwrap(); + let mut mm_bob = block_on(MarketMakerIt::start_with_envs( + bob_conf.conf, + bob_conf.rpc_password, + None, + &envs, + )) + .unwrap(); let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); log!("Bob log path: {}", mm_bob.log_path.display()); @@ -633,7 +662,13 @@ fn test_v2_swap_utxo_utxo() { Mm2TestConf::light_node_trade_v2(&format!("0x{}", hex::encode(alice_priv_key)), &coins, &[&mm_bob .ip .to_string()]); - let mut mm_alice = MarketMakerIt::start(alice_conf.conf, alice_conf.rpc_password, None).unwrap(); + let mut mm_alice = block_on(MarketMakerIt::start_with_envs( + alice_conf.conf, + alice_conf.rpc_password, + None, + &envs, + )) + .unwrap(); let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); log!("Alice log path: {}", mm_alice.log_path.display()); @@ -681,7 +716,11 @@ fn test_v2_swap_utxo_utxo() { let locked_alice = block_on(get_locked_amount(&mm_alice, MYCOIN1)); assert_eq!(locked_alice.coin, MYCOIN1); - let expected: MmNumberMultiRepr = MmNumber::from("778.00001").into(); + let expected: MmNumberMultiRepr = if SET_BURN_PUBKEY_TO_ALICE.get() { + MmNumber::from("777.00001").into() // no dex fee if dex pubkey is alice + } else { + MmNumber::from("778.00001").into() + }; assert_eq!(locked_alice.locked_amount, expected); // amount must unlocked after funding tx is sent diff --git a/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs b/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs index 723ce0bfe5..af91881da5 100644 --- a/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs @@ -4,20 +4,21 @@ use crate::docker_tests::eth_docker_tests::{erc20_coin_with_random_privkey, erc2 use crate::integration_tests_common::*; use crate::{generate_utxo_coin_with_privkey, generate_utxo_coin_with_random_privkey, random_secp256k1_secret}; use coins::coin_errors::ValidatePaymentError; -use coins::eth::checksum_address; +use coins::eth::{checksum_address, EthCoin}; +use coins::utxo::utxo_standard::UtxoStandardCoin; use coins::utxo::{dhash160, UtxoCommonOps}; -use coins::{ConfirmPaymentInput, FoundSwapTxSpend, MarketCoinOps, MmCoin, MmCoinEnum, RefundPaymentArgs, RewardTarget, - SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SwapOps, - SwapTxTypeWithSecretHash, ValidateWatcherSpendInput, WatcherOps, WatcherSpendType, +use coins::{ConfirmPaymentInput, DexFee, FoundSwapTxSpend, MarketCoinOps, MmCoin, MmCoinEnum, RefundPaymentArgs, + RewardTarget, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SwapOps, + SwapTxTypeWithSecretHash, TestCoin, ValidateWatcherSpendInput, WatcherOps, WatcherSpendType, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, EARLY_CONFIRMATION_ERR_LOG, INVALID_CONTRACT_ADDRESS_ERR_LOG, INVALID_PAYMENT_STATE_ERR_LOG, INVALID_RECEIVER_ERR_LOG, INVALID_REFUND_TX_ERR_LOG, INVALID_SCRIPT_ERR_LOG, INVALID_SENDER_ERR_LOG, INVALID_SWAP_ID_ERR_LOG, OLD_TRANSACTION_ERR_LOG}; -use common::{block_on, block_on_f01, now_sec, wait_until_sec, DEX_FEE_ADDR_RAW_PUBKEY}; +use common::{block_on, block_on_f01, now_sec, wait_until_sec}; use crypto::privkey::{key_pair_from_secret, key_pair_from_seed}; -use mm2_main::lp_swap::{dex_fee_amount, dex_fee_amount_from_taker_coin, generate_secret, get_payment_locktime, - MAKER_PAYMENT_SENT_LOG, MAKER_PAYMENT_SPEND_FOUND_LOG, MAKER_PAYMENT_SPEND_SENT_LOG, - REFUND_TEST_FAILURE_LOG, TAKER_PAYMENT_REFUND_SENT_LOG, WATCHER_MESSAGE_SENT_LOG}; +use mm2_main::lp_swap::{generate_secret, get_payment_locktime, MAKER_PAYMENT_SENT_LOG, MAKER_PAYMENT_SPEND_FOUND_LOG, + MAKER_PAYMENT_SPEND_SENT_LOG, REFUND_TEST_FAILURE_LOG, TAKER_PAYMENT_REFUND_SENT_LOG, + WATCHER_MESSAGE_SENT_LOG}; use mm2_number::BigDecimal; use mm2_number::MmNumber; use mm2_test_helpers::for_tests::{enable_eth_coin, erc20_dev_conf, eth_dev_conf, eth_jst_testnet_conf, mm_dump, @@ -26,6 +27,7 @@ use mm2_test_helpers::for_tests::{enable_eth_coin, erc20_dev_conf, eth_dev_conf, DEFAULT_RPC_PASSWORD}; use mm2_test_helpers::get_passphrase; use mm2_test_helpers::structs::WatcherConf; +use mocktopus::mocking::*; use num_traits::{One, Zero}; use primitives::hash::H256; use serde_json::Value; @@ -824,10 +826,12 @@ fn test_watcher_spends_maker_payment_eth_utxo() { let eth_volume = BigDecimal::from_str("0.01").unwrap(); let mycoin_volume = BigDecimal::from_str("1").unwrap(); - let min_tx_amount = BigDecimal::from_str("0.00001").unwrap().into(); + let min_tx_amount = BigDecimal::from_str("0.00001").unwrap(); - let dex_fee: BigDecimal = dex_fee_amount("MYCOIN", "ETH", &MmNumber::from(mycoin_volume.clone()), &min_tx_amount) - .fee_amount() + let coin = TestCoin::new("MYCOIN"); + TestCoin::min_tx_amount.mock_safe(move |_| MockResult::Return(min_tx_amount.clone())); + let dex_fee: BigDecimal = DexFee::new_from_taker_coin(&coin, "ETH", &MmNumber::from(mycoin_volume.clone()), None) + .fee_amount() // returns Standard fee (default for TestCoin) .into(); let alice_mycoin_reward_sent = balances.alice_acoin_balance_before - balances.alice_acoin_balance_after.clone() @@ -967,15 +971,13 @@ fn test_watcher_spends_maker_payment_erc20_utxo() { let mycoin_volume = BigDecimal::from_str("1").unwrap(); let jst_volume = BigDecimal::from_str("1").unwrap(); - let min_tx_amount = BigDecimal::from_str("0.00001").unwrap().into(); - let dex_fee: BigDecimal = dex_fee_amount( - "MYCOIN", - "ERC20DEV", - &MmNumber::from(mycoin_volume.clone()), - &min_tx_amount, - ) - .fee_amount() - .into(); + let min_tx_amount = BigDecimal::from_str("0.00001").unwrap(); + let coin = TestCoin::new("MYCOIN"); + TestCoin::min_tx_amount.mock_safe(move |_| MockResult::Return(min_tx_amount.clone())); + let dex_fee: BigDecimal = + DexFee::new_from_taker_coin(&coin, "ERC20DEV", &MmNumber::from(mycoin_volume.clone()), None) + .fee_amount() // returns Standard fee (default for TestCoin) + .into(); let alice_mycoin_reward_sent = balances.alice_acoin_balance_before - balances.alice_acoin_balance_after.clone() - mycoin_volume.clone() @@ -1224,15 +1226,9 @@ fn test_watcher_validate_taker_fee_utxo() { let taker_pubkey = taker_coin.my_public_key().unwrap(); let taker_amount = MmNumber::from((10, 1)); - let fee_amount = dex_fee_amount_from_taker_coin(&taker_coin, maker_coin.ticker(), &taker_amount); + let dex_fee = DexFee::new_from_taker_coin(&taker_coin, maker_coin.ticker(), &taker_amount, None); - let taker_fee = block_on(taker_coin.send_taker_fee( - &DEX_FEE_ADDR_RAW_PUBKEY, - fee_amount, - Uuid::new_v4().as_bytes(), - lock_duration, - )) - .unwrap(); + let taker_fee = block_on(taker_coin.send_taker_fee(dex_fee, Uuid::new_v4().as_bytes(), lock_duration)).unwrap(); let confirm_payment_input = ConfirmPaymentInput { payment_tx: taker_fee.tx_hex(), @@ -1248,7 +1244,6 @@ fn test_watcher_validate_taker_fee_utxo() { taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), sender_pubkey: taker_pubkey.to_vec(), min_block_number: 0, - fee_addr: DEX_FEE_ADDR_RAW_PUBKEY.to_vec(), lock_duration, })); assert!(validate_taker_fee_res.is_ok()); @@ -1257,7 +1252,6 @@ fn test_watcher_validate_taker_fee_utxo() { taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), sender_pubkey: maker_coin.my_public_key().unwrap().to_vec(), min_block_number: 0, - fee_addr: DEX_FEE_ADDR_RAW_PUBKEY.to_vec(), lock_duration, })) .unwrap_err() @@ -1275,7 +1269,6 @@ fn test_watcher_validate_taker_fee_utxo() { taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), sender_pubkey: taker_pubkey.to_vec(), min_block_number: std::u64::MAX, - fee_addr: DEX_FEE_ADDR_RAW_PUBKEY.to_vec(), lock_duration, })) .unwrap_err() @@ -1295,7 +1288,6 @@ fn test_watcher_validate_taker_fee_utxo() { taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), sender_pubkey: taker_pubkey.to_vec(), min_block_number: 0, - fee_addr: DEX_FEE_ADDR_RAW_PUBKEY.to_vec(), lock_duration: 0, })) .unwrap_err() @@ -1308,11 +1300,14 @@ fn test_watcher_validate_taker_fee_utxo() { _ => panic!("Expected `WrongPaymentTx` transaction too old, found {:?}", error), } + let mock_pubkey = taker_pubkey.to_vec(); + ::dex_pubkey + .mock_safe(move |_| MockResult::Return(Box::leak(Box::new(mock_pubkey.clone())))); + let error = block_on_f01(taker_coin.watcher_validate_taker_fee(WatcherValidateTakerFeeInput { taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), sender_pubkey: taker_pubkey.to_vec(), min_block_number: 0, - fee_addr: taker_pubkey.to_vec(), lock_duration, })) .unwrap_err() @@ -1339,14 +1334,8 @@ fn test_watcher_validate_taker_fee_eth() { let taker_pubkey = taker_keypair.public(); let taker_amount = MmNumber::from((1, 1)); - let fee_amount = dex_fee_amount_from_taker_coin(&taker_coin, "ETH", &taker_amount); - let taker_fee = block_on(taker_coin.send_taker_fee( - &DEX_FEE_ADDR_RAW_PUBKEY, - fee_amount, - Uuid::new_v4().as_bytes(), - lock_duration, - )) - .unwrap(); + let dex_fee = DexFee::new_from_taker_coin(&taker_coin, "ETH", &taker_amount, None); + let taker_fee = block_on(taker_coin.send_taker_fee(dex_fee, Uuid::new_v4().as_bytes(), lock_duration)).unwrap(); let confirm_payment_input = ConfirmPaymentInput { payment_tx: taker_fee.tx_hex(), @@ -1361,7 +1350,6 @@ fn test_watcher_validate_taker_fee_eth() { taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), sender_pubkey: taker_pubkey.to_vec(), min_block_number: 0, - fee_addr: DEX_FEE_ADDR_RAW_PUBKEY.to_vec(), lock_duration, })); assert!(validate_taker_fee_res.is_ok()); @@ -1371,7 +1359,6 @@ fn test_watcher_validate_taker_fee_eth() { taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), sender_pubkey: wrong_keypair.public().to_vec(), min_block_number: 0, - fee_addr: DEX_FEE_ADDR_RAW_PUBKEY.to_vec(), lock_duration, })) .unwrap_err() @@ -1389,7 +1376,6 @@ fn test_watcher_validate_taker_fee_eth() { taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), sender_pubkey: taker_pubkey.to_vec(), min_block_number: std::u64::MAX, - fee_addr: DEX_FEE_ADDR_RAW_PUBKEY.to_vec(), lock_duration, })) .unwrap_err() @@ -1405,11 +1391,13 @@ fn test_watcher_validate_taker_fee_eth() { ), } + let mock_pubkey = taker_pubkey.to_vec(); + ::dex_pubkey.mock_safe(move |_| MockResult::Return(Box::leak(Box::new(mock_pubkey.clone())))); + let error = block_on_f01(taker_coin.watcher_validate_taker_fee(WatcherValidateTakerFeeInput { taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), sender_pubkey: taker_pubkey.to_vec(), min_block_number: 0, - fee_addr: taker_pubkey.to_vec(), lock_duration, })) .unwrap_err() @@ -1424,6 +1412,7 @@ fn test_watcher_validate_taker_fee_eth() { error ), } + ::dex_pubkey.clear_mock(); } #[test] @@ -1436,14 +1425,8 @@ fn test_watcher_validate_taker_fee_erc20() { let taker_pubkey = taker_keypair.public(); let taker_amount = MmNumber::from((1, 1)); - let fee_amount = dex_fee_amount_from_taker_coin(&taker_coin, "ETH", &taker_amount); - let taker_fee = block_on(taker_coin.send_taker_fee( - &DEX_FEE_ADDR_RAW_PUBKEY, - fee_amount, - Uuid::new_v4().as_bytes(), - lock_duration, - )) - .unwrap(); + let dex_fee = DexFee::new_from_taker_coin(&taker_coin, "ETH", &taker_amount, None); + let taker_fee = block_on(taker_coin.send_taker_fee(dex_fee, Uuid::new_v4().as_bytes(), lock_duration)).unwrap(); let confirm_payment_input = ConfirmPaymentInput { payment_tx: taker_fee.tx_hex(), @@ -1458,7 +1441,6 @@ fn test_watcher_validate_taker_fee_erc20() { taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), sender_pubkey: taker_pubkey.to_vec(), min_block_number: 0, - fee_addr: DEX_FEE_ADDR_RAW_PUBKEY.to_vec(), lock_duration, })); assert!(validate_taker_fee_res.is_ok()); @@ -1468,7 +1450,6 @@ fn test_watcher_validate_taker_fee_erc20() { taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), sender_pubkey: wrong_keypair.public().to_vec(), min_block_number: 0, - fee_addr: DEX_FEE_ADDR_RAW_PUBKEY.to_vec(), lock_duration, })) .unwrap_err() @@ -1486,7 +1467,6 @@ fn test_watcher_validate_taker_fee_erc20() { taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), sender_pubkey: taker_pubkey.to_vec(), min_block_number: std::u64::MAX, - fee_addr: DEX_FEE_ADDR_RAW_PUBKEY.to_vec(), lock_duration, })) .unwrap_err() @@ -1502,11 +1482,13 @@ fn test_watcher_validate_taker_fee_erc20() { ), } + let mock_pubkey = taker_pubkey.to_vec(); + ::dex_pubkey.mock_safe(move |_| MockResult::Return(Box::leak(Box::new(mock_pubkey.clone())))); + let error = block_on_f01(taker_coin.watcher_validate_taker_fee(WatcherValidateTakerFeeInput { taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), sender_pubkey: taker_pubkey.to_vec(), min_block_number: 0, - fee_addr: taker_pubkey.to_vec(), lock_duration, })) .unwrap_err() @@ -1521,6 +1503,7 @@ fn test_watcher_validate_taker_fee_erc20() { error ), } + ::dex_pubkey.clear_mock(); } #[test] diff --git a/mm2src/mm2_main/tests/docker_tests_main.rs b/mm2src/mm2_main/tests/docker_tests_main.rs index 707f558631..480e3a63ee 100644 --- a/mm2src/mm2_main/tests/docker_tests_main.rs +++ b/mm2src/mm2_main/tests/docker_tests_main.rs @@ -6,6 +6,7 @@ #![feature(drain_filter)] #![feature(hash_raw_entry)] #![cfg(not(target_arch = "wasm32"))] +#![feature(local_key_cell_methods)] // for setting global vars in tests #[cfg(test)] #[macro_use] diff --git a/mm2src/mm2_main/tests/docker_tests_sia_unique.rs b/mm2src/mm2_main/tests/docker_tests_sia_unique.rs index 521da60e01..a176277c64 100644 --- a/mm2src/mm2_main/tests/docker_tests_sia_unique.rs +++ b/mm2src/mm2_main/tests/docker_tests_sia_unique.rs @@ -7,6 +7,7 @@ #![feature(drain_filter)] #![feature(hash_raw_entry)] #![cfg(not(target_arch = "wasm32"))] +#![feature(local_key_cell_methods)] #[cfg(test)] #[macro_use] diff --git a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs index fdb5dd9d74..77e01b6f1c 100644 --- a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs +++ b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs @@ -6360,6 +6360,7 @@ mod trezor_tests { "ticker": ticker_coin, "rpc_mode": "Default", "nodes": [ + {"url": "https://sepolia.drpc.org"}, {"url": "https://rpc2.sepolia.org"}, {"url": "https://rpc.sepolia.org/"} ], diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index b4466c338b..d19de79559 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -248,6 +248,7 @@ pub const ETH_MAINNET_CHAIN_ID: u64 = 1; pub const ETH_MAINNET_SWAP_CONTRACT: &str = "0x24abe4c71fc658c91313b6552cd40cd808b3ea80"; pub const ETH_SEPOLIA_NODES: &[&str] = &[ + "https://sepolia.drpc.org", "https://ethereum-sepolia-rpc.publicnode.com", "https://rpc2.sepolia.org", "https://1rpc.io/sepolia", diff --git a/mm2src/mm2_test_helpers/src/structs.rs b/mm2src/mm2_test_helpers/src/structs.rs index baba173461..3c0e9b02f2 100644 --- a/mm2src/mm2_test_helpers/src/structs.rs +++ b/mm2src/mm2_test_helpers/src/structs.rs @@ -995,7 +995,6 @@ pub enum MyTxHistoryTarget { Iguana, AccountId { account_id: u32 }, AddressId(HDAccountAddressId), - AddressDerivationPath(String), } #[derive(Debug, Deserialize)] diff --git a/mm2src/trading_api/Cargo.toml b/mm2src/trading_api/Cargo.toml index 4fd9514fb9..4f714ed34d 100644 --- a/mm2src/trading_api/Cargo.toml +++ b/mm2src/trading_api/Cargo.toml @@ -23,6 +23,7 @@ url = { version = "2.2.2", features = ["serde"] } [features] test-ext-api = [] # use test config to connect to an external api +for-tests = ["dep:mocktopus"] [dev-dependencies] mocktopus = { version = "0.8.0" } \ No newline at end of file diff --git a/mm2src/trading_api/src/one_inch_api/client.rs b/mm2src/trading_api/src/one_inch_api/client.rs index ef3c61ef6b..2825d930b5 100644 --- a/mm2src/trading_api/src/one_inch_api/client.rs +++ b/mm2src/trading_api/src/one_inch_api/client.rs @@ -10,7 +10,7 @@ use mm2_net::transport::slurp_url_with_headers; use serde::de::DeserializeOwned; use url::Url; -#[cfg(any(test, feature = "mocktopus"))] +#[cfg(any(test, feature = "for-tests"))] use mocktopus::macros::*; const ONE_INCH_API_ENDPOINT_V6_0: &str = "swap/v6.0/"; @@ -96,7 +96,7 @@ pub struct ApiClient { } #[allow(clippy::swap_ptr_to_ref)] // need for moctopus -#[cfg_attr(any(test, feature = "mocktopus"), mockable)] +#[cfg_attr(any(test, feature = "for-tests"), mockable)] impl ApiClient { #[allow(unused_variables)] #[allow(clippy::result_large_err)]