diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index 30192cd0db..06f250fd49 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -72,7 +72,7 @@ mm2_number = { path = "../mm2_number"} mm2_p2p = { path = "../mm2_p2p" } 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" } @@ -160,6 +160,7 @@ winapi = "0.3" [dev-dependencies] mm2_test_helpers = { path = "../mm2_test_helpers" } +mocktopus = { version = "0.8.0" } [target.'cfg(target_arch = "wasm32")'.dev-dependencies] wagyu-zcash-parameters = { version = "0.2" } diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 068c750d37..15f91fd419 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -80,7 +80,6 @@ use mm2_core::mm_ctx::{MmArc, MmWeak}; use mm2_event_stream::behaviour::{EventBehaviour, EventInitStatus}; 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; @@ -1105,14 +1104,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)), @@ -1179,7 +1172,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, @@ -1517,7 +1509,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: &[], @@ -2119,7 +2110,6 @@ impl WatcherOps for EthCoin { #[async_trait] #[cfg_attr(test, mockable)] -#[async_trait] impl MarketCoinOps for EthCoin { fn ticker(&self) -> &str { &self.ticker[..] } @@ -2490,6 +2480,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() } } @@ -5804,8 +5797,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; diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index c7f1e51d13..3a534b603a 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -586,7 +586,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: &[], @@ -622,7 +621,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: &[], @@ -662,7 +660,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: &[], @@ -701,7 +698,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 ce97c96cb3..f3b364e0ef 100644 --- a/mm2src/coins/lightning.rs +++ b/mm2src/coins/lightning.rs @@ -610,13 +610,7 @@ impl LightningCoin { #[async_trait] impl SwapOps for LightningCoin { // Todo: This uses dummy data for now for the sake of swap P.O.C., this should be implemented probably after agreeing on how fees will work for lightning - 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]))) } @@ -1253,6 +1247,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 6c7fea7b26..08431fa03e 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; @@ -48,7 +49,7 @@ use common::custom_futures::timeout::TimeoutError; use common::executor::{abortable_queue::{AbortableQueue, WeakSpawner}, AbortSettings, AbortedError, SpawnAbortable, 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}; @@ -65,9 +66,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}, MmNumber}; use mm2_rpc::data::legacy::{EnabledCoin, GetEnabledResponse, Mm2RpcResult}; +#[cfg(any(test, feature = "mocktopus"))] +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}; @@ -86,6 +90,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; @@ -207,6 +215,7 @@ pub mod coin_balance; use coin_balance::{AddressBalanceStatus, HDAddressBalance, HDWalletBalanceOps}; pub mod lp_price; +pub mod swap_features; pub mod watcher_common; pub mod coin_errors; @@ -276,6 +285,17 @@ pub mod z_coin; use crate::coin_balance::{BalanceObjectOps, HDWalletBalanceObject}; use z_coin::{ZCoin, ZcoinProtocolInfo}; +/// Default swap protocol version before version field added to NegotiationDataMsg +pub const LEGACY_PROTOCOL_VERSION: u16 = 0; + +/// Current swap protocol version +pub const SWAP_PROTOCOL_VERSION: u16 = 1; + +/// Minimal supported swap protocol version implemented by remote peer +pub const MIN_SWAP_PROTOCOL_VERSION: u16 = LEGACY_PROTOCOL_VERSION; + +// TODO: add version field to the SWAP V2 negotiation protocol + pub type TransactionFut = Box + Send>; pub type TransactionResult = Result; pub type BalanceResult = Result>; @@ -701,7 +721,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, } @@ -988,7 +1007,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], @@ -997,7 +1015,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], @@ -1068,8 +1085,12 @@ pub enum WatcherRewardError { /// Swap operations (mostly based on the Hash/Time locked transactions implemented by coin wallets). #[async_trait] +// Note: when you want to use mocked objects in this crate (like TestCoin, etc) from mm2_main crate +// you also need to add cfg_attr feature = "mocktopus" to 'mockable' because mocktopus is marked as 'optional' in coins/Cargo.toml +// otherwise mocks called from other crates won't work +#[cfg_attr(any(test, feature = "mocktopus"), 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; @@ -1181,6 +1202,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 @@ -1356,8 +1391,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) @@ -2047,8 +2080,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; } @@ -3548,6 +3588,7 @@ impl MmCoinEnum { 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"))] + // TODO: we do not have such feature = "zhtlc" in toml. Remove this cfg part? MmCoinEnum::ZCoin(ref c) => c.as_ref().rpc_client.is_native(), _ => false, } @@ -3604,32 +3645,189 @@ impl MmCoinStruct { } } +/// Calculates DEX fee with a threshold based on min tx amount of the taker coin. +/// preburn_account_active param indicates that swap nodes version supports burning dex fee for non kmd coins. +/// It could be None if dex fee is needed only to get total dex fee amount +/// taker_pubkey also may be optional if it is not known yet but we need total dex fee amount +pub fn dex_fee_from_taker_coin( + taker_coin: &dyn MmCoin, + maker_coin: &str, + trade_amount: &MmNumber, + taker_pubkey: Option<&[u8]>, + preburn_account_active: Option, +) -> DexFee { + DexFee::new_from_taker_coin( + taker_coin.deref(), + maker_coin, + trade_amount, + taker_pubkey, + preburn_account_active, + ) +} + +/// 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) + } + } + + /// Creates a new `DexFee` for a taker coin to sell. + fn new_from_taker_coin( + taker_coin: &dyn MmCoin, + rel_ticker: &str, + trade_amount: &MmNumber, + taker_pubkey: Option<&[u8]>, + preburn_account_active: Option, + ) -> DexFee { + if let Some(taker_pubkey) = taker_pubkey { + if !taker_coin.is_privacy() + && taker_coin.burn_pubkey() == taker_pubkey + && preburn_account_active.unwrap_or(false) + { + 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()); + if dex_fee <= min_tx_amount { + return DexFee::Standard(min_tx_amount); + } + + 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); + return DexFee::WithBurn { + fee_amount, + burn_amount, + burn_destination: DexFeeBurnDestination::KmdOpReturn, + }; + } else if taker_coin.should_burn_dex_fee() && preburn_account_active.unwrap_or(false) { + // 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, + }; + } + } + 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 { + (dex_fee.clone(), MmNumber::from(0)) + } + } + + /// 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 + return (min_tx_amount.clone(), burn_amount); + } + // Default case where burn_amount is considered dust + (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(), } @@ -3638,6 +3836,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()), } @@ -3646,22 +3845,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 { @@ -5506,9 +5707,11 @@ where #[cfg(test)] mod tests { use super::*; - use common::block_on; use mm2_test_helpers::for_tests::RICK; + use mocktopus::mocking::{MockResult, Mockable}; + + const PRE_BURN_ACCOUNT_ACTIVE: bool = true; #[test] fn test_lp_coinfind() { @@ -5559,6 +5762,149 @@ 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, Some(PRE_BURN_ACCOUNT_ACTIVE)); + 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, Some(PRE_BURN_ACCOUNT_ACTIVE)); + 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, Some(PRE_BURN_ACCOUNT_ACTIVE)); + 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, Some(PRE_BURN_ACCOUNT_ACTIVE)); + 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, Some(PRE_BURN_ACCOUNT_ACTIVE)); + 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, Some(PRE_BURN_ACCOUNT_ACTIVE)); + 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, Some(PRE_BURN_ACCOUNT_ACTIVE)); + 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, Some(PRE_BURN_ACCOUNT_ACTIVE)); + 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()), + Some(PRE_BURN_ACCOUNT_ACTIVE), + ); + 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/qrc20.rs b/mm2src/coins/qrc20.rs index 3f86b78539..df1740736e 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -759,14 +759,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)); @@ -864,7 +858,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)?; @@ -1292,6 +1286,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 930f078c53..976253cf32 100644 --- a/mm2src/coins/qrc20/qrc20_tests.rs +++ b/mm2src/coins/qrc20/qrc20_tests.rs @@ -339,18 +339,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: &[], @@ -362,11 +365,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: &[], @@ -382,7 +385,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: &[], @@ -399,7 +401,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: &[], @@ -420,7 +421,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 1bd8ef6c2d..2253d6138c 100644 --- a/mm2src/coins/siacoin.rs +++ b/mm2src/coins/siacoin.rs @@ -394,18 +394,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/solana.rs b/mm2src/coins/solana.rs new file mode 100644 index 0000000000..c8e883aa23 --- /dev/null +++ b/mm2src/coins/solana.rs @@ -0,0 +1,1098 @@ +use std::{collections::HashMap, + convert::{TryFrom, TryInto}, + fmt::Debug, + ops::Deref, + str::FromStr, + sync::{Arc, Mutex}}; + +use async_trait::async_trait; +use base58::ToBase58; +use bincode::{deserialize, serialize}; +use bitcrypto::sha256; +use common::{async_blocking, + executor::{abortable_queue::AbortableQueue, AbortableSystem, AbortedError}, + log::error, + now_sec}; +use crypto::HDPathToCoin; +use derive_more::Display; +use futures::{compat::Future01CompatExt, + {FutureExt, TryFutureExt}}; +use futures01::Future; +use keys::KeyPair; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use mm2_number::{BigDecimal, MmNumber}; +use num_traits::ToPrimitive; +use rpc::v1::types::Bytes as BytesJson; +pub use satomic_swap::{instruction::AtomicSwapInstruction, STORAGE_SPACE_ALLOCATED}; +use serde_json::{self as json, Value as Json}; +use solana_client::{client_error::{ClientError, ClientErrorKind}, + rpc_client::RpcClient, + rpc_request::TokenAccountsFilter}; +pub use solana_sdk::transaction::Transaction as SolTransaction; +use solana_sdk::{commitment_config::{CommitmentConfig, CommitmentLevel}, + instruction::{AccountMeta, Instruction}, + native_token::sol_to_lamports, + program_error::ProgramError, + pubkey::{ParsePubkeyError, Pubkey}, + signature::{Keypair as SolKeypair, Signer}}; +use spl_token::solana_program; + +use super::{CoinBalance, HistorySyncState, MarketCoinOps, MmCoin, SwapOps, TradeFee, Transaction, TransactionEnum, + TransactionErr, WatcherOps}; +use crate::coin_errors::{MyAddressError, ValidatePaymentResult}; +use crate::hd_wallet::HDPathAccountToAddressId; +use crate::solana::{solana_common::{lamports_to_sol, PrepareTransferData, SufficientBalanceError}, + spl::SplTokenInfo}; +use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinFutSpawner, ConfirmPaymentInput, DexFee, + FeeApproxStage, FoundSwapTxSpend, MakerSwapTakerCoin, MmCoinEnum, NegotiateSwapContractAddrErr, + PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, + PrivKeyPolicyNotAllowed, RawTransactionError, RawTransactionFut, RawTransactionRequest, + RawTransactionResult, RefundError, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, + SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignRawTransactionRequest, SignatureResult, + SpendPaymentArgs, TakerSwapMakerCoin, TradePreimageFut, TradePreimageResult, TradePreimageValue, + TransactionData, TransactionDetails, TransactionFut, TransactionResult, TransactionType, TxMarshalingErr, + UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, + ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, + ValidateWatcherSpendInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherReward, WatcherRewardError, + WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, + WithdrawError, WithdrawFut, WithdrawRequest, WithdrawResult}; + +pub mod solana_common; +mod solana_decode_tx_helpers; +pub mod spl; + +#[cfg(test)] mod solana_common_tests; +#[cfg(test)] mod solana_tests; +#[cfg(test)] mod spl_tests; + +pub const SOLANA_DEFAULT_DECIMALS: u64 = 9; +pub const LAMPORTS_DUMMY_AMOUNT: u64 = 10; + +#[async_trait] +pub trait SolanaCommonOps { + fn rpc(&self) -> &RpcClient; + + fn is_token(&self) -> bool; + + async fn check_balance_and_prepare_transfer( + &self, + max: bool, + amount: BigDecimal, + fees: u64, + ) -> Result>; +} + +impl From for BalanceError { + fn from(e: ClientError) -> Self { + match e.kind { + ClientErrorKind::Io(e) => BalanceError::Transport(e.to_string()), + ClientErrorKind::Reqwest(e) => BalanceError::Transport(e.to_string()), + ClientErrorKind::RpcError(e) => BalanceError::Transport(format!("{:?}", e)), + ClientErrorKind::SerdeJson(e) => BalanceError::InvalidResponse(e.to_string()), + ClientErrorKind::Custom(e) => BalanceError::Internal(e), + ClientErrorKind::SigningError(_) + | ClientErrorKind::TransactionError(_) + | ClientErrorKind::FaucetError(_) => BalanceError::Internal("not_reacheable".to_string()), + } + } +} + +impl From for BalanceError { + fn from(e: ParsePubkeyError) -> Self { BalanceError::Internal(format!("{:?}", e)) } +} + +impl From for WithdrawError { + fn from(e: ClientError) -> Self { + match e.kind { + ClientErrorKind::Io(e) => WithdrawError::Transport(e.to_string()), + ClientErrorKind::Reqwest(e) => WithdrawError::Transport(e.to_string()), + ClientErrorKind::RpcError(e) => WithdrawError::Transport(format!("{:?}", e)), + ClientErrorKind::SerdeJson(e) => WithdrawError::InternalError(e.to_string()), + ClientErrorKind::Custom(e) => WithdrawError::InternalError(e), + ClientErrorKind::SigningError(_) + | ClientErrorKind::TransactionError(_) + | ClientErrorKind::FaucetError(_) => WithdrawError::InternalError("not_reacheable".to_string()), + } + } +} + +impl From for WithdrawError { + fn from(e: ParsePubkeyError) -> Self { WithdrawError::InvalidAddress(format!("{:?}", e)) } +} + +impl From for WithdrawError { + fn from(e: ProgramError) -> Self { WithdrawError::InternalError(format!("{:?}", e)) } +} + +#[derive(Debug)] +pub enum AccountError { + NotFundedError(String), + ParsePubKeyError(String), + ClientError(ClientErrorKind), +} + +impl From for AccountError { + fn from(e: ClientError) -> Self { AccountError::ClientError(e.kind) } +} + +impl From for AccountError { + fn from(e: ParsePubkeyError) -> Self { AccountError::ParsePubKeyError(format!("{:?}", e)) } +} + +impl From for WithdrawError { + fn from(e: AccountError) -> Self { + match e { + AccountError::NotFundedError(_) => WithdrawError::ZeroBalanceToWithdrawMax, + AccountError::ParsePubKeyError(err) => WithdrawError::InternalError(err), + AccountError::ClientError(e) => WithdrawError::Transport(format!("{:?}", e)), + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct SolanaActivationParams { + confirmation_commitment: CommitmentLevel, + client_url: String, + #[serde(default)] + path_to_address: HDPathAccountToAddressId, +} + +#[derive(Debug, Display)] +pub enum SolanaFromLegacyReqErr { + InvalidCommitmentLevel(String), + InvalidClientParsing(json::Error), + ClientNoAvailableNodes(String), +} + +#[derive(Debug, Display)] +pub enum KeyPairCreationError { + #[display(fmt = "Signature error: {}", _0)] + SignatureError(ed25519_dalek::SignatureError), + #[display(fmt = "KeyPairFromSeed error: {}", _0)] + KeyPairFromSeed(String), +} + +impl From for KeyPairCreationError { + fn from(e: ed25519_dalek::SignatureError) -> Self { KeyPairCreationError::SignatureError(e) } +} + +fn generate_keypair_from_slice(priv_key: &[u8]) -> Result> { + let secret_key = ed25519_dalek::SecretKey::from_bytes(priv_key)?; + let public_key = ed25519_dalek::PublicKey::from(&secret_key); + let key_pair = ed25519_dalek::Keypair { + secret: secret_key, + public: public_key, + }; + solana_sdk::signature::keypair_from_seed(key_pair.to_bytes().as_ref()) + .map_to_mm(|e| KeyPairCreationError::KeyPairFromSeed(e.to_string())) +} + +pub async fn solana_coin_with_policy( + ctx: &MmArc, + ticker: &str, + conf: &Json, + params: SolanaActivationParams, + priv_key_policy: PrivKeyBuildPolicy, +) -> Result { + let client = RpcClient::new_with_commitment(params.client_url.clone(), CommitmentConfig { + commitment: params.confirmation_commitment, + }); + let decimals = conf["decimals"].as_u64().unwrap_or(SOLANA_DEFAULT_DECIMALS) as u8; + + let priv_key = match priv_key_policy { + PrivKeyBuildPolicy::IguanaPrivKey(priv_key) => priv_key, + PrivKeyBuildPolicy::GlobalHDAccount(global_hd) => { + let path_to_coin: HDPathToCoin = try_s!(json::from_value(conf["derivation_path"].clone())); + let derivation_path = try_s!(params.path_to_address.to_derivation_path(&path_to_coin)); + try_s!(global_hd.derive_secp256k1_secret(&derivation_path)) + }, + PrivKeyBuildPolicy::Trezor => return ERR!("{}", PrivKeyPolicyNotAllowed::HardwareWalletNotSupported), + }; + + let key_pair = try_s!(generate_keypair_from_slice(priv_key.as_slice())); + let my_address = key_pair.pubkey().to_string(); + let spl_tokens_infos = Arc::new(Mutex::new(HashMap::new())); + + // Create an abortable system linked to the `MmCtx` so if the context is stopped via `MmArc::stop`, + // all spawned futures related to `SolanaCoin` will be aborted as well. + let abortable_system: AbortableQueue = try_s!(ctx.abortable_system.create_subsystem()); + + let solana_coin = SolanaCoin(Arc::new(SolanaCoinImpl { + my_address, + key_pair, + ticker: ticker.to_string(), + client, + decimals, + spl_tokens_infos, + abortable_system, + })); + Ok(solana_coin) +} + +/// pImpl idiom. +pub struct SolanaCoinImpl { + ticker: String, + key_pair: SolKeypair, + client: RpcClient, + decimals: u8, + my_address: String, + spl_tokens_infos: Arc>>, + /// This spawner is used to spawn coin's related futures that should be aborted on coin deactivation + /// and on [`MmArc::stop`]. + pub abortable_system: AbortableQueue, +} + +#[derive(Clone)] +pub struct SolanaCoin(Arc); +impl Deref for SolanaCoin { + type Target = SolanaCoinImpl; + fn deref(&self) -> &SolanaCoinImpl { &self.0 } +} + +#[async_trait] +impl SolanaCommonOps for SolanaCoin { + fn rpc(&self) -> &RpcClient { &self.client } + + fn is_token(&self) -> bool { false } + + async fn check_balance_and_prepare_transfer( + &self, + max: bool, + amount: BigDecimal, + fees: u64, + ) -> Result> { + solana_common::check_balance_and_prepare_transfer(self, max, amount, fees).await + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct SolanaFeeDetails { + pub amount: BigDecimal, +} + +async fn withdraw_base_coin_impl(coin: SolanaCoin, req: WithdrawRequest) -> WithdrawResult { + let (hash, fees) = coin.estimate_withdraw_fees().await?; + let res = coin + .check_balance_and_prepare_transfer(req.max, req.amount.clone(), fees) + .await?; + let to = solana_sdk::pubkey::Pubkey::try_from(&*req.to)?; + let tx = solana_sdk::system_transaction::transfer(&coin.key_pair, &to, res.lamports_to_send, hash); + let serialized_tx = serialize(&tx).map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + let total_amount = lamports_to_sol(res.lamports_to_send); + let received_by_me = if req.to == coin.my_address { + total_amount.clone() + } else { + 0.into() + }; + let spent_by_me = &total_amount + &res.sol_required; + Ok(TransactionDetails { + tx: TransactionData::new_signed(serialized_tx.into(), tx.signatures[0].to_string()), + from: vec![coin.my_address.clone()], + to: vec![req.to], + total_amount: spent_by_me.clone(), + my_balance_change: &received_by_me - &spent_by_me, + spent_by_me, + received_by_me, + block_height: 0, + timestamp: now_sec(), + fee_details: Some( + SolanaFeeDetails { + amount: res.sol_required, + } + .into(), + ), + coin: coin.ticker.clone(), + internal_id: vec![].into(), + kmd_rewards: None, + transaction_type: TransactionType::StandardTransfer, + memo: None, + }) +} + +async fn withdraw_impl(coin: SolanaCoin, req: WithdrawRequest) -> WithdrawResult { + let validate_address_result = coin.validate_address(&req.to); + if !validate_address_result.is_valid { + return MmError::err(WithdrawError::InvalidAddress( + validate_address_result.reason.unwrap_or_else(|| "Unknown".to_string()), + )); + } + withdraw_base_coin_impl(coin, req).await +} + +type SolTxFut = Box + Send + 'static>; + +impl Transaction for SolTransaction { + fn tx_hex(&self) -> Vec { + serialize(self).unwrap_or_else(|e| { + error!("Error serializing SolTransaction: {}", e); + vec![] + }) + } + + fn tx_hash_as_bytes(&self) -> BytesJson { + let hash = match self.signatures.get(0) { + Some(signature) => signature, + None => { + error!("No signature found in SolTransaction"); + return BytesJson(Vec::new()); + }, + }; + BytesJson(Vec::from(hash.as_ref())) + } +} + +impl SolanaCoin { + pub async fn estimate_withdraw_fees(&self) -> Result<(solana_sdk::hash::Hash, u64), MmError> { + let hash = async_blocking({ + let coin = self.clone(); + move || coin.rpc().get_latest_blockhash() + }) + .await?; + let to = self.key_pair.pubkey(); + + let tx = solana_sdk::system_transaction::transfer(&self.key_pair, &to, LAMPORTS_DUMMY_AMOUNT, hash); + let fees = async_blocking({ + let coin = self.clone(); + move || coin.rpc().get_fee_for_message(tx.message()) + }) + .await?; + Ok((hash, fees)) + } + + pub async fn my_balance_spl(&self, infos: SplTokenInfo) -> Result> { + let token_accounts = async_blocking({ + let coin = self.clone(); + move || { + coin.rpc().get_token_accounts_by_owner( + &coin.key_pair.pubkey(), + TokenAccountsFilter::Mint(infos.token_contract_address), + ) + } + }) + .await?; + if token_accounts.is_empty() { + return Ok(CoinBalance { + spendable: Default::default(), + unspendable: Default::default(), + }); + } + let actual_token_pubkey = + Pubkey::from_str(&token_accounts[0].pubkey).map_err(|e| BalanceError::Internal(format!("{:?}", e)))?; + let amount = async_blocking({ + let coin = self.clone(); + move || coin.rpc().get_token_account_balance(&actual_token_pubkey) + }) + .await?; + let balance = + BigDecimal::from_str(&amount.ui_amount_string).map_to_mm(|e| BalanceError::Internal(e.to_string()))?; + Ok(CoinBalance { + spendable: balance, + unspendable: Default::default(), + }) + } + + fn my_balance_impl(&self) -> BalanceFut { + let coin = self.clone(); + let fut = async_blocking(move || { + // this is blocking IO + let res = coin.rpc().get_balance(&coin.key_pair.pubkey())?; + Ok(lamports_to_sol(res)) + }); + Box::new(fut.boxed().compat()) + } + + pub fn add_spl_token_info(&self, ticker: String, info: SplTokenInfo) { + self.spl_tokens_infos.lock().unwrap().insert(ticker, info); + } + + /// WARNING + /// Be very careful using this function since it returns dereferenced clone + /// of value behind the MutexGuard and makes it non-thread-safe. + pub fn get_spl_tokens_infos(&self) -> HashMap { + let guard = self.spl_tokens_infos.lock().unwrap(); + (*guard).clone() + } + + fn send_hash_time_locked_payment(&self, args: SendPaymentArgs<'_>) -> SolTxFut { + let receiver = Pubkey::new(args.other_pubkey); + let swap_program_id = Pubkey::new(try_tx_fus_opt!( + args.swap_contract_address, + format!( + "Unable to extract Bytes from args.swap_contract_address ( {:?} )", + args.swap_contract_address + ) + )); + let amount = sol_to_lamports(try_tx_fus_opt!( + args.amount.to_f64(), + format!("Unable to extract value from args.amount ( {:?} )", args.amount) + )); + let secret_hash: [u8; 32] = try_tx_fus!(<[u8; 32]>::try_from(args.secret_hash)); + let (vault_pda, vault_pda_data, vault_bump_seed, vault_bump_seed_data, rent_exemption_lamports) = + try_tx_fus!(self.create_vaults(args.time_lock, secret_hash, swap_program_id, STORAGE_SPACE_ALLOCATED)); + let swap_instruction = AtomicSwapInstruction::LamportsPayment { + secret_hash, + lock_time: args.time_lock, + amount, + receiver, + rent_exemption_lamports, + vault_bump_seed, + vault_bump_seed_data, + }; + + let accounts = vec![ + AccountMeta::new(self.key_pair.pubkey(), true), + AccountMeta::new(vault_pda_data, false), + AccountMeta::new(vault_pda, false), + AccountMeta::new(solana_program::system_program::id(), false), + ]; + self.sign_and_send_swap_transaction_fut(swap_program_id, accounts, swap_instruction.pack()) + } + + fn spend_hash_time_locked_payment(&self, args: SpendPaymentArgs) -> SolTxFut { + let sender = Pubkey::new(args.other_pubkey); + let swap_program_id = Pubkey::new(try_tx_fus_opt!( + args.swap_contract_address.as_ref(), + format!( + "Unable to extract Bytes from args.swap_contract_address ( {:?} )", + args.swap_contract_address + ) + )); + let secret: [u8; 32] = try_tx_fus!(<[u8; 32]>::try_from(args.secret)); + let secret_hash = sha256(secret.as_slice()).take(); + let (lock_time, tx_secret_hash, amount, token_program) = + try_tx_fus!(self.get_swap_transaction_details(args.other_payment_tx)); + if secret_hash != tx_secret_hash { + try_tx_fus_err!(format!( + "Provided secret_hash {:?} does not match transaction secret_hash {:?}", + secret_hash, tx_secret_hash + )); + } + let (vault_pda, vault_pda_data, vault_bump_seed, vault_bump_seed_data, _rent_exemption_lamports) = + try_tx_fus!(self.create_vaults(lock_time, secret_hash, swap_program_id, STORAGE_SPACE_ALLOCATED)); + let swap_instruction = AtomicSwapInstruction::ReceiverSpend { + secret, + lock_time, + amount, + sender, + token_program, + vault_bump_seed, + vault_bump_seed_data, + }; + let accounts = vec![ + AccountMeta::new(self.key_pair.pubkey(), true), + AccountMeta::new(vault_pda_data, false), + AccountMeta::new(vault_pda, false), + AccountMeta::new(solana_program::system_program::id(), false), + ]; + self.sign_and_send_swap_transaction_fut(swap_program_id, accounts, swap_instruction.pack()) + } + + fn refund_hash_time_locked_payment(&self, args: RefundPaymentArgs) -> SolTxFut { + let receiver = Pubkey::new(args.other_pubkey); + let swap_program_id = Pubkey::new(try_tx_fus_opt!( + args.swap_contract_address.as_ref(), + format!( + "Unable to extract Bytes from args.swap_contract_address ( {:?} )", + args.swap_contract_address + ) + )); + let (lock_time, secret_hash, amount, token_program) = + try_tx_fus!(self.get_swap_transaction_details(args.payment_tx)); + let (vault_pda, vault_pda_data, vault_bump_seed, vault_bump_seed_data, _rent_exemption_lamports) = + try_tx_fus!(self.create_vaults(lock_time, secret_hash, swap_program_id, STORAGE_SPACE_ALLOCATED)); + let swap_instruction = AtomicSwapInstruction::SenderRefund { + secret_hash, + lock_time, + amount, + receiver, + token_program, + vault_bump_seed, + vault_bump_seed_data, + }; + let accounts = vec![ + AccountMeta::new(self.key_pair.pubkey(), true), // Marked as signer + AccountMeta::new(vault_pda_data, false), // Not a signer + AccountMeta::new(vault_pda, false), // Not a signer + AccountMeta::new(solana_program::system_program::id(), false), //system_program must be included + ]; + self.sign_and_send_swap_transaction_fut(swap_program_id, accounts, swap_instruction.pack()) + } + + fn get_swap_transaction_details(&self, tx_hex: &[u8]) -> Result<(u64, [u8; 32], u64, Pubkey), Box> { + let transaction: SolTransaction = deserialize(tx_hex) + .map_err(|e| Box::new(TransactionErr::Plain(ERRL!("error deserializing tx_hex: {:?}", e))))?; + + let instruction = transaction + .message + .instructions + .get(0) + .ok_or_else(|| Box::new(TransactionErr::Plain(ERRL!("Instruction not found in message"))))?; + + let instruction_data = &instruction.data[..]; + let instruction = AtomicSwapInstruction::unpack(instruction_data[0], instruction_data) + .map_err(|e| Box::new(TransactionErr::Plain(ERRL!("error unpacking tx data: {:?}", e))))?; + + match instruction { + AtomicSwapInstruction::LamportsPayment { + secret_hash, + lock_time, + amount, + .. + } => Ok((lock_time, secret_hash, amount, Pubkey::new_from_array([0; 32]))), + AtomicSwapInstruction::SPLTokenPayment { + secret_hash, + lock_time, + amount, + token_program, + .. + } => Ok((lock_time, secret_hash, amount, token_program)), + AtomicSwapInstruction::ReceiverSpend { + secret, + lock_time, + amount, + token_program, + .. + } => Ok((lock_time, sha256(&secret).take(), amount, token_program)), + AtomicSwapInstruction::SenderRefund { + secret_hash, + lock_time, + amount, + token_program, + .. + } => Ok((lock_time, secret_hash, amount, token_program)), + } + } + + fn sign_and_send_swap_transaction_fut( + &self, + program_id: Pubkey, + accounts: Vec, + data: Vec, + ) -> SolTxFut { + let coin = self.clone(); + Box::new( + async move { coin.sign_and_send_swap_transaction(program_id, accounts, data).await } + .boxed() + .compat(), + ) + } + + pub async fn sign_and_send_swap_transaction( + &self, + program_id: Pubkey, + accounts: Vec, + data: Vec, + ) -> Result { + // Construct the instruction to send to the program + // The parameters here depend on your specific program's requirements + let instruction = Instruction { + program_id, + accounts, // Specify account metas here + data, // Pass data to the program here + }; + + // Create a transaction + let recent_blockhash = self + .client + .get_latest_blockhash() + .map_err(|e| TransactionErr::Plain(format!("Failed to get recent blockhash: {:?}", e)))?; + + let transaction: SolTransaction = SolTransaction::new_signed_with_payer( + &[instruction], + Some(&self.key_pair.pubkey()), //payer pubkey + &[&self.key_pair], //payer + recent_blockhash, + ); + + // Send the transaction + let tx = self + .client + .send_and_confirm_transaction(&transaction) + .map(|_signature| transaction) + .map_err(|e| TransactionErr::Plain(ERRL!("Solana ClientError: {:?}", e)))?; + + Ok(tx) + } + + fn create_vaults( + &self, + lock_time: u64, + secret_hash: [u8; 32], + program_id: Pubkey, + space: u64, + ) -> Result<(Pubkey, Pubkey, u8, u8, u64), Box> { + let seeds: &[&[u8]] = &[b"swap", &lock_time.to_le_bytes()[..], &secret_hash[..]]; + let (vault_pda, bump_seed) = Pubkey::find_program_address(seeds, &program_id); + + let seeds_data: &[&[u8]] = &[b"swap_data", &lock_time.to_le_bytes()[..], &secret_hash[..]]; + let (vault_pda_data, bump_seed_data) = Pubkey::find_program_address(seeds_data, &program_id); + + let rent_exemption_lamports = self + .client + .get_minimum_balance_for_rent_exemption( + space + .try_into() + .map_err(|e| Box::new(TransactionErr::Plain(ERRL!("unable to convert space: {:?}", e))))?, + ) + .map_err(|e| { + Box::new(TransactionErr::Plain(ERRL!( + "error get_minimum_balance_for_rent_exemption: {:?}", + e + ))) + })?; + + Ok(( + vault_pda, + vault_pda_data, + bump_seed, + bump_seed_data, + rent_exemption_lamports, + )) + } +} + +#[async_trait] +impl MarketCoinOps for SolanaCoin { + fn ticker(&self) -> &str { &self.ticker } + + fn my_address(&self) -> MmResult { Ok(self.my_address.clone()) } + + async fn get_public_key(&self) -> Result> { + Ok(self.key_pair.pubkey().to_string()) + } + + fn sign_message_hash(&self, _message: &str) -> Option<[u8; 32]> { unimplemented!() } + + fn sign_message(&self, message: &str) -> SignatureResult { solana_common::sign_message(self, message) } + + fn verify_message(&self, signature: &str, message: &str, pubkey_bs58: &str) -> VerificationResult { + solana_common::verify_message(self, signature, message, pubkey_bs58) + } + + fn my_balance(&self) -> BalanceFut { + let decimals = self.decimals as u64; + let fut = self.my_balance_impl().and_then(move |result| { + Ok(CoinBalance { + spendable: result.with_prec(decimals), + unspendable: 0.into(), + }) + }); + Box::new(fut) + } + + fn base_coin_balance(&self) -> BalanceFut { + let decimals = self.decimals as u64; + let fut = self + .my_balance_impl() + .and_then(move |result| Ok(result.with_prec(decimals))); + Box::new(fut) + } + + fn platform_ticker(&self) -> &str { self.ticker() } + + fn send_raw_tx(&self, tx: &str) -> Box + Send> { + let coin = self.clone(); + let tx = tx.to_owned(); + let fut = async_blocking(move || { + let bytes = hex::decode(tx).map_to_mm(|e| e).map_err(|e| format!("{:?}", e))?; + let tx: SolTransaction = deserialize(bytes.as_slice()) + .map_to_mm(|e| e) + .map_err(|e| format!("{:?}", e))?; + // this is blocking IO + let signature = coin.rpc().send_transaction(&tx).map_err(|e| format!("{:?}", e))?; + Ok(signature.to_string()) + }); + Box::new(fut.boxed().compat()) + } + + fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box + Send> { + let coin = self.clone(); + let tx = tx.to_owned(); + let fut = async_blocking(move || { + let tx = try_s!(deserialize(tx.as_slice())); + // this is blocking IO + let signature = coin.rpc().send_transaction(&tx).map_err(|e| format!("{:?}", e))?; + Ok(signature.to_string()) + }); + Box::new(fut.boxed().compat()) + } + + #[inline(always)] + async fn sign_raw_tx(&self, _args: &SignRawTransactionRequest) -> RawTransactionResult { + MmError::err(RawTransactionError::NotImplemented { + coin: self.ticker().to_string(), + }) + } + + fn wait_for_confirmations(&self, _input: ConfirmPaymentInput) -> Box + Send> { + unimplemented!() + } + + fn wait_for_htlc_tx_spend(&self, args: WaitForHTLCTxSpendArgs<'_>) -> TransactionFut { unimplemented!() } + + fn tx_enum_from_bytes(&self, _bytes: &[u8]) -> Result> { + MmError::err(TxMarshalingErr::NotSupported( + "tx_enum_from_bytes is not supported for Solana yet.".to_string(), + )) + } + + fn current_block(&self) -> Box + Send> { + let coin = self.clone(); + let fut = async_blocking(move || coin.rpc().get_block_height().map_err(|e| format!("{:?}", e))); + Box::new(fut.boxed().compat()) + } + + fn display_priv_key(&self) -> Result { Ok(self.key_pair.secret().to_bytes()[..].to_base58()) } + + fn min_tx_amount(&self) -> BigDecimal { BigDecimal::from(0) } + + fn min_trading_vol(&self) -> MmNumber { MmNumber::from("0.00777") } + + fn should_burn_dex_fee(&self) -> bool { unimplemented!() } + + fn is_trezor(&self) -> bool { unimplemented!() } +} + +#[async_trait] +impl SwapOps for SolanaCoin { + fn send_taker_fee(&self, dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionFut { unimplemented!() } + + fn send_maker_payment(&self, maker_payment: SendPaymentArgs) -> TransactionFut { + Box::new( + self.send_hash_time_locked_payment(maker_payment) + .map(TransactionEnum::from), + ) + } + + fn send_taker_payment(&self, taker_payment: SendPaymentArgs) -> TransactionFut { + Box::new( + self.send_hash_time_locked_payment(taker_payment) + .map(TransactionEnum::from), + ) + } + + async fn send_maker_spends_taker_payment( + &self, + maker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { + self.spend_hash_time_locked_payment(maker_spends_payment_args) + .compat() + .await + .map(TransactionEnum::from) + } + + async fn send_taker_spends_maker_payment( + &self, + taker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { + self.spend_hash_time_locked_payment(taker_spends_payment_args) + .compat() + .await + .map(TransactionEnum::from) + } + + async fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { + self.refund_hash_time_locked_payment(taker_refunds_payment_args) + .map(TransactionEnum::from) + .compat() + .await + } + + async fn send_maker_refunds_payment(&self, maker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { + self.refund_hash_time_locked_payment(maker_refunds_payment_args) + .map(TransactionEnum::from) + .compat() + .await + } + + fn validate_fee(&self, _validate_fee_args: ValidateFeeArgs) -> ValidatePaymentFut<()> { unimplemented!() } + + async fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentResult<()> { + unimplemented!() + } + + async fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentResult<()> { + unimplemented!() + } + + fn check_if_my_payment_sent( + &self, + _if_my_payment_sent_args: CheckIfMyPaymentSentArgs, + ) -> Box, Error = String> + Send> { + unimplemented!() + } + + async fn search_for_swap_tx_spend_my( + &self, + _: SearchForSwapTxSpendInput<'_>, + ) -> Result, String> { + unimplemented!() + } + + async fn search_for_swap_tx_spend_other( + &self, + _: SearchForSwapTxSpendInput<'_>, + ) -> Result, String> { + unimplemented!() + } + + fn check_tx_signed_by_pub(&self, tx: &[u8], expected_pub: &[u8]) -> Result> { + unimplemented!(); + } + + async fn extract_secret( + &self, + secret_hash: &[u8], + spend_tx: &[u8], + watcher_reward: bool, + ) -> Result, String> { + unimplemented!() + } + + fn is_auto_refundable(&self) -> bool { false } + + async fn wait_for_htlc_refund(&self, _tx: &[u8], _locktime: u64) -> RefundResult<()> { + MmError::err(RefundError::Internal( + "wait_for_htlc_refund is not supported for this coin!".into(), + )) + } + + fn negotiate_swap_contract_addr( + &self, + _other_side_address: Option<&[u8]>, + ) -> Result, MmError> { + unimplemented!() + } + + #[inline] + fn derive_htlc_key_pair(&self, _swap_unique_data: &[u8]) -> KeyPair { todo!() } + + #[inline] + fn derive_htlc_pubkey(&self, swap_unique_data: &[u8]) -> Vec { + self.derive_htlc_key_pair(swap_unique_data).public_slice().to_vec() + } + + fn validate_other_pubkey(&self, _raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { unimplemented!() } + + async fn maker_payment_instructions( + &self, + _args: PaymentInstructionArgs<'_>, + ) -> Result>, MmError> { + unimplemented!() + } + + async fn taker_payment_instructions( + &self, + _args: PaymentInstructionArgs<'_>, + ) -> Result>, MmError> { + unimplemented!() + } + + fn validate_maker_payment_instructions( + &self, + _instructions: &[u8], + _args: PaymentInstructionArgs<'_>, + ) -> Result> { + unimplemented!() + } + + fn validate_taker_payment_instructions( + &self, + _instructions: &[u8], + _args: PaymentInstructionArgs<'_>, + ) -> Result> { + unimplemented!() + } +} + +#[async_trait] +impl TakerSwapMakerCoin for SolanaCoin { + async fn on_taker_payment_refund_start(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } + + async fn on_taker_payment_refund_success(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } +} + +#[async_trait] +impl MakerSwapTakerCoin for SolanaCoin { + async fn on_maker_payment_refund_start(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } + + async fn on_maker_payment_refund_success(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } +} + +#[async_trait] +impl WatcherOps for SolanaCoin { + fn create_maker_payment_spend_preimage( + &self, + _maker_payment_tx: &[u8], + _time_lock: u64, + _maker_pub: &[u8], + _secret_hash: &[u8], + _swap_unique_data: &[u8], + ) -> TransactionFut { + unimplemented!(); + } + + fn send_maker_payment_spend_preimage(&self, _input: SendMakerPaymentSpendPreimageInput) -> TransactionFut { + unimplemented!(); + } + + fn create_taker_payment_refund_preimage( + &self, + _taker_payment_tx: &[u8], + _time_lock: u64, + _maker_pub: &[u8], + _secret_hash: &[u8], + _swap_contract_address: &Option, + _swap_unique_data: &[u8], + ) -> TransactionFut { + unimplemented!(); + } + + fn send_taker_payment_refund_preimage(&self, _watcher_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { + unimplemented!(); + } + + fn watcher_validate_taker_fee(&self, input: WatcherValidateTakerFeeInput) -> ValidatePaymentFut<()> { + unimplemented!(); + } + + fn watcher_validate_taker_payment(&self, _input: WatcherValidatePaymentInput) -> ValidatePaymentFut<()> { + unimplemented!(); + } + + fn taker_validates_payment_spend_or_refund(&self, _input: ValidateWatcherSpendInput) -> ValidatePaymentFut<()> { + unimplemented!() + } + + async fn watcher_search_for_swap_tx_spend( + &self, + input: WatcherSearchForSwapTxSpendInput<'_>, + ) -> Result, String> { + unimplemented!(); + } + + async fn get_taker_watcher_reward( + &self, + other_coin: &MmCoinEnum, + coin_amount: Option, + other_coin_amount: Option, + reward_amount: Option, + wait_until: u64, + ) -> Result> { + unimplemented!(); + } + + async fn get_maker_watcher_reward( + &self, + other_coin: &MmCoinEnum, + reward_amount: Option, + wait_until: u64, + ) -> Result, MmError> { + unimplemented!(); + } +} + +#[async_trait] +impl MmCoin for SolanaCoin { + fn is_asset_chain(&self) -> bool { false } + + fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.abortable_system) } + + fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut { + Box::new(Box::pin(withdraw_impl(self.clone(), req)).compat()) + } + + fn get_raw_transaction(&self, _req: RawTransactionRequest) -> RawTransactionFut { unimplemented!() } + + fn get_tx_hex_by_hash(&self, tx_hash: Vec) -> RawTransactionFut { unimplemented!() } + + fn decimals(&self) -> u8 { self.decimals } + + fn convert_to_address(&self, _from: &str, _to_address_format: Json) -> Result { unimplemented!() } + + fn validate_address(&self, address: &str) -> ValidateAddressResult { + if address.len() != 44 { + return ValidateAddressResult { + is_valid: false, + reason: Some("Invalid address length".to_string()), + }; + } + let result = Pubkey::try_from(address); + match result { + Ok(pubkey) => { + if pubkey.is_on_curve() { + ValidateAddressResult { + is_valid: true, + reason: None, + } + } else { + ValidateAddressResult { + is_valid: false, + reason: Some("not_on_curve".to_string()), + } + } + }, + Err(err) => ValidateAddressResult { + is_valid: false, + reason: Some(format!("{:?}", err)), + }, + } + } + + fn process_history_loop(&self, _ctx: MmArc) -> Box + Send> { unimplemented!() } + + fn history_sync_status(&self) -> HistorySyncState { unimplemented!() } + + /// Get fee to be paid per 1 swap transaction + fn get_trade_fee(&self) -> Box + Send> { unimplemented!() } + + async fn get_sender_trade_fee( + &self, + _value: TradePreimageValue, + _stage: FeeApproxStage, + _include_refund_fee: bool, + ) -> TradePreimageResult { + unimplemented!() + } + + fn get_receiver_trade_fee(&self, _stage: FeeApproxStage) -> TradePreimageFut { unimplemented!() } + + async fn get_fee_to_send_taker_fee( + &self, + _dex_fee_amount: DexFee, + _stage: FeeApproxStage, + ) -> TradePreimageResult { + unimplemented!() + } + + fn required_confirmations(&self) -> u64 { 1 } + + fn requires_notarization(&self) -> bool { false } + + fn set_required_confirmations(&self, _confirmations: u64) { unimplemented!() } + + fn set_requires_notarization(&self, _requires_nota: bool) { unimplemented!() } + + fn swap_contract_address(&self) -> Option { unimplemented!() } + + fn fallback_swap_contract(&self) -> Option { unimplemented!() } + + fn mature_confirmations(&self) -> Option { None } + + fn coin_protocol_info(&self, _amount_to_receive: Option) -> Vec { Vec::new() } + + fn is_coin_protocol_supported( + &self, + _info: &Option>, + _amount_to_send: Option, + _locktime: u64, + _is_maker: bool, + ) -> bool { + true + } + + fn on_disabled(&self) -> Result<(), AbortedError> { AbortableSystem::abort_all(&self.abortable_system) } + + fn on_token_deactivated(&self, _ticker: &str) { unimplemented!() } +} diff --git a/mm2src/coins/solana/spl.rs b/mm2src/coins/solana/spl.rs new file mode 100644 index 0000000000..c71a0315bd --- /dev/null +++ b/mm2src/coins/solana/spl.rs @@ -0,0 +1,600 @@ +use super::{CoinBalance, HistorySyncState, MarketCoinOps, MmCoin, SwapOps, TradeFee, TransactionEnum, WatcherOps}; +use crate::coin_errors::{MyAddressError, ValidatePaymentResult}; +use crate::solana::solana_common::{ui_amount_to_amount, PrepareTransferData, SufficientBalanceError}; +use crate::solana::{solana_common, AccountError, SolanaCommonOps, SolanaFeeDetails}; +use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinFutSpawner, ConfirmPaymentInput, DexFee, FeeApproxStage, + FoundSwapTxSpend, MakerSwapTakerCoin, MmCoinEnum, NegotiateSwapContractAddrErr, PaymentInstructionArgs, + PaymentInstructions, PaymentInstructionsErr, RawTransactionError, RawTransactionFut, + RawTransactionRequest, RawTransactionResult, RefundError, RefundPaymentArgs, RefundResult, + SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignRawTransactionRequest, + SignatureResult, SolanaCoin, SpendPaymentArgs, TakerSwapMakerCoin, TradePreimageFut, TradePreimageResult, + TradePreimageValue, TransactionData, TransactionDetails, TransactionFut, TransactionResult, + TransactionType, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, + ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, + ValidatePaymentInput, ValidateWatcherSpendInput, VerificationResult, WaitForHTLCTxSpendArgs, + WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, + WatcherValidateTakerFeeInput, WithdrawError, WithdrawFut, WithdrawRequest, WithdrawResult}; +use async_trait::async_trait; +use bincode::serialize; +use common::executor::{abortable_queue::AbortableQueue, AbortableSystem, AbortedError}; +use common::{async_blocking, now_sec}; +use futures::{FutureExt, TryFutureExt}; +use futures01::Future; +use keys::KeyPair; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use mm2_number::{BigDecimal, MmNumber}; +use rpc::v1::types::Bytes as BytesJson; +use serde_json::Value as Json; +use solana_client::{rpc_client::RpcClient, rpc_request::TokenAccountsFilter}; +use solana_sdk::message::Message; +use solana_sdk::transaction::Transaction; +use solana_sdk::{pubkey::Pubkey, signature::Signer}; +use spl_associated_token_account::{create_associated_token_account, get_associated_token_address}; +use std::{convert::TryFrom, + fmt::{Debug, Formatter, Result as FmtResult}, + str::FromStr, + sync::Arc}; + +#[derive(Debug)] +pub enum SplTokenCreationError { + InvalidPubkey(String), + Internal(String), +} + +impl From for SplTokenCreationError { + fn from(e: AbortedError) -> Self { SplTokenCreationError::Internal(e.to_string()) } +} + +pub struct SplTokenFields { + pub decimals: u8, + pub ticker: String, + pub token_contract_address: Pubkey, + pub abortable_system: AbortableQueue, +} + +#[derive(Clone, Debug)] +pub struct SplTokenInfo { + pub token_contract_address: Pubkey, + pub decimals: u8, +} + +#[derive(Debug)] +pub struct SplProtocolConf { + pub platform_coin_ticker: String, + pub decimals: u8, + pub token_contract_address: String, +} + +#[derive(Clone)] +pub struct SplToken { + pub conf: Arc, + pub platform_coin: SolanaCoin, +} + +impl Debug for SplToken { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { f.write_str(&self.conf.ticker) } +} + +impl SplToken { + pub fn new( + decimals: u8, + ticker: String, + token_address: String, + platform_coin: SolanaCoin, + ) -> MmResult { + let token_contract_address = solana_sdk::pubkey::Pubkey::from_str(&token_address) + .map_err(|e| MmError::new(SplTokenCreationError::InvalidPubkey(format!("{:?}", e))))?; + + // Create an abortable system linked to the `platform_coin` so if the platform coin is disabled, + // all spawned futures related to `SplToken` will be aborted as well. + let abortable_system = platform_coin.abortable_system.create_subsystem()?; + + let conf = Arc::new(SplTokenFields { + decimals, + ticker, + token_contract_address, + abortable_system, + }); + Ok(SplToken { conf, platform_coin }) + } + + pub fn get_info(&self) -> SplTokenInfo { + SplTokenInfo { + token_contract_address: self.conf.token_contract_address, + decimals: self.decimals(), + } + } +} + +async fn withdraw_spl_token_impl(coin: SplToken, req: WithdrawRequest) -> WithdrawResult { + let (hash, fees) = coin.platform_coin.estimate_withdraw_fees().await?; + let res = coin + .check_balance_and_prepare_transfer(req.max, req.amount.clone(), fees) + .await?; + let system_destination_pubkey = solana_sdk::pubkey::Pubkey::try_from(&*req.to)?; + let contract_key = coin.get_underlying_contract_pubkey(); + let auth_key = coin.platform_coin.key_pair.pubkey(); + let funding_address = coin.get_pubkey().await?; + let dest_token_address = get_associated_token_address(&system_destination_pubkey, &contract_key); + let mut instructions = Vec::with_capacity(1); + let account_info = async_blocking({ + let coin = coin.clone(); + move || coin.rpc().get_account(&dest_token_address) + }) + .await; + if account_info.is_err() { + let instruction_creation = create_associated_token_account(&auth_key, &dest_token_address, &contract_key); + instructions.push(instruction_creation); + } + let amount = ui_amount_to_amount(req.amount, coin.conf.decimals)?; + let instruction_transfer_checked = spl_token::instruction::transfer_checked( + &spl_token::id(), + &funding_address, + &contract_key, + &dest_token_address, + &auth_key, + &[&auth_key], + amount, + coin.conf.decimals, + )?; + instructions.push(instruction_transfer_checked); + let msg = Message::new(&instructions, Some(&auth_key)); + let signers = vec![&coin.platform_coin.key_pair]; + let tx = Transaction::new(&signers, msg, hash); + let serialized_tx = serialize(&tx).map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + let received_by_me = if req.to == coin.platform_coin.my_address { + res.to_send.clone() + } else { + 0.into() + }; + Ok(TransactionDetails { + tx: TransactionData::new_signed(serialized_tx.into(), tx.signatures[0].to_string()), + from: vec![coin.platform_coin.my_address.clone()], + to: vec![req.to], + total_amount: res.to_send.clone(), + spent_by_me: res.to_send.clone(), + my_balance_change: &received_by_me - &res.to_send, + received_by_me, + block_height: 0, + timestamp: now_sec(), + fee_details: Some( + SolanaFeeDetails { + amount: res.sol_required, + } + .into(), + ), + coin: coin.conf.ticker.clone(), + internal_id: vec![].into(), + kmd_rewards: None, + transaction_type: TransactionType::StandardTransfer, + memo: None, + }) +} + +async fn withdraw_impl(coin: SplToken, req: WithdrawRequest) -> WithdrawResult { + let validate_address_result = coin.validate_address(&req.to); + if !validate_address_result.is_valid { + return MmError::err(WithdrawError::InvalidAddress( + validate_address_result.reason.unwrap_or_else(|| "Unknown".to_string()), + )); + } + withdraw_spl_token_impl(coin, req).await +} + +#[async_trait] +impl SolanaCommonOps for SplToken { + fn rpc(&self) -> &RpcClient { &self.platform_coin.client } + + fn is_token(&self) -> bool { true } + + async fn check_balance_and_prepare_transfer( + &self, + max: bool, + amount: BigDecimal, + fees: u64, + ) -> Result> { + solana_common::check_balance_and_prepare_transfer(self, max, amount, fees).await + } +} + +impl SplToken { + fn get_underlying_contract_pubkey(&self) -> Pubkey { self.conf.token_contract_address } + + async fn get_pubkey(&self) -> Result> { + let coin = self.clone(); + let token_accounts = async_blocking(move || { + coin.rpc().get_token_accounts_by_owner( + &coin.platform_coin.key_pair.pubkey(), + TokenAccountsFilter::Mint(coin.get_underlying_contract_pubkey()), + ) + }) + .await?; + if token_accounts.is_empty() { + return MmError::err(AccountError::NotFundedError("account_not_funded".to_string())); + } + Ok(Pubkey::from_str(&token_accounts[0].pubkey)?) + } + + fn my_balance_impl(&self) -> BalanceFut { + let coin = self.clone(); + let fut = async move { + coin.platform_coin + .my_balance_spl(SplTokenInfo { + token_contract_address: coin.conf.token_contract_address, + decimals: coin.conf.decimals, + }) + .await + }; + Box::new(fut.boxed().compat()) + } +} + +#[async_trait] +impl MarketCoinOps for SplToken { + fn ticker(&self) -> &str { &self.conf.ticker } + + fn my_address(&self) -> MmResult { Ok(self.platform_coin.my_address.clone()) } + + async fn get_public_key(&self) -> Result> { unimplemented!() } + + fn sign_message_hash(&self, _message: &str) -> Option<[u8; 32]> { unimplemented!() } + + fn sign_message(&self, message: &str) -> SignatureResult { + solana_common::sign_message(&self.platform_coin, message) + } + + fn verify_message(&self, signature: &str, message: &str, pubkey_bs58: &str) -> VerificationResult { + solana_common::verify_message(&self.platform_coin, signature, message, pubkey_bs58) + } + + fn my_balance(&self) -> BalanceFut { + let fut = self.my_balance_impl().and_then(Ok); + Box::new(fut) + } + + fn base_coin_balance(&self) -> BalanceFut { self.platform_coin.base_coin_balance() } + + fn platform_ticker(&self) -> &str { self.platform_coin.ticker() } + + #[inline(always)] + fn send_raw_tx(&self, tx: &str) -> Box + Send> { + self.platform_coin.send_raw_tx(tx) + } + + #[inline(always)] + fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box + Send> { + self.platform_coin.send_raw_tx_bytes(tx) + } + + #[inline(always)] + async fn sign_raw_tx(&self, _args: &SignRawTransactionRequest) -> RawTransactionResult { + MmError::err(RawTransactionError::NotImplemented { + coin: self.ticker().to_string(), + }) + } + + fn wait_for_confirmations(&self, _input: ConfirmPaymentInput) -> Box + Send> { + unimplemented!() + } + + fn wait_for_htlc_tx_spend(&self, args: WaitForHTLCTxSpendArgs<'_>) -> TransactionFut { unimplemented!() } + + fn tx_enum_from_bytes(&self, _bytes: &[u8]) -> Result> { + MmError::err(TxMarshalingErr::NotSupported( + "tx_enum_from_bytes is not supported for Spl yet.".to_string(), + )) + } + + fn current_block(&self) -> Box + Send> { self.platform_coin.current_block() } + + fn display_priv_key(&self) -> Result { self.platform_coin.display_priv_key() } + + fn min_tx_amount(&self) -> BigDecimal { BigDecimal::from(0) } + + fn min_trading_vol(&self) -> MmNumber { MmNumber::from("0.00777") } + + fn is_trezor(&self) -> bool { self.platform_coin.is_trezor() } + + fn should_burn_dex_fee(&self) -> bool { unimplemented!() } +} + +#[async_trait] +impl SwapOps for SplToken { + fn send_taker_fee(&self, dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionFut { unimplemented!() } + + fn send_maker_payment(&self, _maker_payment_args: SendPaymentArgs) -> TransactionFut { unimplemented!() } + + fn send_taker_payment(&self, _taker_payment_args: SendPaymentArgs) -> TransactionFut { unimplemented!() } + + async fn send_maker_spends_taker_payment( + &self, + _maker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { + unimplemented!() + } + + async fn send_taker_spends_maker_payment( + &self, + _taker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { + unimplemented!() + } + + async fn send_taker_refunds_payment( + &self, + _taker_refunds_payment_args: RefundPaymentArgs<'_>, + ) -> TransactionResult { + unimplemented!() + } + + async fn send_maker_refunds_payment( + &self, + _maker_refunds_payment_args: RefundPaymentArgs<'_>, + ) -> TransactionResult { + todo!() + } + + fn validate_fee(&self, _validate_fee_args: ValidateFeeArgs) -> ValidatePaymentFut<()> { unimplemented!() } + + async fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentResult<()> { + unimplemented!() + } + + async fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentResult<()> { + unimplemented!() + } + + fn check_if_my_payment_sent( + &self, + _if_my_payment_sent_args: CheckIfMyPaymentSentArgs, + ) -> Box, Error = String> + Send> { + unimplemented!() + } + + async fn search_for_swap_tx_spend_my( + &self, + _: SearchForSwapTxSpendInput<'_>, + ) -> Result, String> { + unimplemented!() + } + + async fn search_for_swap_tx_spend_other( + &self, + _: SearchForSwapTxSpendInput<'_>, + ) -> Result, String> { + unimplemented!() + } + + fn check_tx_signed_by_pub(&self, tx: &[u8], expected_pub: &[u8]) -> Result> { + unimplemented!(); + } + + async fn extract_secret( + &self, + secret_hash: &[u8], + spend_tx: &[u8], + watcher_reward: bool, + ) -> Result, String> { + unimplemented!() + } + + fn is_auto_refundable(&self) -> bool { false } + + async fn wait_for_htlc_refund(&self, _tx: &[u8], _locktime: u64) -> RefundResult<()> { + MmError::err(RefundError::Internal( + "wait_for_htlc_refund is not supported for this coin!".into(), + )) + } + + fn negotiate_swap_contract_addr( + &self, + _other_side_address: Option<&[u8]>, + ) -> Result, MmError> { + unimplemented!() + } + + #[inline] + fn derive_htlc_key_pair(&self, _swap_unique_data: &[u8]) -> KeyPair { todo!() } + + #[inline] + fn derive_htlc_pubkey(&self, swap_unique_data: &[u8]) -> Vec { + self.derive_htlc_key_pair(swap_unique_data).public_slice().to_vec() + } + + fn validate_other_pubkey(&self, _raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { unimplemented!() } + + async fn maker_payment_instructions( + &self, + _args: PaymentInstructionArgs<'_>, + ) -> Result>, MmError> { + unimplemented!() + } + + async fn taker_payment_instructions( + &self, + _args: PaymentInstructionArgs<'_>, + ) -> Result>, MmError> { + unimplemented!() + } + + fn validate_maker_payment_instructions( + &self, + _instructions: &[u8], + _args: PaymentInstructionArgs<'_>, + ) -> Result> { + unimplemented!() + } + + fn validate_taker_payment_instructions( + &self, + _instructions: &[u8], + _args: PaymentInstructionArgs<'_>, + ) -> Result> { + unimplemented!() + } +} + +#[async_trait] +impl TakerSwapMakerCoin for SplToken { + async fn on_taker_payment_refund_start(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } + + async fn on_taker_payment_refund_success(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } +} + +#[async_trait] +impl MakerSwapTakerCoin for SplToken { + async fn on_maker_payment_refund_start(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } + + async fn on_maker_payment_refund_success(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } +} + +#[async_trait] +impl WatcherOps for SplToken { + fn send_maker_payment_spend_preimage(&self, _input: SendMakerPaymentSpendPreimageInput) -> TransactionFut { + unimplemented!(); + } + + fn create_taker_payment_refund_preimage( + &self, + _taker_payment_tx: &[u8], + _time_lock: u64, + _maker_pub: &[u8], + _secret_hash: &[u8], + _swap_contract_address: &Option, + _swap_unique_data: &[u8], + ) -> TransactionFut { + unimplemented!(); + } + + fn create_maker_payment_spend_preimage( + &self, + _maker_payment_tx: &[u8], + _time_lock: u64, + _maker_pub: &[u8], + _secret_hash: &[u8], + _swap_unique_data: &[u8], + ) -> TransactionFut { + unimplemented!(); + } + + fn send_taker_payment_refund_preimage(&self, _watcher_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { + unimplemented!(); + } + + fn watcher_validate_taker_fee(&self, _input: WatcherValidateTakerFeeInput) -> ValidatePaymentFut<()> { + unimplemented!(); + } + + fn watcher_validate_taker_payment(&self, _input: WatcherValidatePaymentInput) -> ValidatePaymentFut<()> { + unimplemented!(); + } + + fn taker_validates_payment_spend_or_refund(&self, _input: ValidateWatcherSpendInput) -> ValidatePaymentFut<()> { + unimplemented!() + } + + async fn watcher_search_for_swap_tx_spend( + &self, + input: WatcherSearchForSwapTxSpendInput<'_>, + ) -> Result, String> { + unimplemented!(); + } + + async fn get_taker_watcher_reward( + &self, + other_coin: &MmCoinEnum, + coin_amount: Option, + other_coin_amount: Option, + reward_amount: Option, + wait_until: u64, + ) -> Result> { + unimplemented!(); + } + + async fn get_maker_watcher_reward( + &self, + other_coin: &MmCoinEnum, + reward_amount: Option, + wait_until: u64, + ) -> Result, MmError> { + unimplemented!(); + } +} + +#[async_trait] +impl MmCoin for SplToken { + fn is_asset_chain(&self) -> bool { false } + + fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.conf.abortable_system) } + + fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut { + Box::new(Box::pin(withdraw_impl(self.clone(), req)).compat()) + } + + fn get_raw_transaction(&self, _req: RawTransactionRequest) -> RawTransactionFut { unimplemented!() } + + fn get_tx_hex_by_hash(&self, tx_hash: Vec) -> RawTransactionFut { unimplemented!() } + + fn decimals(&self) -> u8 { self.conf.decimals } + + fn convert_to_address(&self, _from: &str, _to_address_format: Json) -> Result { unimplemented!() } + + fn validate_address(&self, address: &str) -> ValidateAddressResult { self.platform_coin.validate_address(address) } + + fn process_history_loop(&self, _ctx: MmArc) -> Box + Send> { unimplemented!() } + + fn history_sync_status(&self) -> HistorySyncState { unimplemented!() } + + /// Get fee to be paid per 1 swap transaction + fn get_trade_fee(&self) -> Box + Send> { unimplemented!() } + + async fn get_sender_trade_fee( + &self, + _value: TradePreimageValue, + _stage: FeeApproxStage, + _include_refund_fee: bool, + ) -> TradePreimageResult { + unimplemented!() + } + + fn get_receiver_trade_fee(&self, _stage: FeeApproxStage) -> TradePreimageFut { unimplemented!() } + + async fn get_fee_to_send_taker_fee( + &self, + _dex_fee_amount: DexFee, + _stage: FeeApproxStage, + ) -> TradePreimageResult { + unimplemented!() + } + + fn required_confirmations(&self) -> u64 { 1 } + + fn requires_notarization(&self) -> bool { false } + + fn set_required_confirmations(&self, _confirmations: u64) { unimplemented!() } + + fn set_requires_notarization(&self, _requires_nota: bool) { unimplemented!() } + + fn swap_contract_address(&self) -> Option { unimplemented!() } + + fn fallback_swap_contract(&self) -> Option { unimplemented!() } + + fn mature_confirmations(&self) -> Option { Some(1) } + + fn coin_protocol_info(&self, _amount_to_receive: Option) -> Vec { Vec::new() } + + fn is_coin_protocol_supported( + &self, + _info: &Option>, + _amount_to_send: Option, + _locktime: u64, + _is_maker: bool, + ) -> bool { + true + } + + fn on_disabled(&self) -> Result<(), AbortedError> { self.conf.abortable_system.abort_all() } + + fn on_token_deactivated(&self, _ticker: &str) {} +} diff --git a/mm2src/coins/swap_features.rs b/mm2src/coins/swap_features.rs new file mode 100644 index 0000000000..7245f84969 --- /dev/null +++ b/mm2src/coins/swap_features.rs @@ -0,0 +1,30 @@ +/// Framework to activate new swap protocol features at certain protocol version + +#[derive(PartialEq)] +pub enum LegacySwapFeature { + // Sending part of dex fee to a dedicated account to exchange it on KMD coins and burn them + SendToPreBurnAccount, +} + +impl LegacySwapFeature { + /// Features activated/deactivated by protocol version. Tuple elements are: + /// element.0 is version when the feature is activated, + /// element.1 is version when the feature is discontinued, + /// element.2 is a feature itself, registered in the LegacySwapFeature enum + const SWAP_FEATURE_ACTIVATION: &[(u16, Option, LegacySwapFeature)] = &[ + (1, None, LegacySwapFeature::SendToPreBurnAccount), + // add more features to activate... + ]; + + /// Returns true if feature is active for the protocol version param + pub fn is_active(feature: LegacySwapFeature, version: u16) -> bool { + if let Some(found) = Self::SWAP_FEATURE_ACTIVATION.iter().find(|fv| fv.2 == feature) { + return version >= found.0 + && found + .1 + .map(|ver_discontinued| version < ver_discontinued) + .unwrap_or(true); + } + false + } +} diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 5b4e7953a7..076c6c0a81 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -36,10 +36,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, 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::tendermint::v1beta1::{GetBlockByHeightRequest, GetBlockByHeightResponse, GetLatestBlockRequest, GetLatestBlockResponse}; use cosmrs::proto::cosmos::base::v1beta1::Coin as CoinProto; @@ -81,6 +82,9 @@ use std::str::FromStr; use std::sync::{Arc, Mutex}; use uuid::Uuid; +#[cfg(any(test, feature = "mocktopus"))] +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"; @@ -627,6 +631,7 @@ impl TendermintCommons for TendermintCoin { } } +#[cfg_attr(any(test, feature = "mocktopus"), mockable)] impl TendermintCoin { #[allow(clippy::too_many_arguments)] pub async fn init( @@ -638,7 +643,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, @@ -938,7 +943,9 @@ impl TendermintCoin { memo: String, 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(); @@ -1023,7 +1030,9 @@ impl TendermintCoin { memo: String, 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); }; @@ -1095,9 +1104,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 @@ -1393,6 +1403,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() @@ -1542,8 +1553,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], @@ -1551,20 +1561,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 { @@ -1601,8 +1647,7 @@ impl TendermintCoin { &self, fee_tx: &TransactionEnum, expected_sender: &[u8], - fee_addr: &[u8], - amount: &BigDecimal, + dex_fee: &DexFee, decimals: u8, uuid: &[u8], denom: String, @@ -1621,66 +1666,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 ))); } @@ -1780,6 +1799,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.to_string() { + 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.to_string() { + 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.to_string() { + 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.to_string() { + 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.to_string() { + 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, @@ -1978,9 +2143,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 @@ -2680,6 +2845,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(), @@ -2691,17 +2859,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 { @@ -2868,8 +3029,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(), @@ -3322,10 +3482,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, + } + } + #[test] fn test_tx_hash_str_from_bytes() { let tx_hex = "0a97010a8f010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e64126f0a2d636f736d6f7331737661773061716334353834783832356a753775613033673578747877643061686c3836687a122d636f736d6f7331737661773061716334353834783832356a753775613033673578747877643061686c3836687a1a0f0a057561746f6d120631303030303018d998bf0512670a500a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a2102000eef4ab169e7b26a4a16c47420c4176ab702119ba57a8820fb3e53c8e7506212040a020801180312130a0d0a057561746f6d12043130303010a08d061a4093e5aec96f7d311d129f5ec8714b21ad06a75e483ba32afab86354400b2ac8350bfc98731bbb05934bf138282750d71aadbe08ceb6bb195f2b55e1bbfdddaaad"; @@ -3689,19 +3862,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], @@ -3718,22 +3892,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], @@ -3745,26 +3928,37 @@ 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 pubkey = dex_fee_tx.auth_info.as_ref().unwrap().signer_infos[0] + 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_response + .tx + .as_ref() + .unwrap() + .auth_info + .as_ref() + .unwrap() + .signer_infos[0] .public_key .as_ref() .unwrap() .value[2..] .to_vec(); + 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], @@ -3782,7 +3976,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], @@ -3799,7 +3992,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], @@ -3813,10 +4005,17 @@ 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 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(); - let pubkey = fee_with_memo_tx.auth_info.as_ref().unwrap().signer_infos[0] + let pubkey = fee_with_memo_tx_response + .tx + .as_ref() + .unwrap() + .auth_info + .as_ref() + .unwrap() + .signer_infos[0] .public_key .as_ref() .unwrap() @@ -3824,20 +4023,94 @@ pub mod tendermint_coin_tests { .to_vec(); 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(), }); + TendermintCoin::request_tx.mock_safe(move |_, _| { + let mock_tx = mock_tx.clone(); + MockResult::Return(Box::pin(async move { Ok(mock_tx) })) + }); 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("0abd030a91030a212f636f736d6f732e62616e6b2e763162657461312e4d73674d756c746953656e6412eb020a770a2a6e7563316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c65647736337912490a446962632f4637463238464633433039303234413032323545444242444232303745353837324432423445463246423837344645343742303545463943394137443231314312013912770a2a6e7563316567307167617a37336a737676727676747a713478383233686d7a387161706c656877326b3212490a446962632f4637463238464633433039303234413032323545444242444232303745353837324432423445463246423837344645343742303545463943394137443231314312013712770a2a6e756331346e7938336a6d306637303430357435726a7039736b796c6c77366876687a356e356370796612490a446962632f46374632384646334330393032344130323235454442424442323037453538373244324234454632464238373446453437423035454639433941374432313143120132122433376339323861362d393161382d346466312d616536372d663636616537323538326338188c0912680a500a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21025a37975c079a7543603fcab24e2565a4adee3cf9af8934690e103282fa40251112040a020801180312140a0e0a05756e75636c1205333338383510c8d0071a4018c1ce9472bd9e99e6e9d181d3be2d7aa1b56da030de4bf02f8fcb867b1a1ca47d64f7795bd7891923a13774da28723a76c531cde13a0cffc5a17c8ccd371ed5").unwrap().as_slice()).unwrap(); + let mock_tx = fee_with_burn_tx.clone(); + + let pubkey = fee_with_burn_tx.auth_info.as_ref().unwrap().signer_infos[0] + .public_key + .as_ref() + .unwrap() + .value[2..] + .to_vec(); + + let fee_with_burn_cosmos_tx = TransactionEnum::CosmosTransaction(CosmosTransaction { + data: TxRaw::decode(fee_with_burn_tx.encode_to_vec().as_slice()).unwrap(), + }); + + TendermintCoin::request_tx.mock_safe(move |_, _| { + let mock_tx = mock_tx.clone(); + MockResult::Return(Box::pin(async move { Ok(mock_tx) })) + }); + let uuid: Uuid = "37c928a6-91a8-4df1-ae67-f66ae72582c8".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 a2df5bc117..5329a4a1f3 100644 --- a/mm2src/coins/tendermint/tendermint_token.rs +++ b/mm2src/coins/tendermint/tendermint_token.rs @@ -104,16 +104,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 } @@ -181,8 +174,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(), @@ -479,6 +471,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 8f1dd0a508..f03767bf5b 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 = "mocktopus"))] 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 = "mocktopus"), 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 = "mocktopus"), 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 = "mocktopus"), 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 = "mocktopus"), 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 = "mocktopus"), mockable)] impl TakerCoinSwapOpsV2 for TestCoin { async fn send_taker_funding(&self, args: SendTakerFundingArgs<'_>) -> Result { todo!() } diff --git a/mm2src/coins/utxo/bch.rs b/mm2src/coins/utxo/bch.rs index a700ec0b5a..43054c6241 100644 --- a/mm2src/coins/utxo/bch.rs +++ b/mm2src/coins/utxo/bch.rs @@ -870,16 +870,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] @@ -937,9 +929,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 @@ -1276,6 +1267,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 106258ea55..fa7d0c37a6 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 @@ -824,6 +815,7 @@ impl WatcherOps for QtumCoin { } #[async_trait] +#[cfg_attr(test, mockable)] impl MarketCoinOps for QtumCoin { fn ticker(&self) -> &str { &self.utxo_arc.conf.ticker } @@ -897,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 b1e7f1ef4c..6667123ed5 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 @@ -1212,21 +1210,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); @@ -1352,7 +1346,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 91116e109a..1b686a88ed 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}; @@ -62,6 +63,8 @@ use std::cmp::Ordering; use std::collections::hash_map::{Entry, HashMap}; 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; @@ -69,7 +72,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"; @@ -1238,43 +1241,68 @@ 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 maker_value = args + .taker_tx + .first_output() + .map_to_mm(|e| TxGenError::PrevTxIsNotValid(e.to_string()))? + .value + - dex_fee_value + - tx_fee; + // 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( @@ -1289,7 +1317,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, @@ -1306,7 +1334,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( @@ -1349,7 +1377,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( @@ -1400,6 +1428,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)); @@ -1433,7 +1463,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); @@ -1460,47 +1490,92 @@ 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 = 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(), + )?; + let burn_address = 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(), + )?; + + let fee_amount = dex_fee + .fee_amount_as_u64(coin.as_ref().decimals) + .map_err(|err| err.get_inner().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.get_inner().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 @@ -2036,7 +2111,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 +2121,7 @@ pub fn watcher_validate_taker_fee( let taker_fee_hash = input.taker_fee_hash; let min_block_number = input.min_block_number; let lock_duration = input.lock_duration; - let fee_addr = input.fee_addr.to_vec(); - + let dex_pubkey = coin.dex_pubkey().to_vec(); let fut = async move { let mut attempts = 0; loop { @@ -2097,7 +2171,7 @@ pub fn watcher_validate_taker_fee( } let address = address_from_raw_pubkey( - &fee_addr, + &dex_pubkey, coin.as_ref().conf.address_prefixes.clone(), coin.as_ref().conf.checksum_type, coin.as_ref().conf.bech32_hrp.clone(), @@ -2129,23 +2203,102 @@ 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 output {:?} 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, + let dex_address = try_f!(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(), ) - .map_to_mm(ValidatePaymentError::TxDeserializationError)); + .map_to_mm(ValidatePaymentError::InternalError)); + let burn_address = try_f!(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(), + ) + .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 { @@ -2157,9 +2310,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() @@ -2187,58 +2337,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 - ))); - } + match dex_fee { + DexFee::NoFee => {}, + DexFee::Standard(fee_amount) => { + validate_dex_output(&coin, &tx, output_index, &dex_address, &fee_amount)?; }, - None => { - return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "Provided dex fee tx {:?} does not have output {}", - tx, output_index - ))) - }, - } - - 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()) @@ -2971,6 +3091,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 @@ -3974,11 +4096,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 @@ -5185,42 +5305,125 @@ 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 = 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[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 = 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(); + let burn_address = 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(), + ) + .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_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index d72c3d4963..5f2864e2b6 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 @@ -969,6 +962,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 373fd29305..9d0bbe5b01 100644 --- a/mm2src/coins/utxo/utxo_tests.rs +++ b/mm2src/coins/utxo/utxo_tests.rs @@ -43,6 +43,7 @@ use futures::future::{join_all, Either, FutureExt, TryFutureExt}; use keys::prefixes::*; use mm2_core::mm_ctx::MmCtxBuilder; 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}; @@ -2675,6 +2676,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); @@ -2686,7 +2709,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: &[], @@ -2711,7 +2733,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: &[], @@ -2740,7 +2761,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 07462d2a07..9e247fe17b 100644 --- a/mm2src/coins/z_coin.rs +++ b/mm2src/coins/z_coin.rs @@ -86,7 +86,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, @@ -131,6 +131,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"; @@ -201,6 +202,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, @@ -671,6 +673,33 @@ impl ZCoin { Ok(()) } + + /// 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 { @@ -835,6 +864,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, @@ -890,6 +920,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(), @@ -937,6 +974,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), @@ -1068,6 +1106,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( @@ -1090,6 +1130,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 } @@ -1208,20 +1251,16 @@ impl MarketCoinOps for ZCoin { fn is_privacy(&self) -> bool { true } + fn should_burn_dex_fee(&self) -> bool { true } + 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()) } @@ -1376,6 +1415,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(), @@ -1386,7 +1427,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(); @@ -1420,40 +1462,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/z_coin_native_tests.rs b/mm2src/coins/z_coin/z_coin_native_tests.rs index f554d7c5d3..4efad1420a 100644 --- a/mm2src/coins/z_coin/z_coin_native_tests.rs +++ b/mm2src/coins/z_coin/z_coin_native_tests.rs @@ -1,5 +1,29 @@ +//! 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 common::{block_on, now_sec, one_thousand_u32}; use mm2_core::mm_ctx::MmCtxBuilder; use mm2_test_helpers::for_tests::zombie_conf; use std::path::PathBuf; @@ -9,12 +33,12 @@ 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; -#[test] -fn zombie_coin_send_and_refund_maker_payment() { +#[tokio::test] +async fn zombie_coin_send_and_refund_maker_payment() { let ctx = MmCtxBuilder::default().into_mm_arc(); let mut conf = zombie_conf(); let params = default_zcoin_activation_params(); @@ -26,7 +50,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, @@ -35,11 +59,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 { @@ -49,12 +79,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 { @@ -65,15 +95,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] +async fn zombie_coin_send_and_spend_maker_payment() { let ctx = MmCtxBuilder::default().into_mm_arc(); let mut conf = zombie_conf(); let params = default_zcoin_activation_params(); @@ -85,7 +115,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, @@ -94,11 +124,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); @@ -109,33 +148,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] +async fn zombie_coin_send_dex_fee() { let ctx = MmCtxBuilder::default().into_mm_arc(); let mut conf = zombie_conf(); let params = default_zcoin_activation_params(); @@ -147,22 +184,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] +async fn zombie_coin_send_standard_dex_fee() { + let ctx = MmCtxBuilder::default().into_mm_arc(); + let mut conf = zombie_conf(); + let params = default_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(); @@ -193,8 +252,8 @@ fn prepare_zombie_sapling_cache() { } } -#[test] -fn zombie_coin_validate_dex_fee() { +#[tokio::test] +async fn zombie_coin_validate_dex_fee() { let ctx = MmCtxBuilder::default().into_mm_arc(); let mut conf = zombie_conf(); let params = default_zcoin_activation_params(); @@ -206,36 +265,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/78b24c5093cb5de007bd8b34f4ec5c26e88862199dd7b38fc3e0bf60c3c620a9 + let tx_hex = "0400008085202f890000000000005efc0600e80300000000000001745e8e55063d50f286b2d8665b860fb4d489acdd41fa41da6eccbbe871f4854ee4f12460119a7b54c792136c29daf0a848aeafd89630f38926f1c666366bc02433523951dd53294409067bed250213f583d610cb141afe73113f83a2098e616befd22deba3d11cb4246e5a3cb35dfec42466bf4aafc691026c1e560d990354ab9855303c2a70003662a101ecf35b5adbf8bf003832bec5e4941822c619e04402da405bedac41a95a47929ed30ee16e57825b34c711b611872ce946b9621eb6498ae40d7604311a5436a4f2da5c12ef83dee790470dff088cb88277a29a259b8d04e145cd8ad80f2fb6c4720d194d83427b6b78f915b070e91bbcbf84a5d4dec4757cdb19037d4b76307452153608e015b915a7b29f2c306e70650d1e891a0f6469860dba690b1659f6ec38bdc2d5ed23f12c1d9eb78dc62632fbddfa7aaf164b24572e381f28e33259d9078dd3bc896e22d0701efc68f7dc4487c392d994a062fedce5abcd0c230dfbd8c4e7c2e6b4784a4313593acf76d9e791db65c3b3270e036be3fd8dd5bc597fb5f3f48602e7219807060e27209d56b24f17d5a26da5d7307d18780194def01212a511f8470589584278bffd82ffff015e5de4d1a3c01e482995d1cece7efacfbfb8d7780abae18f88ae9c5789e54c6248c00bb1caa4a16145e8d08e9f90f93d5b12aac8e7fec7dce114bf77ad8a0b927ead9f7ea46dfd23a52111478ead12cc7a54e118ac51f3624f8e5923778f6274ca361247965f42332ae30c1f498e7acb4b89bf508802127cf0952bf2a31fc6b6aafda6f477ab98d01a01c912b4e0d0ca9c535c2e731e4f58c36066e1c631cc703773fa204d5db897d6a8da2c0af846979984a9e8bbf0113b94b233a7cfb7b878ecd4794bbb99a3d8cafba4204c2633501b8215adb1100aae3c21486ca126b913326fc6867306e50414391333ef2c1e1432ba7315b1dd0d744854f4e7ff7b742b5861b8cb59927183284d2db3f38aef52b244e391b059fc1057938f4c7081c3fd208b78714f3ae235db3f4db431c7fbcc446469f811e387f82fd6008e4b6a1b0bbd4760d1e9ad3111884cf83820058015ddfbd89588816f1b1016b27d41b40faf7f94d53e9925edf2e4dcec23554ee21a9ef3bfd185498b16c6b7b4a7c79f46e256ba498e465468304bf8827432ffbf93bd988f90e77d9bcc72ef729aac729ef25e8eeaabb6d21c4bf320c04158572085cf6293ddfb183a9936e9205ea1dc8175777c66bdd762f52b62a554033e338b2f2ea9511ce7a0448b046ecb03258d95d4743c0c80b9dbae49f69250b7021368d3855526ae0440d55388653912f2f86ddf8128697c1c9155a16bd4e5632c8c35b650b37aeb61d57f0cf1606459ae4472f4c4a721c5e1b78144557f7ca641cc2c04c9760e9d27d7730231f0e2d2f8fd118292fbe420dd69cdf95ead77f01791f9be4e91e1379a27b9c042ac8a25978e9cf77fd9b63c443ac218ec16a1fd96e208f1558746eb92704618ea787b33c3731c146c5a485d2398e41b25742c54b70d081850b812d07a145d191023852f74bb40e00656f9a8e25bef21e6436d65f05354c84369d05c7e4197832cbe460d80fc7b4d202c7a6e4646730c372f4e0b179225d033d19571a0c090823863c88148c8b0b9a68177a5f24ffc698a4d7c0a9756ec06d9b5ab53be48ce2a9ff744951afe354c271f9ff2d11bda37eccaefc79f28b9aef3cf3e4107fb9e18b9cf9a3d0d810b713ed2648b7787b98c592a6be3505c0d053ebb5786bb90dc3d6f3bd6d0c5292f86bfad170e3b4b56921bd897cbaee97d58d7ce4a891dbad972a7f968d72224c7acb89a25f8b828b4e2e3cc7f7fbc8b902667120870ed0ad7c2e5347d2b7d8a863fe89bb5e1573f5d8dd2e26e1ffc07cc8f95b5640e670941ba9ee83c6c1287c8ba0dc73d9beab0d726a4db82eff96c8c932ff1d3f24206faef182f784328bf9e554e3ee4bb38857c8ae43ecee421f13ab113e6a561ef78738bcda577671a3dee7ce4be41cd37562f64c0616c6640dec87452cd30ebe3316f6d40be064392f0e620be3371f74838432e08a757aa0d157634bcb2d3d23ded907aef74780f7eed1becdcd05ddc91b8f7ece2504c2a5fd261c67a6200e3efaa39893ab5778243b8d70f83806d54654fb8307bb23f7edef133e6f6eb8bdf01ba307be546568f4f0447e0e582b9ad908ce6a04f4453817ca2fc542a8adb7512260e1012c20e4ec425b7e2806becf14ac84594800f76056c3d2fd4edc498442b43909ef3b007c4db189691b9726c17ea5e9a9ef262890252e193440abdd7e2901134bee4de58d4866a7dc69a302ac953c318cdad842a5a4c7c1341f772519ad350e53f23b41996ab0d3c1edecc0bee7f86b9add873bdfa4134ec9993782d5fd0299a17de6fb774b55f45a0c7051e365b30e2e23ca39b405c9f6b58927fbf2ccca777d888439a8ef43f99a64cedf321522c38283b2cefde744bc176a0505ec23d810bc4474b0a9dfa2aca0032608559559fb978fe180e727d68a73234519ea545867307b2d9c3bbff9070dc7d5117de318aee94fe7a909f846a46a31a0caf101bf5625dde52708f5b2189097f5525ba0c44234bc4705bc24fcbb44d43d09dc74a389cc4ea32cb82bf3578abe3943182c1e8acbc7bbe4d2e151a5481e28e34b0621bb85d1bd7a2778fdada261172059d305538fd4ab908cf29743fc3fcefcc5774872c18b70c77b38a9343ac331174aa0b5266c7ba68d082fe550b0cecf4d4b99529c492e8220fb0d87160b33431bdeb40393f45bdfc9c5d781f718c9e4a598834c74dfcb86aa7ac5f1ae7c88052c420c5ba65e1f09015d0b2c420d87fa10ba802c3cd05b7f35c0daac0098aa9865fa0a6ca4da10f4cec3fe62f2b50b1f77fabdb16d6f779f08ea30e746b8c29388f41c70ef33e309ffb4d08e51f27893b3f85ca57d1c7d3f73bf14c76e03d1ad215db84c3b6ded38d84bffe4c79951aa0b689a10b395ba5a834f0766cca5e9110419d944f691df22ea46e370b2931e3ce8be06caefedeaf6cc6095c2cd654200d92861c80137ca55f7cc5939c31af177426e2dcba53e75202b7e087c32679ca38832bdf535bea854302ff6262296a0b3ceb1b5d6d38902e8be64728f4224ab8aefc20cc2e5a9be85e319e546cc7d9649048ffebb16bb4ffb2a1e7ccb50b5e12283ee7a68d48f37f84fafb3ad0d2420d4cd1796bb5fe9ff55734b61847b6f99094563b27b780910a00c0f968f6465639e9362e9122b41948083c7b7a9815094e6a780052fb1b2f20b0af5b53868b2bb209e5589aefc6b376b7edd1200546bfc51dfd3bda62c9ea8eb79f1b75cab4d67d0cdad6bba2d14daa00019735b37c17a71b83f905d7e78cde70632111f69864361598bb83c78c75e857bb39fddbaee6c299cdffcd1244b20fc07fbe880c35ab926f1504d7a93431ee942efcbd6e3b86da2cb4f0c03d51bfadec4ca15564d54a1b9cf6fe57cc5754a00d75a5b5eb2f5b107d22bed8819a8ac28bc77b8b21aa40ad65decd368c690b174c05d5eb5dfd4eba825779c767807f8e45708b12b6e38eafea6efd3a0d64e740d0a9117d2e82ee94bc92929a5d87dbfcc6e7e3ff1be6d54f0236c90e8c918833c7ebeadc7430417b88769c1dd06dca5e53a635cc67a9b46d3dfa4505c51a63739a6523f8c18ec2b9ccb4f36c6f3b532cc27b15038ccb62a20cfaae6707f9c972ed826792ed2e41c2a58187b87780989f3b3f29efa1054e08593304d5ea5cb267dac61131874c4caa34bd6cd2a22569e6eb9ccebfc249750d96b6354c233fdc292005f04f7823c3b87420020c36829c6959ae338185f97074697eef433f319f396bb486b69520fdf91f110040f3babf1cf4a5e88c09cd2ebb9685bf2e532776165daed9abfb7a7066a78541467a7c119de71a29414459beb0a97bdebac9f8ec9bbdf17346ddf79fc55cdbab93d32b2db46519e6495721f8bcf512245fcec8c00fe9a428d5f2fe83a8b7a63d2bec01c62ba635901066328d10f93e9737d986c7137bc8459fdfa69d6fa41e1029f117402df57070b3c3e1993be762d75c7ed39a0d435c71dcec6255d3cca5eca300782c0e795b90da33a202bf00020fad8b254ca0410dc4bb49fb28005b79796ece98b7c0aba1dead4a1e8ae010dc233fac9c5d614cc3550dca31e46bc4e51997c435846c01ade5a9b3f09379690e1fbc85df24fa3ca17c8274daee7db84043b8f671a81d2413ccc8c1bde2e2cd4aa7bef8b86d8221b6602e6ec2b996058b7db0207f29f150386847a9625d66288db397addb96b701a0522091a9ab28c164f28cc1d92ba740fd46bee92b0bcd1b1de18603c6767e73158e47ab34e2624ec06b6fba66b09353893add65be3e60112d22a8c10b1c46f89b979c649b686b18db83164d351d48e23c95cefcd1247fafd7a5f6b8c346a20ed1b95af1051fea86618eb7569e7e5765b6d0cd1c4d7d77f0483da78680de98ca8b3a13c2717e1904af02b07ec1eb23d339adcce34c4a14e1fa9ea86195eb0b0faac8a7241d9ec7bfbe0e006cf002aca2ff875a18541405f73d5c439417a53711bf2639a1d7debd16de9b327e8c56a009b7561a223629c1ef2e13c384b928347a92b873ec988fae2a8de004"; 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), } @@ -243,42 +300,75 @@ 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/1d7db42155ed1ace74ca00dbf21ac4108f2d25baf83accf9fffd3c4d1d4239b6 + let tx_2_hex = "0400008085202f8900000000000034fc0600e80300000000000001a18aa4c3884d0d1a2123cba24bed5882c1f97fccfc7c14e2276504554e046f268e028917a6bda8a84bb4659da995048ea8f33aaab14954a7a67072993930662a15372c4dcf6e33684e35611b2e69fbee3c318519af6c2b0d8fbf035ada9fcf0145d3f14afa06481c9cb1cf0ac5a8b5b85a6753de2e1892ee5fe17f13599e00e0a4d59591030d007e43b9604da70037a07b8563a241dcb31efe0379996b5fec1b6894af965704d175032c3b0cfd311662b156900aadb76db5d9631d81350cc399e66138c9a51258a7f10922d0a2a2c3530b63486ee79e83ab1013701e9317f1cf12d4d03f5062932f5c8a50b40eb8768f3cd917cd75acba59b4b35c9ca9fa01c74b18eed47e614f06ff93b9451cb0547288a7b6689464d856f29cbba463434ff584ac982ab9e30be34d78c5ce43e35fb218cf3a3fbbcc0be787c2fe8bc81fbbc4b4bf3a9a91d74c7702cab77f7292fea24d6ef17b63f29ec73053e7b5c2c40b3719c1a9a7d0070b1bddf712aedf8b6165e29cc4b71ddce73dc4752b4551566a0102c8e8391fbcd124d1e2b1cca28c3fd2da2c7fd36cd3134e3e7cbb9323a600ea06256d5ce0da0efc8bfe90ab15406c763d50de9fe7daf6aa6ff90db6fc6236414447177f7b8ff792322b70bb2ef11eb400ab9609f7099ef393232b4bc31bc76485e80d24e8961e03ef542dafbf2ebc4138de6ab18e9afd468c31a35f7cdd8822f18ca7404795e95296eb31baeb8305faa05f0c8c08f48b5ff071c7f5479a2181d33abe8c6c172988cca1fe24921664b0ce3caab56ae8548bc542e867990c6ebe49483939cb85702d58cbe40fe52e34bc57c91795625f869430009bfff50cd96e7a548dac852700751a4160ebe71a5639abf6e4efa7e8fd1946bea7d7dab3ce28921f1371d3ecd8540429a36f2175f40ff96c0f72a3b18429e83a8cb3259db864044eb0a6a481233108109cc6c9c798976d2abdc6cb62252b18262b05736fc557e214de4259c5c856c3c2d022b18d0782135b387e9ade580a432c1a8fec86deadc287f09101583b3e30c55024c72fc78bb8ce453541b34d6bf0252d675d48be06e84c41125f5110c36e0cbd113931bf40838b926e6611e60bc6741c1779fec015da4547f71e42af65e1814b39179e062a095765061e56778ab4ba0892a4b3160045672b249fc5350174b195fa323f87c781a5ec71af9f17c4208ce9172717eb9e187c9005d2e8db184d95eb6efd252804159db9f2a1c227b516ee7ebc958a593892b927f958a35d8ebe8edd20acf62920adb4344a673beb4ec350ce1cb3c7bf7c5008ef857cb4e1c32040710b304863d6de29017717e90748c37f4182b30a5175b991bdc8358982659c1944e5391e344f60ab6870d9f75e407345f8bf89cfc7ecf38b8a021fd747e4f58c7117c240c6043759cffe6ec30417b6bad46ac6638bd62f68995f3f24293c0ddfbe2c5bf50b1d1e9c1c07a000f4eb3bada725bc9867551b93b5df49aa0dcc2890d3f36cd3d5315606e460ac2843497b1f1fef34ec6064bc6453997fdc1e197181b35090c1b53701a5b3ba8fc923a8704982786d0ba7593f0f256f8788a65cdd8e7e0681632a0832f6b3d01ea4b9c159ff87595d25da0ff3a6bda1dc1f66091d0509e6ce636205713bf9fe8607091a5d69a3c0f545a68499206f84a1b96231227419cb2898c110dcf7c818a27912171416394bef4bd8b9f1616d91a5e018e484fcf9a685c50a75472d0a393215faf302795042bfb15d4f30e7ef7c12af9a50deb729a3d93441c482af6928f1037fa3464d73b501b592a7e8f065c7c4a4c0f37ca09dca67f39aa153dd6c72ce086895865b4ab6ec8bb89a59c2f4497b955d32deba77d9c4e9f8eebe0f5a8f808da89d77f451cb09908d506fe7e79d230f66a8d5b5f0ccc53ff074862b64b64291221ccd41745ffaf8e18ab98c090505e908534772d10f8f586e6170c6d4b20004c83957fe8ccb72a1485cc9af649cad6a05e58092310ad054367b1b7fe45066e91db277cbcf5bd26842a766676bdb56b30ac53b44fdc30aeb00a81317796c2ce0d88f59a46be27086570d8c620661f16c04bfa0124bcc3c978f6745072c15d5d912a93c24509bdbc3aba2caddfcef001dde162ebb89c7f433122ea6d093ff3b22bcd8a2564d69f09c02ea0310549af862c7338d287b94223a55d3c56884cfbfc4bf4b9161848e4e571c500f3d1a7e1d0954d2d86481804b4c8ab795346ec31b9fb139b7d8af4673c0069f1030ecb3f183ab8d86d67866fe0d4778bc96e05120b41046c3ec643097d62cfaa3b1c29aac340056d080c034cb1d56ba90cb8164d21645703281906058901e4c131c261ec7d7e2c29f2000387cb989cd6c70401c24b96449c0f3f25d63caceab4d9722e5fe4444f7a964ba4b37260ef93c45492516cb1e4901d4c75d2e0feb012cbe1751350c929e5f1c8dcafbecea6e966a57516e6158ec0385c49b591816b8d588b4f006742f36e5de5af2a0193368c712395c59effd3e01b923d3a825567a125ee5169ad19160c905cee9cc0ccec5611899572a8132a21be704a95a574941418e7ef8b6e40236434c61972dc0bbfbb84aa7317d7570e98206d190d319c7dedee24779f89802bd80165f398f8d2cd555b12dc58c4c98b7ba40ea80197532f214e15e4bcecc7531e69ea8a93988ba5c1011600dda0bcab460d70af3abd26540497d1a7e2787fdf4916f034cee53866d7a83397677487ee87f7889e707b0c59104340fbe99d36f624e7e7c39bacf113d2f98ba3bd54d1ed53d58aac6fdca42d06020b29b4959cd1e14db6351f611f50b6f9251123a1ac640864b36522568bc4e515590331a7e3d7c22d24133bcd3b7e40e44f8535e1576b51d64e8a4e6e4add358fd0481cfa35f3b45fa0bbf0cd8bd45cde4445f1d10733ea43051aefca672ef0428654ab346dfda2e8ec28cc92bfbd5001bc3e34dd91abcb18460ceba223c4a60e40a53f808316f41142541ece5cd8727269664f8eb28358893b564a071d1240aa8edd615f6d8c31ddc4132b5273d3183c04736cdc02fabb49fc284be83507c2c131d90700ef7335e00702d8c6b569007073251e179eb76439f7c117680a97939c077c37967c022df61621ab28e37b4fed1e252caf48ce0cf80a55b4a07d91620ed96d55768caa6f33e7288407f394d655313c0b5f821cc460fd89c9c1482d51c5f98c4c4f6da2e45004e35050b6f3d25150177c44a7f17d1b0e954bbe4c8083dc993f8ddef7459c284dd117f58d925dc70e580fce4dd27b98bd43533c824849001c94518ed9010bc01"; + 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(); } fn default_zcoin_activation_params() -> ZcoinActivationParams { @@ -287,8 +377,8 @@ fn default_zcoin_activation_params() -> ZcoinActivationParams { required_confirmations: None, requires_notarization: None, zcash_params_path: None, - scan_blocks_per_iteration: 0, - scan_interval_ms: 0, + scan_blocks_per_iteration: one_thousand_u32(), + scan_interval_ms: 10, account: 0, } } diff --git a/mm2src/coins/z_coin/z_htlc.rs b/mm2src/coins/z_coin/z_htlc.rs index c2fba37a88..ffda1aba42 100644 --- a/mm2src/coins/z_coin/z_htlc.rs +++ b/mm2src/coins/z_coin/z_htlc.rs @@ -11,7 +11,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; @@ -85,19 +85,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 bd71d554c6..f959c58af3 100644 --- a/mm2src/coins/z_coin/z_rpc.rs +++ b/mm2src/coins/z_coin/z_rpc.rs @@ -832,10 +832,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 de201856d8..4146416bc6 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! { @@ -204,12 +205,15 @@ pub const APPLICATION_GRPC_WEB_TEXT_PROTO: &str = "application/grpc-web-text+pro pub const SATOSHIS: u64 = 100_000_000; pub const DEX_FEE_ADDR_PUBKEY: &str = "03bc2c7ba671bae4a6fc835244c9762b41647b9827d4780a89a949b984a8ddcc06"; +pub const DEX_BURN_ADDR_PUBKEY: &str = "034777b18effce6f7a849b72de8e6810bf7a7e050274b3782e1b5a13d0263a44dc"; // TODO: fix for real pubkey 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 +622,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 4fa36b0f82..3ddc5e0e09 100644 --- a/mm2src/mm2_main/Cargo.toml +++ b/mm2src/mm2_main/Cargo.toml @@ -118,7 +118,7 @@ tokio = { version = "1.20", features = ["io-util", "rt-multi-thread", "net", "si winapi = "0.3" [dev-dependencies] -coins = { path = "../coins", features = ["for-tests"] } +coins = { path = "../coins", features = ["for-tests", "mocktopus"] } coins_activation = { path = "../coins_activation", features = ["for-tests"] } mm2_test_helpers = { path = "../mm2_test_helpers" } mocktopus = "0.8.0" @@ -136,3 +136,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/for_tests/recreate_maker_swap_maker_expected.json b/mm2src/mm2_main/src/for_tests/recreate_maker_swap_maker_expected.json index 180cade8d2..4c4e83f43f 100644 --- a/mm2src/mm2_main/src/for_tests/recreate_maker_swap_maker_expected.json +++ b/mm2src/mm2_main/src/for_tests/recreate_maker_swap_maker_expected.json @@ -26,6 +26,7 @@ "taker_payment_spend_trade_fee":null}}}, {"timestamp":1638984456603,"event":{"type":"Negotiated", "data":{ + "taker_version": 1, "taker_payment_locktime":1638992240, "taker_pubkey":"03b1e544ce2d860219bc91314b5483421a553a7b33044659eff0be9214ed58addd", "maker_coin_swap_contract_addr":null, diff --git a/mm2src/mm2_main/src/for_tests/recreate_maker_swap_maker_payment_wait_confirm_failed_maker_expected.json b/mm2src/mm2_main/src/for_tests/recreate_maker_swap_maker_payment_wait_confirm_failed_maker_expected.json index 4e95aa537a..e329b26758 100644 --- a/mm2src/mm2_main/src/for_tests/recreate_maker_swap_maker_payment_wait_confirm_failed_maker_expected.json +++ b/mm2src/mm2_main/src/for_tests/recreate_maker_swap_maker_payment_wait_confirm_failed_maker_expected.json @@ -26,6 +26,7 @@ "taker_payment_spend_trade_fee":null}}}, {"timestamp":1638984456603,"event":{"type":"Negotiated", "data":{ + "taker_version": 1, "taker_payment_locktime":1638992240, "taker_pubkey":"03b1e544ce2d860219bc91314b5483421a553a7b33044659eff0be9214ed58addd", "maker_coin_swap_contract_addr":null, diff --git a/mm2src/mm2_main/src/for_tests/recreate_taker_swap_taker_expected.json b/mm2src/mm2_main/src/for_tests/recreate_taker_swap_taker_expected.json index 1c1cc39acf..2caf95b17a 100644 --- a/mm2src/mm2_main/src/for_tests/recreate_taker_swap_taker_expected.json +++ b/mm2src/mm2_main/src/for_tests/recreate_taker_swap_taker_expected.json @@ -26,6 +26,7 @@ "maker_payment_spend_trade_fee":null}}}, {"timestamp":1638984456204,"event":{"type":"Negotiated", "data":{ + "maker_version": 1, "maker_payment_locktime":1639000040, "maker_pubkey":"0315d9c51c657ab1be4ae9d3ab6e76a619d3bccfe830d5363fa168424c0d044732", "secret_hash":"4da9e7080175e8e10842e0e161b33cd298cab30b", diff --git a/mm2src/mm2_main/src/for_tests/recreate_taker_swap_taker_payment_wait_confirm_failed_taker_expected.json b/mm2src/mm2_main/src/for_tests/recreate_taker_swap_taker_payment_wait_confirm_failed_taker_expected.json index e743669805..13c0d847f9 100644 --- a/mm2src/mm2_main/src/for_tests/recreate_taker_swap_taker_payment_wait_confirm_failed_taker_expected.json +++ b/mm2src/mm2_main/src/for_tests/recreate_taker_swap_taker_payment_wait_confirm_failed_taker_expected.json @@ -26,6 +26,7 @@ "maker_payment_spend_trade_fee":null}}}, {"timestamp":1638984456204,"event":{"type":"Negotiated", "data":{ + "maker_version": 1, "maker_payment_locktime":1639000040, "maker_pubkey":"0315d9c51c657ab1be4ae9d3ab6e76a619d3bccfe830d5363fa168424c0d044732", "secret_hash":"4da9e7080175e8e10842e0e161b33cd298cab30b", diff --git a/mm2src/mm2_main/src/lp_network.rs b/mm2src/mm2_main/src/lp_network.rs index 08ae5f8b3e..9de49245f3 100644 --- a/mm2src/mm2_main/src/lp_network.rs +++ b/mm2src/mm2_main/src/lp_network.rs @@ -39,7 +39,8 @@ use mm2_metrics::{mm_label, mm_timing}; use serde::de; use std::net::ToSocketAddrs; -use crate::{lp_healthcheck, lp_ordermatch, lp_stats, lp_swap}; +use crate::{lp_healthcheck, lp_ordermatch, lp_stats, + lp_swap::{self, SwapMsg, SwapMsgExt}}; pub type P2PRequestResult = Result>; pub type P2PProcessResult = Result>; @@ -163,7 +164,18 @@ async fn process_p2p_message( }, Some(lp_swap::SWAP_PREFIX) => { if let Err(e) = - lp_swap::process_swap_msg(ctx.clone(), split.next().unwrap_or_default(), &message.data).await + lp_swap::process_swap_msg::(ctx.clone(), split.next().unwrap_or_default(), &message.data).await + { + log::error!("{}", e); + return; + } + + to_propagate = true; + }, + Some(lp_swap::SWAP_PREFIX_EXT) => { + if let Err(e) = + lp_swap::process_swap_msg::(ctx.clone(), split.next().unwrap_or_default(), &message.data) + .await { log::error!("{}", e); return; diff --git a/mm2src/mm2_main/src/lp_ordermatch.rs b/mm2src/mm2_main/src/lp_ordermatch.rs index 620cb79bfb..2f634b5611 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}; @@ -74,8 +74,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, @@ -2993,7 +2993,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, @@ -3151,7 +3150,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 7692503c18..48f0d09f2c 100644 --- a/mm2src/mm2_main/src/lp_swap.rs +++ b/mm2src/mm2_main/src/lp_swap.rs @@ -62,24 +62,27 @@ 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, + LEGACY_PROTOCOL_VERSION}; +#[cfg(feature = "for-tests")] use common::env_var_as_bool; use common::log::{debug, warn}; use common::now_sec; use common::time_cache::DuplicateCache; 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::{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}; use secp256k1::{PublicKey, SecretKey, Signature}; +use serde::de::DeserializeOwned; use serde::Serialize; use serde_json::{self as json, Value as Json}; use std::collections::{HashMap, HashSet}; @@ -144,6 +147,7 @@ pub use taker_swap::{calc_max_taker_vol, check_balance_for_taker_swap, max_taker pub use trade_preimage::trade_preimage_rpc; pub const SWAP_PREFIX: TopicPrefix = "swap"; +pub const SWAP_PREFIX_EXT: TopicPrefix = "swapext"; pub const SWAP_V2_PREFIX: TopicPrefix = "swapv2"; pub const SWAP_FINISHED_LOG: &str = "Swap finished: "; pub const TX_HELPER_PREFIX: TopicPrefix = "txhlp"; @@ -158,6 +162,15 @@ 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; + +/// Sending part of dex fee to the pre-burn account is active +pub const PRE_BURN_ACCOUNT_ACTIVE: bool = true; + cfg_wasm32! { use mm2_db::indexed_db::{ConstructibleDb, DbLocked}; use saved_swap::migrate_swaps_data; @@ -176,10 +189,25 @@ pub enum SwapMsg { TakerPayment(Vec), } +// Extension to SwapMsg with version added to negotiation exchange +#[derive(Clone, Debug, Eq, Deserialize, PartialEq, Serialize)] +pub enum SwapMsgExt { + NegotiationVersioned(NegotiationDataMsgVersion), + NegotiationReplyVersioned(NegotiationDataMsgVersion), +} + +/// Utility wrapper to allow manage both SwapMsg and SwapMsgExt as one entity +#[derive(Clone, Debug, Serialize)] +#[serde(untagged)] +pub enum SwapMsgWrapper { + Legacy(SwapMsg), + Ext(SwapMsgExt), +} + #[derive(Debug, Default)] pub struct SwapMsgStore { - negotiation: Option, - negotiation_reply: Option, + negotiation: Option, + negotiation_reply: Option, negotiated: Option, taker_fee: Option, maker_payment: Option, @@ -196,6 +224,11 @@ impl SwapMsgStore { } } +/// Process swap v1 or v1 extension messages +pub trait ProcessSwapMsg { + fn swap_msg_to_store(self, msg_store: &mut SwapMsgStore); +} + /// Storage for P2P messages, which are exchanged during SwapV2 protocol execution. #[derive(Debug)] pub struct SwapV2MsgStore { @@ -257,18 +290,22 @@ pub fn p2p_private_and_peer_id_to_broadcast(ctx: &MmArc, p2p_privkey: Option<&Ke } } -/// Spawns the loop that broadcasts message every `interval` seconds returning the AbortOnDropHandle +/// Spawns the loop that broadcasts group of messages every `interval` seconds returning the AbortOnDropHandle /// to stop it pub fn broadcast_swap_msg_every( ctx: MmArc, - topic: String, - msg: T, + msgs: Vec<(String, T)>, // topic and message interval_sec: f64, p2p_privkey: Option, ) -> AbortOnDropHandle { let fut = async move { loop { - broadcast_swap_message(&ctx, topic.clone(), msg.clone(), &p2p_privkey); + let msgs_cloned = msgs.clone(); + for msg in msgs_cloned { + broadcast_swap_message(&ctx, msg.0, msg.1, &p2p_privkey); + // TODO: delete the sleep (and return to only one message instead of array) when all nodes are upgraded to support version in negotiation + Timer::sleep(5.0).await; // this a delay between tries of new and old requests (to ensure the receiver processes the new message first) + } Timer::sleep(interval_sec).await; } }; @@ -323,10 +360,65 @@ pub fn broadcast_p2p_tx_msg(ctx: &MmArc, topic: String, msg: &TransactionEnum, p broadcast_p2p_msg(ctx, topic, encoded_msg, from); } -pub async fn process_swap_msg(ctx: MmArc, topic: &str, msg: &[u8]) -> P2PRequestResult<()> { +impl ProcessSwapMsg for SwapMsg { + fn swap_msg_to_store(self, msg_store: &mut SwapMsgStore) { + match self { + // build NegotiationDataMsgVersion from legacy NegotiationDataMsg with default version: + SwapMsg::Negotiation(data) => { + msg_store.negotiation = Some(NegotiationDataMsgVersion { + version: LEGACY_PROTOCOL_VERSION, + msg: data, + }) + }, + // build NegotiationDataMsgVersion from legacy NegotiationDataMsg with default version: + SwapMsg::NegotiationReply(data) => { + msg_store.negotiation_reply = Some(NegotiationDataMsgVersion { + version: LEGACY_PROTOCOL_VERSION, + msg: 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), + } + } +} + +impl ProcessSwapMsg for SwapMsgExt { + fn swap_msg_to_store(self, msg_store: &mut SwapMsgStore) { + match self { + SwapMsgExt::NegotiationVersioned(data) => { + #[cfg(feature = "for-tests")] + { + if env_var_as_bool("USE_NON_VERSIONED_TAKER") { + // ignore versioned msg to emulate old taker not supporting it + break; + } + } + msg_store.negotiation = Some(data); + }, + SwapMsgExt::NegotiationReplyVersioned(data) => { + #[cfg(feature = "for-tests")] + { + if env_var_as_bool("USE_NON_VERSIONED_MAKER") { + // ignore versioned reply to emulate old maker not supporting it + break; + } + } + msg_store.negotiation_reply = Some(data); + }, + } + } +} + +pub async fn process_swap_msg(ctx: MmArc, topic: &str, msg: &[u8]) -> P2PRequestResult<()> +where + SwapMsgT: ProcessSwapMsg + DeserializeOwned + std::fmt::Debug, +{ let uuid = Uuid::from_str(topic).map_to_mm(|e| P2PRequestError::DecodeError(e.to_string()))?; - let msg = match decode_signed::(msg) { + let msg = match decode_signed::(msg) { Ok(m) => m, Err(swap_msg_err) => { #[cfg(not(target_arch = "wasm32"))] @@ -360,14 +452,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); } @@ -377,6 +462,7 @@ pub async fn process_swap_msg(ctx: MmArc, topic: &str, msg: &[u8]) -> P2PRequest } pub fn swap_topic(uuid: &Uuid) -> String { pub_sub_topic(SWAP_PREFIX, &uuid.to_string()) } +pub fn swap_ext_topic(uuid: &Uuid) -> String { pub_sub_topic(SWAP_PREFIX_EXT, &uuid.to_string()) } /// Formats and returns a topic format for `txhlp`. /// @@ -789,62 +875,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, @@ -940,6 +970,31 @@ impl NegotiationDataMsg { } } +/// NegotiationDataMsg with version +#[derive(Clone, Debug, Eq, Deserialize, PartialEq, Serialize)] +pub struct NegotiationDataMsgVersion { + version: u16, + msg: NegotiationDataMsg, +} + +impl NegotiationDataMsgVersion { + pub fn version(&self) -> u16 { self.version } + + pub fn started_at(&self) -> u64 { self.msg.started_at() } + + pub fn payment_locktime(&self) -> u64 { self.msg.payment_locktime() } + + pub fn secret_hash(&self) -> &[u8] { self.msg.secret_hash() } + + pub fn maker_coin_htlc_pub(&self) -> &[u8] { self.msg.maker_coin_htlc_pub() } + + pub fn taker_coin_htlc_pub(&self) -> &[u8] { self.msg.taker_coin_htlc_pub() } + + pub fn maker_coin_swap_contract(&self) -> Option<&[u8]> { self.msg.maker_coin_swap_contract() } + + pub fn taker_coin_swap_contract(&self) -> Option<&[u8]> { self.msg.taker_coin_swap_contract() } +} + #[derive(Clone, Debug, Eq, Deserialize, PartialEq, Serialize)] pub struct PaymentWithInstructions { data: Vec, @@ -1825,11 +1880,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::*; @@ -1838,58 +1888,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::{dex_fee_from_taker_coin, 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() { @@ -2408,49 +2412,88 @@ 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`."), + TestCoin::should_burn_dex_fee.mock_safe(|_| MockResult::Return(true)); + let (kmd_fee_amount, kmd_burn_amount) = + match dex_fee_from_taker_coin(&kmd, "ETH", &MmNumber::from(6150), None, Some(PRE_BURN_ACCOUNT_ACTIVE)) { + DexFee::Standard(_) | DexFee::NoFee => { + panic!("Wrong variant returned for KMD from `dex_fee_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"); + TestCoin::should_burn_dex_fee.mock_safe(|_| MockResult::Return(true)); + let (mycoin_fee_amount, mycoin_burn_amount) = match dex_fee_from_taker_coin( + &mycoin, + "ETH", + &MmNumber::from(6150), + None, + Some(PRE_BURN_ACCOUNT_ACTIVE), + ) { + DexFee::Standard(_) | DexFee::NoFee => { + panic!("Wrong variant returned for MYCOIN from `dex_fee_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`.") - }, - }; - - 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 dex_fee_from_taker_coin(&mycoin, "", &MmNumber::from(6150), None, Some(PRE_BURN_ACCOUNT_ACTIVE)) { + DexFee::Standard(_) | DexFee::NoFee => { + panic!("Wrong variant returned for MYCOIN from `dex_fee_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 dex_fee_from_taker_coin( + &testcoin, + "", + &MmNumber::from(6150), + None, + Some(PRE_BURN_ACCOUNT_ACTIVE), + ) { + DexFee::Standard(_) | DexFee::NoFee => { + panic!("Wrong variant returned for TEST coin from `dex_fee_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 84c1bbc6aa..8610373d46 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap.rs @@ -4,25 +4,32 @@ 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, 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, WAIT_CONFIRM_INTERVAL_SEC}; use crate::lp_dispatcher::{DispatcherContext, LpEvents}; use crate::lp_network::subscribe_to_topic; use crate::lp_ordermatch::MakerOrderBuilder; 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 crate::lp_swap::{broadcast_swap_message, swap_ext_topic, taker_payment_spend_duration, SwapMsgWrapper, + MAX_STARTED_AT_DIFF}; +use crate::lp_swap::{NegotiationDataMsgVersion, SwapMsgExt}; 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}; +use coins::swap_features::LegacySwapFeature; +use coins::SWAP_PROTOCOL_VERSION; +#[cfg(feature = "run-docker-tests")] +use coins::TEST_BURN_ADDR_RAW_PUBKEY; +use coins::{dex_fee_from_taker_coin, CanRefundHtlc, CheckIfMyPaymentSentArgs, ConfirmPaymentInput, DexFee, + FeeApproxStage, FoundSwapTxSpend, MmCoin, MmCoinEnum, PaymentInstructionArgs, PaymentInstructions, + PaymentInstructionsErr, RefundPaymentArgs, SearchForSwapTxSpendInput, SendPaymentArgs, SpendPaymentArgs, + SwapTxTypeWithSecretHash, TradeFee, TradePreimageValue, TransactionEnum, ValidateFeeArgs, + ValidatePaymentInput, MIN_SWAP_PROTOCOL_VERSION}; use common::log::{debug, error, info, warn}; -use common::{bits256, executor::Timer, now_ms, DEX_FEE_ADDR_RAW_PUBKEY}; -use common::{now_sec, wait_until_sec}; +use common::{bits256, executor::Timer, now_ms}; +use common::{env_var_as_bool, now_sec, wait_until_sec}; use crypto::privkey::SerializableSecp256k1Keypair; use crypto::CryptoCtx; use futures::{compat::Future01CompatExt, select, FutureExt}; @@ -120,6 +127,9 @@ async fn save_my_maker_swap_event(ctx: &MmArc, swap: &MakerSwap, event: MakerSav #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct TakerNegotiationData { + /// Protocol version supported by taker peer. Optional because it was added in new NegotiationDataMsgVersion + /// so it could be read as None from a saved swap if the swap started before upgrade to NegotiationDataMsgVersion + pub taker_version: Option, pub taker_payment_locktime: u64, pub taker_pubkey: H264Json, pub maker_coin_swap_contract_addr: Option, @@ -154,6 +164,8 @@ pub struct MakerSwapData { pub maker_payment_lock: u64, /// Allows to recognize one SWAP from the other in the logs. #274. pub uuid: Uuid, + /// Swap protocol version that the remote taker supports. This field introduced with NegotiationDataMsgVersion message so is optional for old takers + pub taker_version: Option, pub started_at: u64, pub maker_coin_start_block: u64, pub taker_coin_start_block: u64, @@ -168,8 +180,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, @@ -289,6 +303,7 @@ impl MakerSwap { }, MakerSwapEvent::StartFailed(err) => self.errors.lock().push(err), MakerSwapEvent::Negotiated(data) => { + self.w().data.taker_version = Some(get_taker_version(&data)); self.taker_payment_lock .store(data.taker_payment_locktime, Ordering::Relaxed); self.w().other_maker_coin_htlc_pub = data.other_maker_coin_htlc_pub(); @@ -406,6 +421,7 @@ impl MakerSwap { } } + /// Creates initial negotiation messages. Creates new and old messages to try both until all nodes are upgraded to NegotiationDataMsgVersion fn get_my_negotiation_data(&self) -> NegotiationDataMsg { let r = self.r(); let secret_hash = self.secret_hash(); @@ -471,6 +487,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; @@ -550,6 +573,7 @@ impl MakerSwap { taker: self.taker.bytes.into(), secret: self.secret.into(), secret_hash: Some(self.secret_hash().into()), + taker_version: None, // not yet known before the negotiation stage started_at, lock_duration: self.payment_locktime, maker_amount: self.maker_amount.clone(), @@ -581,15 +605,34 @@ impl MakerSwap { async fn negotiate(&self) -> Result<(Option, Vec), String> { let negotiation_data = self.get_my_negotiation_data(); + let mut msgs = vec![]; + + // Important to try the new versioned negotiation message first + if cfg!(not(feature = "for-tests")) || !env_var_as_bool("USE_NON_VERSIONED_MAKER") { + let maker_versioned_negotiation_msg = SwapMsgExt::NegotiationVersioned(NegotiationDataMsgVersion { + version: SWAP_PROTOCOL_VERSION, + msg: negotiation_data.clone(), + }); + msgs.push(( + swap_ext_topic(&self.uuid), + SwapMsgWrapper::Ext(maker_versioned_negotiation_msg), + )); + } + + let maker_old_negotiation_msg = SwapMsg::Negotiation(negotiation_data); + msgs.push(( + swap_topic(&self.uuid), + SwapMsgWrapper::Legacy(maker_old_negotiation_msg), + )); - let maker_negotiation_data = SwapMsg::Negotiation(negotiation_data); const NEGOTIATION_TIMEOUT_SEC: u64 = 90; - debug!("Sending maker negotiation data {:?}", maker_negotiation_data); + debug!("Sending maker negotiation data: {:?}", msgs); + // Send both old and new negotiation data to determine whether the remote node is old (supports NegotiationDataMsg) or new (supports NegotiationDataMsgVersion). + // When all nodes upgrade to NegotiationDataMsgVersion we won't need to send both messages and will use 'version' field in NegotiationDataMsgVersion let send_abort_handle = broadcast_swap_msg_every( self.ctx.clone(), - swap_topic(&self.uuid), - maker_negotiation_data, + msgs, NEGOTIATION_TIMEOUT_SEC as f64 / 6., self.p2p_privkey, ); @@ -609,6 +652,22 @@ impl MakerSwap { }, }; drop(send_abort_handle); + + let remote_version = taker_data.version(); + // this check will work when we raise minimal swap protocol version + #[allow(clippy::absurd_extreme_comparisons)] + if remote_version < MIN_SWAP_PROTOCOL_VERSION { + self.broadcast_negotiated_false(); + return Ok((Some(MakerSwapCommand::Finish), vec![MakerSwapEvent::NegotiateFailed( + ERRL!( + "Remote taker protocol version {} too old, minimal version is {}", + remote_version, + MIN_SWAP_PROTOCOL_VERSION + ) + .into(), + )])); + } + 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(); @@ -674,6 +733,7 @@ impl MakerSwap { Ok((Some(MakerSwapCommand::WaitForTakerFee), vec![ MakerSwapEvent::Negotiated(TakerNegotiationData { + taker_version: Some(remote_version), taker_payment_locktime: taker_data.payment_locktime(), // using default to avoid misuse of this field // maker_coin_htlc_pubkey and taker_coin_htlc_pubkey must be used instead @@ -691,8 +751,7 @@ impl MakerSwap { let negotiated = SwapMsg::Negotiated(true); let send_abort_handle = broadcast_swap_msg_every( self.ctx.clone(), - swap_topic(&self.uuid), - negotiated, + vec![(swap_topic(&self.uuid), negotiated)], TAKER_FEE_RECV_TIMEOUT_SEC as f64 / 6., self.p2p_privkey, ); @@ -738,6 +797,36 @@ impl MakerSwap { }; swap_events.push(MakerSwapEvent::MakerPaymentInstructionsReceived(instructions)); + let taker_amount = MmNumber::from(self.taker_amount.clone()); + let remote_version = self + .r() + .data + .taker_version + .ok_or("No swap protocol version".to_owned())?; + let is_burn_active = LegacySwapFeature::is_active(LegacySwapFeature::SendToPreBurnAccount, remote_version); + let dex_fee = dex_fee_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()), + Some(is_burn_active), + ); + debug!( + "MakerSwap::wait_taker_fee remote_version={remote_version} is_burn_active={is_burn_active} 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) => { @@ -750,8 +839,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; @@ -762,7 +849,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(), @@ -905,8 +991,7 @@ impl MakerSwap { let msg = SwapMsg::MakerPayment(payment_data_msg); let abort_send_handle = broadcast_swap_msg_every( self.ctx.clone(), - swap_topic(&self.uuid), - msg, + vec![(swap_topic(&self.uuid), msg)], PAYMENT_MSG_INTERVAL_SEC, self.p2p_privkey, ); @@ -1831,6 +1916,7 @@ impl MakerSavedSwap { maker_payment_lock: 0, uuid: Default::default(), started_at: 0, + taker_version: None, maker_coin_start_block: 0, taker_coin_start_block: 0, maker_payment_trade_fee: None, @@ -2085,6 +2171,7 @@ pub async fn run_maker_swap(swap: RunMakerSwapInput, ctx: MmArc) { let ctx = swap.ctx.clone(); subscribe_to_topic(&ctx, swap_topic(&swap.uuid)); + subscribe_to_topic(&ctx, swap_ext_topic(&swap.uuid)); let mut status = ctx.log.status_handle(); let uuid_str = swap.uuid.to_string(); let to_broadcast = !(swap.maker_coin.is_privacy() || swap.taker_coin.is_privacy()); @@ -2351,6 +2438,11 @@ pub async fn calc_max_maker_vol( }) } +/// Determine version from negotiation data if saved swap data does not store version +/// (if the swap started before the upgrade to versioned negotiation message) +/// In any case it is very undesirable to upgrade mm2 when any swaps are active +fn get_taker_version(negotiation_data: &TakerNegotiationData) -> u16 { negotiation_data.taker_version.unwrap_or(0) } + #[cfg(all(test, not(target_arch = "wasm32")))] mod maker_swap_tests { use super::*; 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 2a5fd662ac..2751a32f80 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs @@ -4,18 +4,22 @@ use super::{swap_v2_topic, LockedAmount, LockedAmountInfo, SavedTradeFee, SwapsC use crate::lp_swap::maker_swap::MakerSwapPreparedParams; use crate::lp_swap::swap_lock::SwapLock; use crate::lp_swap::{broadcast_swap_v2_msg_every, check_balance_for_maker_swap, recv_swap_v2_msg, SecretHashAlgo, - SwapConfirmationsSettings, TransactionIdentifier, MAKER_SWAP_V2_TYPE, MAX_STARTED_AT_DIFF}; + SwapConfirmationsSettings, TransactionIdentifier, MAKER_SWAP_V2_TYPE, MAX_STARTED_AT_DIFF, + PRE_BURN_ACCOUNT_ACTIVE}; use crate::lp_swap::{swap_v2_pb::*, NO_REFUND_FEE}; use async_trait::async_trait; use bitcrypto::{dhash160, sha256}; -use coins::{CanRefundHtlc, ConfirmPaymentInput, DexFee, FeeApproxStage, FundingTxSpend, GenTakerFundingSpendArgs, - GenTakerPaymentSpendArgs, MakerCoinSwapOpsV2, MmCoin, ParseCoinAssocTypes, RefundMakerPaymentSecretArgs, - RefundMakerPaymentTimelockArgs, SearchForFundingSpendErr, SendMakerPaymentArgs, SwapTxTypeWithSecretHash, - TakerCoinSwapOpsV2, ToBytes, TradePreimageValue, Transaction, TxPreimageWithSig, ValidateTakerFundingArgs}; +#[cfg(feature = "run-docker-tests")] +use coins::TEST_BURN_ADDR_RAW_PUBKEY; +use coins::{dex_fee_from_taker_coin, CanRefundHtlc, ConfirmPaymentInput, DexFee, FeeApproxStage, FundingTxSpend, + GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, MakerCoinSwapOpsV2, MmCoin, ParseCoinAssocTypes, + RefundMakerPaymentSecretArgs, RefundMakerPaymentTimelockArgs, SearchForFundingSpendErr, + SendMakerPaymentArgs, SwapTxTypeWithSecretHash, TakerCoinSwapOpsV2, ToBytes, TradePreimageValue, + Transaction, TxPreimageWithSig, ValidateTakerFundingArgs}; 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; @@ -373,8 +377,6 @@ pub struct MakerSwapStateMachine Vec { self.secret_hash() } + + fn dex_fee(&self, taker_pub: &[u8]) -> DexFee { + dex_fee_from_taker_coin( + &self.taker_coin, + self.maker_coin.ticker(), + &self.taker_volume, + Some(taker_pub), + Some(PRE_BURN_ACCOUNT_ACTIVE), // Always active for TPU + ) + } } #[async_trait] @@ -425,6 +437,7 @@ impl; fn to_db_repr(&self) -> MakerSwapDbRepr { + let dummy_taker_pub = vec![]; // we dont know the actual taker pubkey yet so use a dummy taker pub to get common dex fee MakerSwapDbRepr { maker_coin: self.maker_coin.ticker().into(), maker_volume: self.maker_volume.clone(), @@ -436,8 +449,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); } @@ -616,12 +636,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 @@ -639,7 +653,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) => { @@ -1156,7 +1176,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, @@ -1164,7 +1183,7 @@ impl RecreateSwapRe maker_payment_lock: negotiated_event.maker_payment_locktime, uuid: started_event.uuid, started_at: started_event.started_at, + taker_version: None, maker_coin_start_block: started_event.maker_coin_start_block, taker_coin_start_block: started_event.taker_coin_start_block, // Don't set the fee since the value is used when we calculate locked by other swaps amount only. @@ -158,6 +159,7 @@ fn recreate_maker_swap(ctx: MmArc, taker_swap: TakerSavedSwap) -> RecreateSwapRe // Generate `Negotiated` event let maker_negotiated_event = MakerSwapEvent::Negotiated(TakerNegotiationData { + taker_version: Some(SWAP_PROTOCOL_VERSION), taker_payment_locktime: started_event.taker_payment_lock, taker_pubkey: started_event.my_persistent_pub, maker_coin_swap_contract_addr: negotiated_event.maker_coin_swap_contract_addr, @@ -335,6 +337,7 @@ async fn recreate_taker_swap(ctx: MmArc, maker_swap: MakerSavedSwap) -> Recreate taker_payment_lock: negotiated_event.taker_payment_locktime, uuid: started_event.uuid, started_at: started_event.started_at, + maker_version: None, maker_payment_wait: wait_for_maker_payment_conf_until(started_event.started_at, started_event.lock_duration), maker_coin_start_block: started_event.maker_coin_start_block, taker_coin_start_block: started_event.taker_coin_start_block, @@ -360,6 +363,7 @@ async fn recreate_taker_swap(ctx: MmArc, maker_swap: MakerSavedSwap) -> Recreate .or_mm_err(|| RecreateSwapError::NoSecretHash)?; let taker_negotiated_event = TakerSwapEvent::Negotiated(MakerNegotiationData { + maker_version: Some(SWAP_PROTOCOL_VERSION), maker_payment_locktime: started_event.maker_payment_lock, maker_pubkey: started_event.my_persistent_pub, secret_hash: secret_hash.clone(), diff --git a/mm2src/mm2_main/src/lp_swap/swap_watcher.rs b/mm2src/mm2_main/src/lp_swap/swap_watcher.rs index 16e3712f16..6488e466de 100644 --- a/mm2src/mm2_main/src/lp_swap/swap_watcher.rs +++ b/mm2src/mm2_main/src/lp_swap/swap_watcher.rs @@ -9,7 +9,7 @@ use coins::{CanRefundHtlc, ConfirmPaymentInput, FoundSwapTxSpend, MmCoinEnum, Re WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput}; use common::executor::{AbortSettings, SpawnAbortable, Timer}; use common::log::{debug, error, info}; -use common::{now_sec, DEX_FEE_ADDR_RAW_PUBKEY}; +use common::now_sec; use futures::compat::Future01CompatExt; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::MapToMmResult; @@ -187,7 +187,6 @@ impl State for ValidateTakerFee { taker_fee_hash: watcher_ctx.data.taker_fee_hash.clone(), sender_pubkey: watcher_ctx.verified_pub.clone(), min_block_number: watcher_ctx.data.taker_coin_start_block, - fee_addr: DEX_FEE_ADDR_RAW_PUBKEY.clone(), lock_duration: watcher_ctx.data.lock_duration, }) .compat(); diff --git a/mm2src/mm2_main/src/lp_swap/taker_swap.rs b/mm2src/mm2_main/src/lp_swap/taker_swap.rs index 4216408898..45eae4b02d 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_swap.rs @@ -5,25 +5,31 @@ use super::swap_lock::{SwapLock, SwapLockOps}; use super::swap_watcher::{watcher_topic, SwapWatcherMsg}; use super::trade_preimage::{TradePreimageRequest, TradePreimageRpcError, TradePreimageRpcResult}; use super::{broadcast_my_swap_status, broadcast_swap_message, broadcast_swap_msg_every, - check_other_coin_balance_for_swap, dex_fee_amount_from_taker_coin, dex_fee_rate, get_locked_amount, - recv_swap_msg, swap_topic, wait_for_maker_payment_conf_until, AtomicSwap, LockedAmount, MySwapInfo, - NegotiationDataMsg, NegotiationDataV2, NegotiationDataV3, RecoveredSwap, RecoveredSwapAction, SavedSwap, - SavedSwapIo, SavedTradeFee, SwapConfirmationsSettings, SwapError, SwapMsg, SwapPubkeys, SwapTxDataMsg, - SwapsContext, TransactionIdentifier, INCLUDE_REFUND_FEE, NO_REFUND_FEE, WAIT_CONFIRM_INTERVAL_SEC}; + check_other_coin_balance_for_swap, get_locked_amount, recv_swap_msg, swap_topic, + wait_for_maker_payment_conf_until, AtomicSwap, LockedAmount, MySwapInfo, NegotiationDataMsg, + NegotiationDataV2, NegotiationDataV3, RecoveredSwap, RecoveredSwapAction, SavedSwap, SavedSwapIo, + SavedTradeFee, SwapConfirmationsSettings, SwapError, SwapMsg, SwapPubkeys, SwapTxDataMsg, SwapsContext, + TransactionIdentifier, INCLUDE_REFUND_FEE, NO_REFUND_FEE, WAIT_CONFIRM_INTERVAL_SEC}; use crate::lp_network::subscribe_to_topic; use crate::lp_ordermatch::TakerOrderBuilder; use crate::lp_swap::swap_v2_common::mark_swap_as_finished; use crate::lp_swap::taker_restart::get_command_based_on_maker_or_watcher_activity; -use crate::lp_swap::{broadcast_p2p_tx_msg, broadcast_swap_msg_every_delayed, tx_helper_topic, - wait_for_maker_payment_conf_duration, TakerSwapWatcherData, MAX_STARTED_AT_DIFF}; +use crate::lp_swap::{broadcast_p2p_tx_msg, broadcast_swap_msg_every_delayed, swap_ext_topic, tx_helper_topic, + wait_for_maker_payment_conf_duration, SwapMsgWrapper, TakerSwapWatcherData, MAX_STARTED_AT_DIFF}; +use crate::lp_swap::{NegotiationDataMsgVersion, SwapMsgExt}; use coins::lp_price::fetch_swap_coins_price; -use coins::{lp_coinfind, CanRefundHtlc, CheckIfMyPaymentSentArgs, ConfirmPaymentInput, FeeApproxStage, - FoundSwapTxSpend, MmCoin, MmCoinEnum, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, - RefundPaymentArgs, SearchForSwapTxSpendInput, SendPaymentArgs, SpendPaymentArgs, SwapTxTypeWithSecretHash, - TradeFee, TradePreimageValue, ValidatePaymentInput, WaitForHTLCTxSpendArgs}; +use coins::swap_features::LegacySwapFeature; +use coins::SWAP_PROTOCOL_VERSION; +#[cfg(feature = "run-docker-tests")] +use coins::TEST_BURN_ADDR_RAW_PUBKEY; +use coins::{dex_fee_from_taker_coin, lp_coinfind, CanRefundHtlc, CheckIfMyPaymentSentArgs, ConfirmPaymentInput, + DexFee, FeeApproxStage, FoundSwapTxSpend, MmCoin, MmCoinEnum, PaymentInstructionArgs, PaymentInstructions, + PaymentInstructionsErr, RefundPaymentArgs, SearchForSwapTxSpendInput, SendPaymentArgs, SpendPaymentArgs, + SwapTxTypeWithSecretHash, TradeFee, TradePreimageValue, ValidatePaymentInput, WaitForHTLCTxSpendArgs, + MIN_SWAP_PROTOCOL_VERSION}; use common::executor::Timer; use common::log::{debug, error, info, warn}; -use common::{bits256, now_ms, now_sec, wait_until_sec, DEX_FEE_ADDR_RAW_PUBKEY}; +use common::{bits256, env_var_as_bool, now_ms, now_sec, wait_until_sec}; use crypto::{privkey::SerializableSecp256k1Keypair, CryptoCtx}; use futures::{compat::Future01CompatExt, future::try_join, select, FutureExt}; use http::Response; @@ -457,6 +463,7 @@ pub async fn run_taker_swap(swap: RunTakerSwapInput, ctx: MmArc) { let ctx = swap.ctx.clone(); subscribe_to_topic(&ctx, swap_topic(&swap.uuid)); + subscribe_to_topic(&ctx, swap_ext_topic(&swap.uuid)); let mut status = ctx.log.status_handle(); let uuid = swap.uuid.to_string(); let to_broadcast = !(swap.maker_coin.is_privacy() || swap.taker_coin.is_privacy()); @@ -541,6 +548,8 @@ pub struct TakerSwapData { /// Allows to recognize one SWAP from the other in the logs. #274. pub uuid: Uuid, pub started_at: u64, + /// Swap protocol version that the remote maker supports. This field introduced with NegotiationDataMsgVersion message so is optional for old makers + pub maker_version: Option, pub maker_payment_wait: u64, pub maker_coin_start_block: u64, pub taker_coin_start_block: u64, @@ -558,8 +567,10 @@ pub struct TakerSwapData { #[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, @@ -641,6 +652,9 @@ pub struct TakerPaymentSpentData { #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct MakerNegotiationData { + /// Protocol version supported by maker peer. Optional because it was added in new NegotiationDataMsgVersion + /// so it could be read as None from a saved swap if the swap started before upgrade to NegotiationDataMsgVersion + pub maker_version: Option, pub maker_payment_locktime: u64, pub maker_pubkey: H264Json, pub secret_hash: BytesJson, @@ -824,6 +838,7 @@ impl TakerSwap { TakerSwapEvent::Negotiated(data) => { self.maker_payment_lock .store(data.maker_payment_locktime, Ordering::Relaxed); + self.w().data.maker_version = Some(get_maker_version(&data)); self.w().other_maker_coin_htlc_pub = data.other_maker_coin_htlc_pub(); self.w().other_taker_coin_htlc_pub = data.other_taker_coin_htlc_pub(); self.w().secret_hash = data.secret_hash; @@ -965,7 +980,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, @@ -1024,20 +1038,40 @@ 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 = dex_fee_from_taker_coin( + self.taker_coin.deref(), + self.maker_coin.ticker(), + &self.taker_amount, + Some(&self.my_taker_coin_htlc_pub().0), + None, // None as we need only learn total dex fee amount + ); 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 @@ -1113,6 +1147,7 @@ impl TakerSwap { maker_coin: self.maker_coin.ticker().to_owned(), maker: self.maker.bytes.into(), started_at, + maker_version: None, // not yet known on start lock_duration: self.payment_locktime, maker_amount: self.maker_amount.to_decimal(), taker_amount: self.taker_amount.to_decimal(), @@ -1168,6 +1203,20 @@ impl TakerSwap { )])); } + let remote_version = maker_data.version(); + // this check will work when we raise minimal swap protocol version + #[allow(clippy::absurd_extreme_comparisons)] + if remote_version < MIN_SWAP_PROTOCOL_VERSION { + return Ok((Some(TakerSwapCommand::Finish), vec![TakerSwapEvent::NegotiateFailed( + ERRL!( + "Remote maker protocol version {} too old, minimal version is {}", + remote_version, + MIN_SWAP_PROTOCOL_VERSION + ) + .into(), + )])); + } + let customized_lock_duration = (self.r().data.lock_duration as f64 * self.taker_coin.maker_locktime_multiplier()).ceil() as u64; let expected_lock_time = maker_data.started_at().checked_add(customized_lock_duration); @@ -1247,15 +1296,37 @@ impl TakerSwap { taker_coin_swap_contract_bytes, ); - let taker_data = SwapMsg::NegotiationReply(my_negotiation_data); + let (topic, taker_data) = if cfg!(feature = "for-tests") && env_var_as_bool("USE_NON_VERSIONED_TAKER") { + // emulate old taker, sending non-versioned message + ( + swap_topic(&self.uuid), + SwapMsgWrapper::Legacy(SwapMsg::NegotiationReply(my_negotiation_data)), + ) + } else { + // Normal path for versioned taker + match remote_version >= SWAP_PROTOCOL_VERSION { + true => ( + swap_ext_topic(&self.uuid), + SwapMsgWrapper::Ext(SwapMsgExt::NegotiationReplyVersioned(NegotiationDataMsgVersion { + version: SWAP_PROTOCOL_VERSION, + msg: my_negotiation_data, + })), + ), + false => ( + swap_topic(&self.uuid), + SwapMsgWrapper::Legacy(SwapMsg::NegotiationReply(my_negotiation_data)), // remote node is old + ), + } + }; + debug!("Sending taker negotiation data {:?}", taker_data); let send_abort_handle = broadcast_swap_msg_every( self.ctx.clone(), - swap_topic(&self.uuid), - taker_data, + vec![(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(), @@ -1280,6 +1351,7 @@ impl TakerSwap { Ok((Some(TakerSwapCommand::SendTakerFee), vec![TakerSwapEvent::Negotiated( MakerNegotiationData { + maker_version: Some(remote_version), maker_payment_locktime: maker_data.payment_locktime(), // using default to avoid misuse of this field // maker_coin_htlc_pubkey and taker_coin_htlc_pubkey must be used instead @@ -1301,12 +1373,32 @@ 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 remote_version = self + .r() + .data + .maker_version + .ok_or("No swap protocol version".to_owned())?; + let is_burn_active = LegacySwapFeature::is_active(LegacySwapFeature::SendToPreBurnAccount, remote_version); + let dex_fee = dex_fee_from_taker_coin( + self.taker_coin.deref(), + &self.r().data.maker_coin, + &self.taker_amount, + Some(&self.my_taker_coin_htlc_pub().0), + Some(is_burn_active), + ); + 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, @@ -1344,8 +1436,7 @@ impl TakerSwap { let msg = SwapMsg::TakerFee(payment_data_msg); let abort_send_handle = broadcast_swap_msg_every( self.ctx.clone(), - swap_topic(&self.uuid), - msg, + vec![(swap_topic(&self.uuid), msg)], MAKER_PAYMENT_WAIT_TIMEOUT_SEC as f64 / 6., self.p2p_privkey, ); @@ -1718,8 +1809,7 @@ impl TakerSwap { let msg = SwapMsg::TakerPayment(tx_hex); let send_abort_handle = broadcast_swap_msg_every( self.ctx.clone(), - swap_topic(&self.uuid), - msg, + vec![(swap_topic(&self.uuid), msg)], BROADCAST_MSG_INTERVAL_SEC, self.p2p_privkey, ); @@ -2353,13 +2443,18 @@ 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 = dex_fee_from_taker_coin( + self.taker_coin.deref(), + &self.r().data.maker_coin, + &self.taker_amount, + Some(&self.my_taker_coin_htlc_pub().0), + None, // None as we need only learn total dex fee amount + ); 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, }); } @@ -2422,7 +2517,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 = dex_fee_from_taker_coin(my_coin, other_coin.ticker(), &volume, None, None); let fee_to_send_dex_fee = my_coin .get_fee_to_send_taker_fee(dex_fee.clone(), stage) .await @@ -2510,15 +2606,22 @@ 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 = dex_fee_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) + None, + ); 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))?; @@ -2534,7 +2637,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(), @@ -2656,7 +2759,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 = dex_fee_from_taker_coin(coin.deref(), other_coin, &max_possible_2, None, None); let max_fee_to_send_taker_fee = coin .get_fee_to_send_taker_fee(max_dex_fee.clone(), stage) .await @@ -2701,7 +2805,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) @@ -2718,13 +2822,18 @@ pub fn max_taker_vol_from_available( Ok(max_vol) } +/// Determine version from negotiation data if saved swap data does not store version +/// (if the swap started before the upgrade to versioned negotiation message) +/// In any case it is very undesirable to upgrade mm2 when any swaps are active +fn get_maker_version(negotiation_data: &MakerNegotiationData) -> u16 { negotiation_data.maker_version.unwrap_or(0) } + #[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}; + use coins::{dex_fee_from_taker_coin, FoundSwapTxSpend, MarketCoinOps, MmCoin, SwapOps, TestCoin}; use common::{block_on, new_uuid}; use mm2_test_helpers::for_tests::{mm_ctx_with_iguana, ETH_SEPOLIA_SWAP_CONTRACT}; use mocktopus::mocking::*; @@ -3146,7 +3255,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 = dex_fee_from_taker_coin(&coin, "MORTY", &max_taker_vol, None, 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); @@ -3166,7 +3278,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 = dex_fee_from_taker_coin(&coin, "MORTY", &max_taker_vol, None, 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 224ce38090..112eb00b73 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs @@ -1,4 +1,4 @@ -use super::swap_v2_common::*; +use super::{swap_v2_common::*, PRE_BURN_ACCOUNT_ACTIVE}; use super::{LockedAmount, LockedAmountInfo, SavedTradeFee, SwapsContext, TakerSwapPreparedParams, NEGOTIATE_SEND_INTERVAL, NEGOTIATION_TIMEOUT_SEC}; use crate::lp_swap::swap_lock::SwapLock; @@ -8,15 +8,17 @@ 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}; -use coins::{CanRefundHtlc, ConfirmPaymentInput, DexFee, FeeApproxStage, GenTakerFundingSpendArgs, - GenTakerPaymentSpendArgs, MakerCoinSwapOpsV2, MmCoin, ParseCoinAssocTypes, RefundFundingSecretArgs, - RefundTakerPaymentArgs, SendTakerFundingArgs, SpendMakerPaymentArgs, SwapTxTypeWithSecretHash, - TakerCoinSwapOpsV2, ToBytes, TradeFee, TradePreimageValue, Transaction, TxPreimageWithSig, - ValidateMakerPaymentArgs}; +#[cfg(feature = "run-docker-tests")] +use coins::TEST_BURN_ADDR_RAW_PUBKEY; +use coins::{dex_fee_from_taker_coin, CanRefundHtlc, ConfirmPaymentInput, DexFee, FeeApproxStage, + GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, MakerCoinSwapOpsV2, MmCoin, ParseCoinAssocTypes, + RefundFundingSecretArgs, RefundTakerPaymentArgs, SendTakerFundingArgs, SpendMakerPaymentArgs, + SwapTxTypeWithSecretHash, TakerCoinSwapOpsV2, ToBytes, TradeFee, TradePreimageValue, Transaction, + TxPreimageWithSig, ValidateMakerPaymentArgs}; 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; @@ -399,8 +401,6 @@ pub struct TakerSwapStateMachine sha256(self.taker_secret.as_slice()).take().into(), } } + + fn dex_fee(&self) -> DexFee { + let dummy_unique_data = vec![]; + let taker_pub = self.taker_coin.derive_htlc_pubkey_v2_bytes(&dummy_unique_data); // Using dummy swap data because we need only permanent taker pubkey for dex fee + dex_fee_from_taker_coin( + &self.taker_coin, + self.maker_coin.ticker(), + &self.taker_volume, + Some(&taker_pub), + Some(PRE_BURN_ACCOUNT_ACTIVE), // Always active for TPU + ) + } } #[async_trait] @@ -466,13 +478,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); } @@ -732,12 +751,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 @@ -752,7 +765,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) => { @@ -925,8 +944,8 @@ impl = Cell::new(false); + pub static USE_NON_VERSIONED_MAKER: Cell = Cell::new(false); + pub static USE_NON_VERSIONED_TAKER: Cell = 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"; @@ -861,6 +870,23 @@ 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.as_ref()) + .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())); + } + if USE_NON_VERSIONED_MAKER.get() { + envs.push(("USE_NON_VERSIONED_MAKER", "true")); + } + if USE_NON_VERSIONED_TAKER.get() { + envs.push(("USE_NON_VERSIONED_TAKER", "true")); + } let confpath = unsafe { QTUM_CONF_PATH.as_ref().expect("Qtum config is not set yet") }; let coins = json! ([ @@ -872,12 +898,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, @@ -889,12 +915,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, @@ -906,7 +933,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 d89b456874..69ce1afb48 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs @@ -1,9 +1,10 @@ -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::*; use crate::{fill_address, generate_utxo_coin_with_random_privkey, random_secp256k1_secret, rmd160_from_priv, utxo_coin_from_privkey}; +use crate::{integration_tests_common::*, USE_NON_VERSIONED_MAKER, USE_NON_VERSIONED_TAKER}; use bitcrypto::dhash160; use chain::OutPoint; use coins::utxo::rpc_clients::UnspentInfo; @@ -3890,6 +3891,27 @@ 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")); +} + +// run swap with old maker (before version added to protocol) +#[test] +fn test_trade_base_rel_mycoin_mycoin1_coins_old_maker() { + USE_NON_VERSIONED_MAKER.set(true); + trade_base_rel(("MYCOIN", "MYCOIN1")); +} + +// run swap with old taker (before version added to protocol) +#[test] +fn test_trade_base_rel_mycoin_mycoin1_coins_old_taker() { + USE_NON_VERSIONED_TAKER.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 293610e102..c0c93b22f1 100644 --- a/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs @@ -1725,7 +1725,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, @@ -1733,7 +1732,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, @@ -1836,7 +1834,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, @@ -1844,7 +1841,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 352b383941..bcb125ba26 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::{dex_fee_from_taker_coin, 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 = dex_fee_from_taker_coin(&coin, "MYCOIN", &MmNumber::from(max_possible_2), None, 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 = dex_fee_from_taker_coin(&coin, "MYCOIN", &expected_max_taker_vol, None, 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 = dex_fee_from_taker_coin(&coin, "MYCOIN", &expected_max_taker_vol, None, 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 46d950242d..4c028b28b6 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), @@ -618,13 +621,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.as_ref()) + .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()); @@ -632,7 +661,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()); @@ -680,7 +715,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 49abc5c77f..102e8eacf5 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, - WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, EARLY_CONFIRMATION_ERR_LOG, +use coins::{dex_fee_from_taker_coin, ConfirmPaymentInput, 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; @@ -809,10 +811,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 = dex_fee_from_taker_coin(&coin, "ETH", &MmNumber::from(mycoin_volume.clone()), None, 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() @@ -948,15 +952,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 = + dex_fee_from_taker_coin(&coin, "ERC20DEV", &MmNumber::from(mycoin_volume.clone()), None, 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() @@ -1204,15 +1206,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 = dex_fee_from_taker_coin(&taker_coin, maker_coin.ticker(), &taker_amount, None, 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(), @@ -1228,7 +1224,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()); @@ -1237,7 +1232,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() @@ -1255,7 +1249,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() @@ -1275,7 +1268,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() @@ -1288,11 +1280,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() @@ -1319,14 +1314,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 = dex_fee_from_taker_coin(&taker_coin, "ETH", &taker_amount, None, 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(), @@ -1341,7 +1330,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()); @@ -1351,7 +1339,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() @@ -1369,7 +1356,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() @@ -1385,11 +1371,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() @@ -1404,6 +1392,7 @@ fn test_watcher_validate_taker_fee_eth() { error ), } + ::dex_pubkey.clear_mock(); } #[test] @@ -1416,14 +1405,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 = dex_fee_from_taker_coin(&taker_coin, "ETH", &taker_amount, None, 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(), @@ -1438,7 +1421,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()); @@ -1448,7 +1430,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() @@ -1466,7 +1447,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() @@ -1482,11 +1462,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() @@ -1501,6 +1483,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]