diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 27d6e487eb..fbb8ee9f42 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -434,7 +434,7 @@ impl EthGasLimitV2 { coin_type: &EthCoinType, payment_type: EthPaymentType, method: PaymentMethod, - ) -> Result { + ) -> Result { match coin_type { EthCoinType::Eth => { let gas_limit = match payment_type { @@ -451,7 +451,7 @@ impl EthGasLimitV2 { PaymentMethod::RefundSecret => self.taker.eth_taker_refund_secret, }, }; - Ok(gas_limit) + Ok(gas_limit.into()) }, EthCoinType::Erc20 { .. } => { let gas_limit = match payment_type { @@ -468,25 +468,25 @@ impl EthGasLimitV2 { PaymentMethod::RefundSecret => self.taker.erc20_taker_refund_secret, }, }; - Ok(gas_limit) + Ok(gas_limit.into()) }, EthCoinType::Nft { .. } => Err("NFT protocol is not supported for ETH and ERC20 Swaps".to_string()), } } - fn nft_gas_limit(&self, contract_type: &ContractType, method: PaymentMethod) -> u64 { + fn nft_gas_limit(&self, contract_type: &ContractType, method: PaymentMethod) -> U256 { match contract_type { ContractType::Erc1155 => match method { - PaymentMethod::Send => self.nft_maker.erc1155_payment, - PaymentMethod::Spend => self.nft_maker.erc1155_taker_spend, - PaymentMethod::RefundTimelock => self.nft_maker.erc1155_maker_refund_timelock, - PaymentMethod::RefundSecret => self.nft_maker.erc1155_maker_refund_secret, + PaymentMethod::Send => self.nft_maker.erc1155_payment.into(), + PaymentMethod::Spend => self.nft_maker.erc1155_taker_spend.into(), + PaymentMethod::RefundTimelock => self.nft_maker.erc1155_maker_refund_timelock.into(), + PaymentMethod::RefundSecret => self.nft_maker.erc1155_maker_refund_secret.into(), }, ContractType::Erc721 => match method { - PaymentMethod::Send => self.nft_maker.erc721_payment, - PaymentMethod::Spend => self.nft_maker.erc721_taker_spend, - PaymentMethod::RefundTimelock => self.nft_maker.erc721_maker_refund_timelock, - PaymentMethod::RefundSecret => self.nft_maker.erc721_maker_refund_secret, + PaymentMethod::Send => self.nft_maker.erc721_payment.into(), + PaymentMethod::Spend => self.nft_maker.erc721_taker_spend.into(), + PaymentMethod::RefundTimelock => self.nft_maker.erc721_maker_refund_timelock.into(), + PaymentMethod::RefundSecret => self.nft_maker.erc721_maker_refund_secret.into(), }, } } @@ -3960,6 +3960,32 @@ impl EthCoin { async fn get_address_lock(&self, address: Address) -> Arc> { self.address_nonce_locks.get_adddress_lock(address).await } + + /// Estimate gas for the token `approve` contract call + async fn estimate_approve_gas_limit( + &self, + token_addr: Address, + value: TradePreimageValue, + ) -> TradePreimageResult { + let value = match value { + TradePreimageValue::Exact(value) | TradePreimageValue::UpperBound(value) => { + u256_from_big_decimal(&value, self.decimals).map_mm_err()? + }, + }; + let allowed = self.allowance(self.swap_contract_address).compat().await.map_mm_err()?; + if allowed < value { + // Pass a dummy spender. Let's use `my_address`. + let spender = self.derivation_method.single_addr_or_err().await.map_mm_err()?; + let approve_function = ERC20_CONTRACT.function("approve")?; + let approve_data = approve_function.encode_input(&[Token::Address(spender), Token::Uint(value)])?; + let (approve_gas_limit, _) = self + .estimate_gas_for_contract_call(token_addr, Bytes::from(approve_data), 0.into()) + .await + .map_mm_err()?; + return Ok(approve_gas_limit); + } + Ok(0.into()) + } } #[cfg_attr(test, mockable)] @@ -3982,10 +4008,12 @@ impl EthCoin { default_gas } else { match &action { - Action::Call(contract_addr) => coin - .estimate_gas_for_contract_call_if_conf(*contract_addr, Bytes::from(data.clone()), value) - .await - .map_err(|err| TransactionErr::Plain(ERRL!("{}", err.get_inner())))?, + Action::Call(contract_addr) => { + coin.estimate_gas_for_contract_call_if_conf(*contract_addr, Bytes::from(data.clone()), value) + .await + .map_err(|err| TransactionErr::Plain(ERRL!("{}", err.get_inner())))? + .0 + }, _ => return Err(TransactionErr::InternalError("no gas limit set".to_owned())), } }; @@ -4888,7 +4916,7 @@ impl EthCoin { contract_addr: Address, call_data: Bytes, value: U256, - ) -> Web3RpcResult { + ) -> Web3RpcResult<(U256, U256)> { let coin = self.clone(); let my_address = coin.derivation_method.single_addr_or_err().await.map_mm_err()?; let fee_policy_for_estimate = @@ -4906,11 +4934,17 @@ impl EthCoin { }; // gas price must be supplied because some smart contracts base their // logic on gas price, e.g. TUSD: https://github.com/KomodoPlatform/atomicDEX-API/issues/643 - let estimate_gas_req = call_request_with_pay_for_gas_option(estimate_gas_req, pay_for_gas_option); - coin.estimate_gas_wrapper(estimate_gas_req) + let estimate_gas_req = call_request_with_pay_for_gas_option(estimate_gas_req, pay_for_gas_option.clone()); + let gas_limit = coin + .estimate_gas_wrapper(estimate_gas_req) .compat() .await - .map_to_mm(Web3RpcError::from) + .map_to_mm(Web3RpcError::from)?; + let fee_per_gas = match pay_for_gas_option { + PayForGasOption::Legacy { gas_price } => gas_price, + PayForGasOption::Eip1559 { max_fee_per_gas, .. } => max_fee_per_gas, + }; + Ok((gas_limit, fee_per_gas)) } /// Calls estimate_gas_for_contract_call if the `estimate_gas_mult` conf param is set or `default_gas` is None. @@ -4920,16 +4954,16 @@ impl EthCoin { contract_addr: Address, call_data: Bytes, value: U256, - ) -> Web3RpcResult { - let gas_estimated = self + ) -> Web3RpcResult<(U256, U256)> { + let (gas_estimated, fee_per_gas) = self .estimate_gas_for_contract_call(contract_addr, call_data, value) .await?; if let Some(estimate_gas_mult) = self.estimate_gas_mult { let gas_estimated = u256_to_big_decimal(gas_estimated, 0).map_mm_err()? * BigDecimal::from_f64(estimate_gas_mult).unwrap_or(BigDecimal::from(1)); - Ok(u256_from_big_decimal(&gas_estimated, 0).map_mm_err()?) + Ok((u256_from_big_decimal(&gas_estimated, 0).map_mm_err()?, fee_per_gas)) } else { - Ok(gas_estimated) + Ok((gas_estimated, fee_per_gas)) } } @@ -5902,6 +5936,27 @@ impl EthCoin { }, } } + + /// Estimated trade fee for the provided gas limit + pub async fn estimate_trade_fee(&self, gas_limit: U256, stage: FeeApproxStage) -> TradePreimageResult { + let pay_for_gas_option = self + .get_swap_pay_for_gas_option(self.get_swap_gas_fee_policy().await.map_mm_err()?) + .await + .map_mm_err()?; + let pay_for_gas_option = increase_gas_price_by_stage(pay_for_gas_option, &stage); + let total_fee = calc_total_fee(gas_limit, &pay_for_gas_option).map_mm_err()?; + let amount = u256_to_big_decimal(total_fee, ETH_DECIMALS).map_mm_err()?; + let fee_coin = match &self.coin_type { + EthCoinType::Eth => &self.ticker, + EthCoinType::Erc20 { platform, .. } => platform, + EthCoinType::Nft { .. } => return MmError::err(TradePreimageError::NftProtocolNotSupported), + }; + Ok(TradeFee { + coin: fee_coin.into(), + amount: amount.into(), + paid_from_trading_vol: false, + }) + } } #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] @@ -6077,7 +6132,6 @@ impl MmCoin for EthCoin { let pay_for_gas_option = increase_gas_price_by_stage(pay_for_gas_option, &stage); let gas_limit = match self.coin_type { EthCoinType::Eth => { - //let eth_payment_gas = self. // this gas_limit includes gas for `ethPayment` and optionally `senderRefund` contract calls if matches!(stage, FeeApproxStage::OrderIssueMax | FeeApproxStage::TradePreimageMax) { U256::from(self.gas_limit.eth_payment) + U256::from(self.gas_limit.eth_sender_refund) @@ -6087,27 +6141,9 @@ impl MmCoin for EthCoin { }, EthCoinType::Erc20 { token_addr, .. } => { let mut gas = U256::from(self.gas_limit.erc20_payment); - let value = match value { - TradePreimageValue::Exact(value) | TradePreimageValue::UpperBound(value) => { - u256_from_big_decimal(&value, self.decimals).map_mm_err()? - }, - }; - let allowed = self.allowance(self.swap_contract_address).compat().await.map_mm_err()?; - if allowed < value { - // estimate gas for the `approve` contract call - - // Pass a dummy spender. Let's use `my_address`. - let spender = self.derivation_method.single_addr_or_err().await.map_mm_err()?; - let approve_function = ERC20_CONTRACT.function("approve")?; - let approve_data = approve_function.encode_input(&[Token::Address(spender), Token::Uint(value)])?; - let approve_gas_limit = self - .estimate_gas_for_contract_call(token_addr, Bytes::from(approve_data), 0.into()) - .await - .map_mm_err()?; + // Add gas for `approve` `erc20Payment` contract calls or zero + gas += self.estimate_approve_gas_limit(token_addr, value).await?; - // this gas_limit includes gas for `approve`, `erc20Payment` contract calls - gas += approve_gas_limit; - } // add 'senderRefund' gas if requested if matches!(stage, FeeApproxStage::TradePreimage | FeeApproxStage::TradePreimageMax) { gas += U256::from(self.gas_limit.erc20_sender_refund); @@ -7551,6 +7587,24 @@ impl Eip1559Ops for EthCoin { #[async_trait] impl TakerCoinSwapOpsV2 for EthCoin { + /// Wrapper for [EthCoin::get_fee_to_send_taker_funding_impl] + async fn get_fee_to_send_taker_funding(&self, args: GetTakerFundingFeeArgs) -> TradePreimageResult { + self.get_fee_to_send_taker_funding_impl(args).await + } + + async fn get_fee_to_spend_taker_funding(&self, stage: FeeApproxStage) -> TradePreimageResult { + self.get_fee_to_spend_taker_funding_impl(stage).await + } + + async fn get_fee_to_spend_taker_payment(&self, stage: FeeApproxStage) -> TradePreimageResult { + self.get_fee_to_spend_taker_payment_impl(stage).await + } + + /// Estimate tx fee to spend taker payment + async fn get_fee_to_refund_taker_payment(&self, stage: FeeApproxStage) -> TradePreimageResult { + self.get_fee_to_refund_taker_payment_impl(stage).await + } + /// Wrapper for [EthCoin::send_taker_funding_impl] async fn send_taker_funding(&self, args: SendTakerFundingArgs<'_>) -> Result { self.send_taker_funding_impl(args).await @@ -7759,6 +7813,21 @@ impl EthCoin { #[async_trait] impl MakerCoinSwapOpsV2 for EthCoin { + async fn get_fee_to_send_maker_payment_v2( + &self, + args: GetFeeToSendMakerPaymentArgs, + ) -> TradePreimageResult { + self.get_fee_to_send_maker_payment_v2_impl(args).await + } + + async fn get_fee_to_spend_maker_payment_v2(&self, stage: FeeApproxStage) -> TradePreimageResult { + self.get_fee_to_spend_maker_payment_v2_impl(stage).await + } + + async fn get_fee_to_refund_maker_payment_v2(&self, stage: FeeApproxStage) -> TradePreimageResult { + self.get_fee_to_refund_maker_payment_v2_impl(stage).await + } + async fn send_maker_payment_v2(&self, args: SendMakerPaymentArgs<'_, Self>) -> Result { self.send_maker_payment_v2_impl(args).await } diff --git a/mm2src/coins/eth/eth_swap_v2/eth_maker_swap_v2.rs b/mm2src/coins/eth/eth_swap_v2/eth_maker_swap_v2.rs index 023c97653f..9c2e0ff390 100644 --- a/mm2src/coins/eth/eth_swap_v2/eth_maker_swap_v2.rs +++ b/mm2src/coins/eth/eth_swap_v2/eth_maker_swap_v2.rs @@ -3,12 +3,13 @@ use super::{ }; use crate::coin_errors::{ValidatePaymentError, ValidatePaymentResult}; use crate::eth::{ - decode_contract_call, get_function_input_data, u256_from_big_decimal, EthCoin, EthCoinType, SignedEthTx, - MAKER_SWAP_V2, + decode_contract_call, get_function_input_data, u256_from_big_decimal, EthCoin, EthCoinType, FeeApproxStage, + SignedEthTx, MAKER_SWAP_V2, }; use crate::{ - ParseCoinAssocTypes, RefundMakerPaymentSecretArgs, RefundMakerPaymentTimelockArgs, SendMakerPaymentArgs, - SpendMakerPaymentArgs, SwapTxTypeWithSecretHash, TransactionErr, ValidateMakerPaymentArgs, + GetFeeToSendMakerPaymentArgs, ParseCoinAssocTypes, RefundMakerPaymentSecretArgs, RefundMakerPaymentTimelockArgs, + SendMakerPaymentArgs, SpendMakerPaymentArgs, SwapTxTypeWithSecretHash, TradeFee, TradePreimageError, + TradePreimageResult, TransactionErr, ValidateMakerPaymentArgs, }; use ethabi::{Function, Token}; use ethcore_transaction::Action; @@ -57,6 +58,24 @@ struct MakerRefundSecretArgs<'a> { } impl EthCoin { + pub(crate) async fn get_fee_to_send_maker_payment_v2_impl( + &self, + args: GetFeeToSendMakerPaymentArgs, + ) -> TradePreimageResult { + let mut gas_limit = self + .gas_limit_v2 + .gas_limit(&self.coin_type, EthPaymentType::MakerPayments, PaymentMethod::Send) + .map_err(|e| TradePreimageError::InternalError(ERRL!("{}", e)))?; + + // Add token approval call gas limit + // TODO: for taker funding we have the taker.approve_payment constant. + // Maybe we could make the same for maker and save extra call + if let EthCoinType::Erc20 { token_addr, .. } = self.coin_type { + gas_limit += self.estimate_approve_gas_limit(token_addr, args.amount).await?; + } + self.estimate_trade_fee(gas_limit, args.stage).await + } + pub(crate) async fn send_maker_payment_v2_impl( &self, args: SendMakerPaymentArgs<'_, Self>, @@ -222,7 +241,7 @@ impl EthCoin { U256::from(ZERO_VALUE), Action::Call(maker_swap_v2_contract), data, - Some(U256::from(gas_limit)), + Some(gas_limit), ) .compat() .await @@ -267,12 +286,37 @@ impl EthCoin { U256::from(ZERO_VALUE), Action::Call(maker_swap_v2_contract), data, - Some(U256::from(gas_limit)), + Some(gas_limit), ) .compat() .await } + /// Estimate spend payment fee using the gas_limit const. + /// Using the gas_limit const because we cannot get correct estimation from the chain for spending a non-existent payment. + pub(crate) async fn get_fee_to_spend_maker_payment_v2_impl( + &self, + stage: FeeApproxStage, + ) -> TradePreimageResult { + let gas_limit = self + .gas_limit_v2 + .gas_limit(&self.coin_type, EthPaymentType::MakerPayments, PaymentMethod::Spend) + .map_err(|e| TradePreimageError::InternalError(ERRL!("{}", e)))?; + self.estimate_trade_fee(gas_limit, stage).await + } + + /// Estimate refund payment fee + pub(crate) async fn get_fee_to_refund_maker_payment_v2_impl( + &self, + stage: FeeApproxStage, + ) -> TradePreimageResult { + let gas_limit = self + .gas_limit_v2 + .gas_limit(&self.coin_type, EthPaymentType::MakerPayments, PaymentMethod::Spend) + .map_err(|e| TradePreimageError::InternalError(ERRL!("{}", e)))?; + self.estimate_trade_fee(gas_limit, stage).await + } + pub(crate) async fn spend_maker_payment_v2_impl( &self, args: SpendMakerPaymentArgs<'_, Self>, @@ -295,7 +339,7 @@ impl EthCoin { U256::from(ZERO_VALUE), Action::Call(maker_swap_v2_contract), data, - Some(U256::from(gas_limit)), + Some(gas_limit), ) .compat() .await diff --git a/mm2src/coins/eth/eth_swap_v2/eth_taker_swap_v2.rs b/mm2src/coins/eth/eth_swap_v2/eth_taker_swap_v2.rs index a9120852ca..3c7c040b70 100644 --- a/mm2src/coins/eth/eth_swap_v2/eth_taker_swap_v2.rs +++ b/mm2src/coins/eth/eth_swap_v2/eth_taker_swap_v2.rs @@ -4,9 +4,10 @@ use super::{ }; use crate::eth::{ decode_contract_call, get_function_input_data, signed_tx_from_web3_tx, u256_from_big_decimal, EthCoin, EthCoinType, - ParseCoinAssocTypes, RefundFundingSecretArgs, RefundTakerPaymentArgs, SendTakerFundingArgs, SignedEthTx, - SwapTxTypeWithSecretHash, TakerPaymentStateV2, TransactionErr, ValidateSwapV2TxError, ValidateSwapV2TxResult, - ValidateTakerFundingArgs, TAKER_SWAP_V2, + FeeApproxStage, GetTakerFundingFeeArgs, ParseCoinAssocTypes, RefundFundingSecretArgs, RefundTakerPaymentArgs, + SendTakerFundingArgs, SignedEthTx, SwapTxTypeWithSecretHash, TakerPaymentStateV2, TradeFee, TradePreimageError, + TradePreimageResult, TransactionErr, ValidateSwapV2TxError, ValidateSwapV2TxResult, ValidateTakerFundingArgs, + TAKER_SWAP_V2, }; use crate::{ FindPaymentSpendError, FundingTxSpend, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, SearchForFundingSpendErr, @@ -78,6 +79,17 @@ struct TakerValidationArgs<'a> { } impl EthCoin { + pub(crate) async fn get_fee_to_send_taker_funding_impl( + &self, + args: GetTakerFundingFeeArgs, + ) -> TradePreimageResult { + let gas_limit = self + .gas_limit_v2 + .gas_limit(&self.coin_type, EthPaymentType::TakerPayments, PaymentMethod::Send) + .map_err(|e| TradePreimageError::InternalError(ERRL!("{}", e)))?; + self.estimate_trade_fee(gas_limit, args.stage).await + } + /// Calls `"ethTakerPayment"` or `"erc20TakerPayment"` swap contract methods. /// Returns taker sent payment transaction. pub(crate) async fn send_taker_funding_impl( @@ -88,9 +100,8 @@ impl EthCoin { .swap_v2_contracts .ok_or_else(|| TransactionErr::Plain(ERRL!("Expected swap_v2_contracts to be Some, but found None")))? .taker_swap_v2_contract; - // TODO add burnFee support + // NOTE burnFee is supported by the eth SwapFeeManager contract let dex_fee = try_tx_s!(u256_from_big_decimal(&args.dex_fee.fee_amount().into(), self.decimals)); - let payment_amount = try_tx_s!(u256_from_big_decimal( &(args.trading_amount.clone() + args.premium_amount.clone()), self.decimals @@ -230,8 +241,14 @@ impl EthCoin { .await?; let decoded = try_tx_s!(decode_contract_call(send_func, args.funding_tx.unsigned().data())); let data = try_tx_s!( - self.prepare_taker_payment_approve_data(args, decoded, token_address) - .await + self.prepare_taker_payment_approve_data( + args.funding_tx.unsigned().value(), + decoded, + token_address, + args.taker_secret_hash, + args.maker_secret_hash + ) + .await ); let approve_tx = self .sign_and_send_transaction( @@ -303,7 +320,7 @@ impl EthCoin { U256::from(ZERO_VALUE), Action::Call(taker_swap_v2_contract), data, - Some(U256::from(gas_limit)), + Some(gas_limit), ) .compat() .await @@ -357,7 +374,7 @@ impl EthCoin { U256::from(ZERO_VALUE), Action::Call(taker_swap_v2_contract), data, - Some(U256::from(gas_limit)), + Some(gas_limit), ) .compat() .await @@ -418,7 +435,7 @@ impl EthCoin { U256::from(ZERO_VALUE), Action::Call(taker_swap_v2_contract), data, - Some(U256::from(gas_limit)), + Some(gas_limit), ) .compat() .await?; @@ -538,22 +555,23 @@ impl EthCoin { /// The `decoded` parameter should contain the transaction input data from the `ethTakerPayment` or `erc20TakerPayment` function of the EtomicSwapTakerV2 contract. async fn prepare_taker_payment_approve_data( &self, - args: &GenTakerFundingSpendArgs<'_, Self>, + value: U256, decoded: Vec, token_address: Address, + taker_secret_hash: &[u8], + maker_secret_hash: &[u8], ) -> Result, PrepareTxDataError> { let function = TAKER_SWAP_V2.function(TAKER_PAYMENT_APPROVE)?; let data = match self.coin_type { EthCoinType::Eth => { - let (dex_fee, amount) = - get_dex_fee_and_amount_from_eth_payment_data(&decoded, args.funding_tx.unsigned().value())?; + let (dex_fee, amount) = get_dex_fee_and_amount_from_eth_payment_data(&decoded, value)?; function.encode_input(&[ decoded[0].clone(), // id from ethTakerPayment Token::Uint(amount), // calculated payment amount (tx value - dexFee) Token::Uint(dex_fee), // dexFee from ethTakerPayment decoded[2].clone(), // receiver from ethTakerPayment - Token::FixedBytes(args.taker_secret_hash.to_vec()), - Token::FixedBytes(args.maker_secret_hash.to_vec()), + Token::FixedBytes(taker_secret_hash.to_vec()), + Token::FixedBytes(maker_secret_hash.to_vec()), Token::Address(token_address), // should be zero address Address::default() ])? }, @@ -564,8 +582,8 @@ impl EthCoin { decoded[1].clone(), // amount from erc20TakerPayment decoded[2].clone(), // dexFee from erc20TakerPayment decoded[4].clone(), // receiver from erc20TakerPayment - Token::FixedBytes(args.taker_secret_hash.to_vec()), - Token::FixedBytes(args.maker_secret_hash.to_vec()), + Token::FixedBytes(taker_secret_hash.to_vec()), + Token::FixedBytes(maker_secret_hash.to_vec()), Token::Address(token_address), // erc20 token address from EthCoinType::Erc20 ])? }, @@ -622,6 +640,48 @@ impl EthCoin { } } + /// Estimate fee to spend taker funding using the gas_limit const + /// because we cannot get correct estimation from the chain for spending a non-existent funding. + pub(crate) async fn get_fee_to_spend_taker_funding_impl( + &self, + stage: FeeApproxStage, + ) -> TradePreimageResult { + let gas_limit = self + .gas_limit_v2 + .gas_limit(&self.coin_type, EthPaymentType::TakerPayments, PaymentMethod::Spend) + .map_err(|e| TradePreimageError::InternalError(ERRL!("{}", e)))?; + // TODO: add stage to param + self.estimate_trade_fee(gas_limit, stage).await + } + + /// Estimate fee to spend taker funding using the gas_limit const + pub(crate) async fn get_fee_to_spend_taker_payment_impl( + &self, + stage: FeeApproxStage, + ) -> TradePreimageResult { + let gas_limit = match self.coin_type { + EthCoinType::Eth | EthCoinType::Erc20 { .. } => self.gas_limit_v2.taker.approve_payment, + EthCoinType::Nft { .. } => return MmError::err(TradePreimageError::NftProtocolNotSupported), + }; + self.estimate_trade_fee(gas_limit.into(), stage).await + } + + /// Estimate fee to spend taker funding using the gas_limit const + pub(crate) async fn get_fee_to_refund_taker_payment_impl( + &self, + stage: FeeApproxStage, + ) -> TradePreimageResult { + let gas_limit = self + .gas_limit_v2 + .gas_limit( + &self.coin_type, + EthPaymentType::TakerPayments, + PaymentMethod::RefundTimelock, + ) + .map_err(|e| TradePreimageError::InternalError(ERRL!("{}", e)))?; + self.estimate_trade_fee(gas_limit, stage).await + } + /// Retrieves the taker smart contract address, the corresponding function, and the token address. /// /// Depending on the coin type (ETH or ERC20), it fetches the appropriate function name and token address. diff --git a/mm2src/coins/eth/nft_swap_v2/mod.rs b/mm2src/coins/eth/nft_swap_v2/mod.rs index a55c66a336..d079a0f476 100644 --- a/mm2src/coins/eth/nft_swap_v2/mod.rs +++ b/mm2src/coins/eth/nft_swap_v2/mod.rs @@ -47,7 +47,7 @@ impl EthCoin { ZERO_VALUE.into(), Action::Call(*args.nft_swap_info.token_address), data, - Some(U256::from(gas_limit)), + Some(gas_limit), ) .compat() .await @@ -161,7 +161,7 @@ impl EthCoin { ZERO_VALUE.into(), Action::Call(nft_maker_swap_v2_contract), data, - Some(U256::from(gas_limit)), + Some(gas_limit), ) .compat() .await @@ -198,7 +198,7 @@ impl EthCoin { ZERO_VALUE.into(), Action::Call(nft_maker_swap_v2_contract), data, - Some(U256::from(gas_limit)), + Some(gas_limit), ) .compat() .await @@ -236,7 +236,7 @@ impl EthCoin { ZERO_VALUE.into(), Action::Call(nft_maker_swap_v2_contract), data, - Some(U256::from(gas_limit)), + Some(gas_limit), ) .compat() .await diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 3fb6bf9409..5616bdc416 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -1441,6 +1441,15 @@ pub struct SendTakerFundingArgs<'a> { pub swap_unique_data: &'a [u8], } +/// Helper struct wirg params to get taker funding fee +pub struct GetTakerFundingFeeArgs { + pub dex_fee: DexFee, + pub stage: FeeApproxStage, + pub premium_amount: BigDecimal, + pub trading_amount: BigDecimal, + pub upper_bound_amount: bool, +} + /// Helper struct wrapping arguments for [TakerCoinSwapOpsV2::refund_taker_funding_secret] pub struct RefundFundingSecretArgs<'a, Coin: ParseCoinAssocTypes + ?Sized> { pub funding_tx: &'a Coin::Tx, @@ -1738,6 +1747,11 @@ pub trait ParseNftAssocTypes { fn parse_contract_type(&self, contract_type: &[u8]) -> Result; } +pub struct GetFeeToSendMakerPaymentArgs { + pub amount: TradePreimageValue, + pub stage: FeeApproxStage, +} + pub struct SendMakerPaymentArgs<'a, Coin: ParseCoinAssocTypes + ?Sized> { /// Maker will be able to refund the payment after this timestamp pub time_lock: u64, @@ -1853,6 +1867,12 @@ pub struct RefundNftMakerPaymentArgs<'a, Coin: ParseCoinAssocTypes + ParseNftAss pub contract_type: &'a Coin::ContractType, } +/// Helper struct to estimate tx fee to spend maker payment +pub struct SpendMakerPaymentFeeArgs { + pub time_lock: u64, + pub amount: BigDecimal, +} + pub struct SpendMakerPaymentArgs<'a, Coin: ParseCoinAssocTypes + ?Sized> { /// Maker payment tx pub maker_payment_tx: &'a Coin::Tx, @@ -1891,6 +1911,18 @@ pub struct SpendNftMakerPaymentArgs<'a, Coin: ParseCoinAssocTypes + ParseNftAsso /// Operations specific to maker coin in [Trading Protocol Upgrade implementation](https://github.com/KomodoPlatform/komodo-defi-framework/issues/1895) #[async_trait] pub trait MakerCoinSwapOpsV2: ParseCoinAssocTypes + CommonSwapOpsV2 + Send + Sync + 'static { + /// Get fee estimate to send maker payment + async fn get_fee_to_send_maker_payment_v2( + &self, + args: GetFeeToSendMakerPaymentArgs, + ) -> TradePreimageResult; + + /// Get fee estimate to spend maker payment + async fn get_fee_to_spend_maker_payment_v2(&self, stage: FeeApproxStage) -> TradePreimageResult; + + /// Get fee estimate to refund maker payment (assume time unlocked refund) + async fn get_fee_to_refund_maker_payment_v2(&self, stage: FeeApproxStage) -> TradePreimageResult; + /// Generate and broadcast maker payment transaction async fn send_maker_payment_v2(&self, args: SendMakerPaymentArgs<'_, Self>) -> Result; @@ -2040,6 +2072,18 @@ pub enum SearchForFundingSpendErr { /// Operations specific to taker coin in [Trading Protocol Upgrade implementation](https://github.com/KomodoPlatform/komodo-defi-framework/issues/1895) #[async_trait] pub trait TakerCoinSwapOpsV2: ParseCoinAssocTypes + CommonSwapOpsV2 + Send + Sync + 'static { + /// Estimate tx fee to send taker funding + async fn get_fee_to_send_taker_funding(&self, args: GetTakerFundingFeeArgs) -> TradePreimageResult; + + /// Estimate tx fee to spend taker funding + async fn get_fee_to_spend_taker_funding(&self, stage: FeeApproxStage) -> TradePreimageResult; + + /// Estimate tx fee to spend taker payment + async fn get_fee_to_spend_taker_payment(&self, stage: FeeApproxStage) -> TradePreimageResult; + + /// Estimate tx fee to spend taker payment + async fn get_fee_to_refund_taker_payment(&self, stage: FeeApproxStage) -> TradePreimageResult; + /// Generate and broadcast taker funding transaction that includes dex fee, maker premium and actual trading volume. /// Funding tx can be reclaimed immediately if maker back-outs (doesn't send maker payment) async fn send_taker_funding(&self, args: SendTakerFundingArgs<'_>) -> Result; @@ -2734,17 +2778,22 @@ pub enum FeeApproxStage { WatcherPreimage, /// Increase the trade fee significantly. OrderIssue, - /// Increase the trade fee significantly (used to calculate max volume). + /// Increase the trade fee significantly, include refund fee. + /// Used to calculate max order volume taking into account possible swap refund. OrderIssueMax, /// Increase the trade fee largely in the trade_preimage rpc. TradePreimage, - /// Increase the trade fee in the trade_preimage rpc (used to calculate max volume for trade preimage). + /// Increase the trade fee largely, include refund fee. + /// Used to calculate max volume for trade preimage rpc, taking into account possible swap refund. TradePreimageMax, } +/// Swap trade volume parameter used in RPCs estimating transaction fee for swaps #[derive(Debug)] pub enum TradePreimageValue { + /// The whole trade amount will be sent to the HTLC contract Exact(BigDecimal), + /// Transaction fee will be deducted from the trade volume (supported for UTXO for now) UpperBound(BigDecimal), } @@ -3909,7 +3958,7 @@ impl MmCoinEnum { matches!(self, MmCoinEnum::EthCoin(_)) } - fn is_platform_coin(&self) -> bool { + pub fn is_platform_coin(&self) -> bool { self.ticker() == self.platform_ticker() } diff --git a/mm2src/coins/qrc20/qrc20_tests.rs b/mm2src/coins/qrc20/qrc20_tests.rs index 335ab4320e..efd00ba10c 100644 --- a/mm2src/coins/qrc20/qrc20_tests.rs +++ b/mm2src/coins/qrc20/qrc20_tests.rs @@ -275,7 +275,7 @@ fn test_wait_for_confirmations_excepted() { let confirmations = 1; let requires_nota = false; - let wait_until = now_sec() + 1; // the transaction is mined already + let wait_until = now_sec() + 10; // the transaction is mined already let check_every = 1; let confirm_payment_input = ConfirmPaymentInput { payment_tx, diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 702b028a43..d24c0bd6cc 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -4481,6 +4481,7 @@ pub mod tendermint_falsecoin_tests { assert_eq!(hex::encode_upper(hash.as_slice()), expected_hash); } + #[ignore = "insufficient balance"] #[test] fn test_htlc_create_and_claim() { let nodes = vec![RpcNode::for_test(IRIS_TESTNET_RPC_URL)]; @@ -5388,6 +5389,7 @@ pub mod tendermint_falsecoin_tests { assert_eq!(expected, actual); } + #[ignore = "insufficient balance"] #[test] fn test_claim_staking_rewards() { let nodes = vec![RpcNode::for_test(IRIS_TESTNET_RPC_URL)]; diff --git a/mm2src/coins/test_coin.rs b/mm2src/coins/test_coin.rs index ad24b86c7c..9fcee4fb61 100644 --- a/mm2src/coins/test_coin.rs +++ b/mm2src/coins/test_coin.rs @@ -21,7 +21,7 @@ use crate::{ WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WeakSpawner, WithdrawFut, WithdrawRequest, }; -use crate::{DexFee, ToBytes, ValidateWatcherSpendInput}; +use crate::{DexFee, GetTakerFundingFeeArgs, ToBytes, ValidateWatcherSpendInput}; use async_trait::async_trait; use common::executor::AbortedError; use futures01::Future; @@ -619,6 +619,22 @@ impl TakerCoinSwapOpsV2 for TestCoin { todo!() } + async fn get_fee_to_send_taker_funding(&self, args: GetTakerFundingFeeArgs) -> TradePreimageResult { + unimplemented!() + } + + async fn get_fee_to_spend_taker_funding(&self, _stage: FeeApproxStage) -> TradePreimageResult { + unimplemented!() + } + + async fn get_fee_to_spend_taker_payment(&self, _stage: FeeApproxStage) -> TradePreimageResult { + unimplemented!() + } + + async fn get_fee_to_refund_taker_payment(&self, _stage: FeeApproxStage) -> TradePreimageResult { + unimplemented!() + } + async fn validate_taker_funding(&self, args: ValidateTakerFundingArgs<'_, Self>) -> ValidateSwapV2TxResult { unimplemented!() } diff --git a/mm2src/coins/utxo/utxo_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index 9c31818bb9..b504a1864f 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -33,19 +33,19 @@ use crate::utxo::utxo_tx_history_v2::{ use crate::{ CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinBalance, CoinBalanceMap, CoinWithDerivationMethod, CoinWithPrivKeyPolicy, CommonSwapOpsV2, ConfirmPaymentInput, DexFee, FindPaymentSpendError, FundingTxSpend, - GenPreimageResult, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, GetWithdrawSenderAddress, IguanaBalanceOps, - IguanaPrivKey, MakerCoinSwapOpsV2, MmCoinEnum, NegotiateSwapContractAddrErr, PrivKeyBuildPolicy, - RawTransactionRequest, RawTransactionResult, RefundFundingSecretArgs, RefundMakerPaymentSecretArgs, - RefundMakerPaymentTimelockArgs, RefundPaymentArgs, RefundTakerPaymentArgs, SearchForFundingSpendErr, - SearchForSwapTxSpendInput, SendMakerPaymentArgs, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, - SendTakerFundingArgs, SignRawTransactionRequest, SignatureResult, SpendMakerPaymentArgs, SpendPaymentArgs, SwapOps, - SwapTxTypeWithSecretHash, TakerCoinSwapOpsV2, ToBytes, TradePreimageValue, TransactionFut, TransactionResult, - TxMarshalingErr, TxPreimageWithSig, ValidateAddressResult, ValidateFeeArgs, ValidateMakerPaymentArgs, - ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, ValidateSwapV2TxResult, - ValidateTakerFundingArgs, ValidateTakerFundingSpendPreimageResult, ValidateTakerPaymentSpendPreimageResult, - ValidateWatcherSpendInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, - WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, - WithdrawFut, + GenPreimageResult, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, GetFeeToSendMakerPaymentArgs, + GetTakerFundingFeeArgs, GetWithdrawSenderAddress, IguanaBalanceOps, IguanaPrivKey, MakerCoinSwapOpsV2, MmCoinEnum, + NegotiateSwapContractAddrErr, PrivKeyBuildPolicy, RawTransactionRequest, RawTransactionResult, + RefundFundingSecretArgs, RefundMakerPaymentSecretArgs, RefundMakerPaymentTimelockArgs, RefundPaymentArgs, + RefundTakerPaymentArgs, SearchForFundingSpendErr, SearchForSwapTxSpendInput, SendMakerPaymentArgs, + SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SendTakerFundingArgs, SignRawTransactionRequest, + SignatureResult, SpendMakerPaymentArgs, SpendPaymentArgs, SwapOps, SwapTxTypeWithSecretHash, TakerCoinSwapOpsV2, + ToBytes, TradePreimageValue, TransactionFut, TransactionResult, TxMarshalingErr, TxPreimageWithSig, + ValidateAddressResult, ValidateFeeArgs, ValidateMakerPaymentArgs, ValidateOtherPubKeyErr, ValidatePaymentError, + ValidatePaymentFut, ValidatePaymentInput, ValidateSwapV2TxResult, ValidateTakerFundingArgs, + ValidateTakerFundingSpendPreimageResult, ValidateTakerPaymentSpendPreimageResult, ValidateWatcherSpendInput, + VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, + WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut, }; use bitcrypto::sign_message_hash; use common::executor::{AbortableSystem, AbortedError}; @@ -593,6 +593,21 @@ impl ToBytes for Public { #[async_trait] impl MakerCoinSwapOpsV2 for UtxoStandardCoin { + async fn get_fee_to_send_maker_payment_v2( + &self, + args: GetFeeToSendMakerPaymentArgs, + ) -> TradePreimageResult { + utxo_common::get_sender_trade_fee(self, args.amount, args.stage).await + } + + async fn get_fee_to_spend_maker_payment_v2(&self, _stage: FeeApproxStage) -> TradePreimageResult { + utxo_common::get_receiver_trade_fee(self.clone()).compat().await + } + + async fn get_fee_to_refund_maker_payment_v2(&self, _stage: FeeApproxStage) -> TradePreimageResult { + utxo_common::get_receiver_trade_fee(self.clone()).compat().await + } + async fn send_maker_payment_v2(&self, args: SendMakerPaymentArgs<'_, Self>) -> Result { utxo_common::send_maker_payment_v2(self.clone(), args).await } @@ -652,6 +667,32 @@ impl MakerCoinSwapOpsV2 for UtxoStandardCoin { #[async_trait] impl TakerCoinSwapOpsV2 for UtxoStandardCoin { + async fn get_fee_to_send_taker_funding(&self, args: GetTakerFundingFeeArgs) -> TradePreimageResult { + let total_amount = + &args.dex_fee.total_spend_amount().to_decimal() + &args.premium_amount + &args.trading_amount; + let preimage_value = if args.upper_bound_amount { + TradePreimageValue::UpperBound(total_amount) + } else { + TradePreimageValue::Exact(total_amount) + }; + utxo_common::get_sender_trade_fee(self, preimage_value, args.stage).await + } + + /// Estimate tx fee to spend taker funding + async fn get_fee_to_spend_taker_funding(&self, _stage: FeeApproxStage) -> TradePreimageResult { + utxo_common::get_receiver_trade_fee(self.clone()).compat().await + } + + /// Estimate tx fee to spend taker payment + async fn get_fee_to_spend_taker_payment(&self, _stage: FeeApproxStage) -> TradePreimageResult { + utxo_common::get_receiver_trade_fee(self.clone()).compat().await + } + + /// Estimate tx fee to refund taker payment + async fn get_fee_to_refund_taker_payment(&self, _stage: FeeApproxStage) -> TradePreimageResult { + utxo_common::get_receiver_trade_fee(self.clone()).compat().await + } + async fn send_taker_funding(&self, args: SendTakerFundingArgs<'_>) -> Result { utxo_common::send_taker_funding(self.clone(), args).await } diff --git a/mm2src/coins/utxo/utxo_tests.rs b/mm2src/coins/utxo/utxo_tests.rs index 7a2fbb214c..dc582f3d25 100644 --- a/mm2src/coins/utxo/utxo_tests.rs +++ b/mm2src/coins/utxo/utxo_tests.rs @@ -1672,6 +1672,7 @@ fn test_network_info_negative_time_offset() { let _info: NetworkInfo = json::from_str(info_str).unwrap(); } +#[ignore = "failed to get electrum server version"] #[test] fn test_unavailable_electrum_proto_version() { ElectrumClientImpl::try_new_arc.mock_safe( @@ -1752,6 +1753,7 @@ fn test_spam_rick() { } } +#[ignore = "electrum server unavailable"] #[test] fn test_one_unavailable_electrum_proto_version() { // First mock with an unrealistically high version requirement that no server would support @@ -1839,6 +1841,7 @@ fn test_qtum_generate_pod() { assert_eq!(expected_res, res.to_string()); } +#[ignore = "internal electrum server error"] #[test] fn test_qtum_add_delegation() { let keypair = key_pair_from_seed("asthma turtle lizard tone genuine tube hunt valley soap cloth urge alpha amazing frost faculty cycle mammal leaf normal bright topple avoid pulse buffalo").unwrap(); @@ -1915,6 +1918,7 @@ fn test_qtum_add_delegation_on_already_delegating() { assert!(res.is_err()); } +#[ignore = "internal electrum server error"] #[test] fn test_qtum_get_delegation_infos() { let keypair = diff --git a/mm2src/mm2_main/src/lp_ordermatch.rs b/mm2src/mm2_main/src/lp_ordermatch.rs index 70e407c232..3369610d0b 100644 --- a/mm2src/mm2_main/src/lp_ordermatch.rs +++ b/mm2src/mm2_main/src/lp_ordermatch.rs @@ -85,12 +85,14 @@ use uuid::Uuid; use crate::lp_network::{broadcast_p2p_msg, request_any_relay, request_one_peer, subscribe_to_topic, P2PRequestError}; use crate::lp_swap::maker_swap_v2::{self, MakerSwapStateMachine, MakerSwapStorage}; use crate::lp_swap::taker_swap_v2::{self, TakerSwapStateMachine, TakerSwapStorage}; +use crate::lp_swap::SwapTotalFeeHelper; 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_v2, 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, TakerSwap, LEGACY_SWAP_TYPE, + calc_max_maker_vol, check_coin_balances_for_swap, check_other_coin_balance_for_swap, create_maker_total_fee_helper, + create_taker_total_fee_helper, detect_secret_hash_algo_v2, 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, + CheckBalanceResult, CoinVolumeInfo, MakerSwap, RunMakerSwapInput, RunTakerSwapInput, SwapConfirmationsSettings, + TakerSwap, LEGACY_SWAP_TYPE, }; use crate::swap_versioning::{legacy_swap_version, SwapVersion}; @@ -4326,18 +4328,15 @@ pub async fn buy(ctx: MmArc, req: Json) -> Result>, String> { try_s!(base_coin.pre_check_for_order_creation(&ctx, &rel_coin).await); let my_amount = &input.volume * &input.price; - try_s!( - check_balance_for_taker_swap( - &ctx, - rel_coin.deref(), - base_coin.deref(), - my_amount, - None, - None, - FeeApproxStage::OrderIssue - ) - .await - ); + let fee_helper = try_s!(create_taker_total_fee_helper( + &ctx, + &rel_coin, + &base_coin, + my_amount, + None, + FeeApproxStage::OrderIssue + )); + try_s!(check_coin_balances_for_swap(&ctx, None, fee_helper.deref(), false).await); let res = try_s!(lp_auto_buy(&ctx, &base_coin, &rel_coin, input).await); Ok(try_s!(Response::builder().body(res))) } @@ -4354,18 +4353,15 @@ pub async fn sell(ctx: MmArc, req: Json) -> Result>, String> { try_s!(base_coin.pre_check_for_order_creation(&ctx, &rel_coin).await); - try_s!( - check_balance_for_taker_swap( - &ctx, - base_coin.deref(), - rel_coin.deref(), - input.volume.clone(), - None, - None, - FeeApproxStage::OrderIssue - ) - .await - ); + let fee_helper = try_s!(create_taker_total_fee_helper( + &ctx, + &base_coin, + &rel_coin, + input.volume.clone(), + None, + FeeApproxStage::OrderIssue + )); + try_s!(check_coin_balances_for_swap(&ctx, None, fee_helper.deref(), false).await); let res = try_s!(lp_auto_buy(&ctx, &base_coin, &rel_coin, input).await); Ok(try_s!(Response::builder().body(res))) @@ -5118,13 +5114,12 @@ async fn cancel_orders_on_error(ctx: &MmArc, req: &SetPriceReq, error: E) Err(error) } -pub async fn check_other_coin_balance_for_order_issue(ctx: &MmArc, other_coin: &MmCoinEnum) -> CheckBalanceResult<()> { - let trade_fee = other_coin - .get_receiver_trade_fee(FeeApproxStage::OrderIssue) - .compat() - .await - .mm_err(|e| CheckBalanceError::from_trade_preimage_error(e, other_coin.ticker()))?; - check_other_coin_balance_for_swap(ctx, other_coin.deref(), None, trade_fee).await +pub async fn check_other_coin_balance_for_order_issue( + ctx: &MmArc, + total_fee_helper: &(dyn SwapTotalFeeHelper + Sync), +) -> CheckBalanceResult<()> { + let trade_fee = total_fee_helper.get_other_coin_fees().await?; + check_other_coin_balance_for_swap(ctx, total_fee_helper.get_other_coin(), None, trade_fee).await } pub async fn check_balance_update_loop(ctx: MmWeak, ticker: String, balance: Option) { @@ -5169,21 +5164,27 @@ pub async fn create_maker_order(ctx: &MmArc, req: SetPriceReq) -> Result Result update_msg.with_new_min_volume(actual_min_vol.into()); } - // Calculate order volume and add to update_msg if new_volume is found in the request + // Calculate new order volume let new_volume = if req.max.unwrap_or(false) { - let max_volume = try_s!(get_max_maker_vol(ctx, &base_coin).await).volume + reserved_amount.clone(); - try_s!(check_other_coin_balance_for_order_issue(ctx, &rel_coin).await); - update_msg.with_new_max_volume(max_volume.clone().into()); - max_volume + try_s!(get_max_maker_vol(ctx, &base_coin).await).volume + reserved_amount.clone() } else if Option::is_some(&req.volume_delta) { - let volume = original_volume + req.volume_delta.unwrap(); + let volume = original_volume + req.volume_delta.clone().unwrap(); if volume <= MmNumber::from("0") { return ERR!("New volume {} should be more than zero", volume); } - try_s!( - check_balance_for_maker_swap( - ctx, - base_coin.deref(), - rel_coin.deref(), - volume.clone(), - None, - None, - FeeApproxStage::OrderIssue - ) - .await - ); - update_msg.with_new_max_volume(volume.clone().into()); volume } else { original_volume }; + let fee_helper = try_s!(create_maker_total_fee_helper( + ctx, + &base_coin, + &rel_coin, + new_volume.clone(), + FeeApproxStage::OrderIssue + )); + + // Check balances for new volume and add it update_msg if new_volume is found in the request + if req.max.unwrap_or(false) { + try_s!(check_other_coin_balance_for_order_issue(ctx, fee_helper.deref()).await); + update_msg.with_new_max_volume(new_volume.clone().into()); + } else if Option::is_some(&req.volume_delta) { + let _ = try_s!(check_coin_balances_for_swap(ctx, None, fee_helper.deref(), req.max.unwrap_or(false)).await); + update_msg.with_new_max_volume(new_volume.clone().into()); + } + if new_volume <= reserved_amount { return ERR!( "New volume {} should be more than reserved amount for order matches {}", diff --git a/mm2src/mm2_main/src/lp_swap.rs b/mm2src/mm2_main/src/lp_swap.rs index b59fe05225..09c9741a7a 100644 --- a/mm2src/mm2_main/src/lp_swap.rs +++ b/mm2src/mm2_main/src/lp_swap.rs @@ -124,15 +124,17 @@ mod trade_preimage; #[cfg(target_arch = "wasm32")] mod swap_wasm_db; -pub use check_balance::{check_other_coin_balance_for_swap, CheckBalanceError, CheckBalanceResult}; +pub use check_balance::{ + check_coin_balances_for_swap, check_other_coin_balance_for_swap, CheckBalanceError, CheckBalanceResult, +}; +pub(crate) use check_balance::{create_maker_total_fee_helper, create_taker_total_fee_helper, SwapTotalFeeHelper}; use crypto::secret_hash_algo::SecretHashAlgo; use crypto::CryptoCtx; use keys::{KeyPair, SECP_SIGN, SECP_VERIFY}; use maker_swap::MakerSwapEvent; pub use maker_swap::{ - calc_max_maker_vol, check_balance_for_maker_swap, get_max_maker_vol, maker_swap_trade_preimage, run_maker_swap, - CoinVolumeInfo, MakerSavedEvent, MakerSavedSwap, MakerSwap, MakerSwapStatusChanged, MakerTradePreimage, - RunMakerSwapInput, MAKER_PAYMENT_SENT_LOG, + calc_max_maker_vol, get_max_maker_vol, maker_swap_trade_preimage, run_maker_swap, CoinVolumeInfo, MakerSavedEvent, + MakerSavedSwap, MakerSwap, MakerSwapStatusChanged, MakerTradePreimage, RunMakerSwapInput, MAKER_PAYMENT_SENT_LOG, }; pub use max_maker_vol_rpc::max_maker_vol; use my_swaps_storage::{MySwapsOps, MySwapsStorage}; @@ -151,10 +153,9 @@ pub use swap_watcher::{ }; use taker_swap::TakerSwapEvent; pub use taker_swap::{ - calc_max_taker_vol, check_balance_for_taker_swap, create_taker_swap_default_params, max_taker_vol, - max_taker_vol_from_available, run_taker_swap, taker_swap_trade_preimage, RunTakerSwapInput, TakerSavedSwap, - TakerSwap, TakerSwapData, TakerSwapPreparedParams, TakerTradePreimage, MAKER_PAYMENT_SPENT_BY_WATCHER_LOG, - REFUND_TEST_FAILURE_LOG, WATCHER_MESSAGE_SENT_LOG, + calc_max_taker_vol, max_taker_vol, max_taker_vol_from_available, run_taker_swap, taker_swap_trade_preimage, + RunTakerSwapInput, TakerSavedSwap, TakerSwap, TakerSwapData, TakerTradePreimage, + MAKER_PAYMENT_SPENT_BY_WATCHER_LOG, REFUND_TEST_FAILURE_LOG, WATCHER_MESSAGE_SENT_LOG, }; pub use trade_preimage::trade_preimage_rpc; diff --git a/mm2src/mm2_main/src/lp_swap/check_balance.rs b/mm2src/mm2_main/src/lp_swap/check_balance.rs index 4af138b22c..70c0f48000 100644 --- a/mm2src/mm2_main/src/lp_swap/check_balance.rs +++ b/mm2src/mm2_main/src/lp_swap/check_balance.rs @@ -1,6 +1,12 @@ -use super::taker_swap::MaxTakerVolumeLessThanDust; +use std::ops::Deref; + +use super::maker_swap::LegacyMakerSwapTotalFeeHelper; +use super::maker_swap_v2::MakerSwapV2TotalFeeHelper; +use super::taker_swap::{LegacyTakerSwapTotalFeeHelper, MaxTakerVolumeLessThanDust}; +use super::taker_swap_v2::TakerSwapV2TotalFeeHelper; use super::{get_locked_amount, get_locked_amount_by_other_swaps}; -use coins::{BalanceError, MmCoin, TradeFee, TradePreimageError}; +use async_trait::async_trait; +use coins::{BalanceError, DexFee, FeeApproxStage, MarketCoinOps, MmCoin, MmCoinEnum, TradeFee, TradePreimageError}; use common::log::debug; use derive_more::Display; use futures::compat::Future01CompatExt; @@ -18,12 +24,12 @@ pub async fn check_my_coin_balance_for_swap( ctx: &MmArc, coin: &dyn MmCoin, swap_uuid: Option<&Uuid>, - volume: MmNumber, - mut trade_fee: TradeFee, - taker_fee: Option, + volume: Option, + dex_fee: Option, + total_trade_fee: Option, ) -> CheckBalanceResult { let ticker = coin.ticker(); - debug!("Check my_coin '{}' balance for swap", ticker); + debug!("Checking coin '{}' balance for swap...", ticker); let balance: MmNumber = coin.my_spendable_balance().compat().await.map_mm_err()?.into(); let locked = match swap_uuid { @@ -31,51 +37,22 @@ pub async fn check_my_coin_balance_for_swap( None => get_locked_amount(ctx, ticker), }; - let dex_fee = match taker_fee { - Some(TakerFeeAdditionalInfo { - dex_fee, - fee_to_send_dex_fee, - }) => { - if fee_to_send_dex_fee.coin != trade_fee.coin { - let err = format!( - "trade_fee {:?} and fee_to_send_dex_fee {:?} coins are expected to be the same", - trade_fee.coin, fee_to_send_dex_fee.coin - ); - return MmError::err(CheckBalanceError::InternalError(err)); - } - // increase `trade_fee` by the `fee_to_send_dex_fee` - trade_fee.amount += fee_to_send_dex_fee.amount; - dex_fee - }, - None => MmNumber::from(0), - }; - - let total_trade_fee = if ticker == trade_fee.coin { - trade_fee.amount - } else { - let platform_coin_balance: MmNumber = coin.platform_coin_balance().compat().await.map_mm_err()?.into(); - check_platform_coin_balance_for_swap(ctx, &platform_coin_balance, trade_fee, swap_uuid) - .await - .map_mm_err()?; - MmNumber::from(0) - }; - debug!( - "my_coin: {} balance: {:?} ({}), locked: {:?} ({}), volume: {:?} ({}), total_trade_fee: {:?} ({}), dex_fee: {:?} ({}", + "coin {} balance {:?} ({:?}), locked {:?} ({:?}), volume {:?} ({:?}), total_trade_fee {:?} ({:?}), dex_fee {:?} ({:?})", ticker, balance.to_fraction(), balance.to_decimal(), locked.to_fraction(), locked.to_decimal(), - volume.to_fraction(), - volume.to_decimal(), - total_trade_fee.to_fraction(), - total_trade_fee.to_decimal(), - dex_fee.to_fraction(), - dex_fee.to_decimal() + volume.as_ref().map(|val| val.to_fraction()), + volume.as_ref().map(|val| val.to_decimal()), + total_trade_fee.as_ref().map(|val| val.to_fraction()), + total_trade_fee.as_ref().map(|val| val.to_decimal()), + dex_fee.as_ref().map(|val| val.to_fraction()), + dex_fee.as_ref().map(|val| val.to_decimal()), ); - let required = volume + total_trade_fee + dex_fee; + let required = volume.unwrap_or_default() + total_trade_fee.unwrap_or_default() + dex_fee.unwrap_or_default(); let available = &balance - &locked; if available < required { @@ -95,9 +72,12 @@ pub async fn check_other_coin_balance_for_swap( swap_uuid: Option<&Uuid>, trade_fee: TradeFee, ) -> CheckBalanceResult<()> { - if trade_fee.paid_from_trading_vol { - return Ok(()); + macro_rules! is_platform_fee { + ($ticker: expr, $trade_fee: expr) => { + $ticker != $trade_fee.coin + }; } + let ticker = coin.ticker(); debug!( "Check other_coin '{}' balance for swap to pay trade fee, trade_fee coin {}", @@ -110,7 +90,10 @@ pub async fn check_other_coin_balance_for_swap( None => get_locked_amount(ctx, ticker), }; - if ticker == trade_fee.coin { + if !is_platform_fee!(ticker, trade_fee) { + if trade_fee.paid_from_trading_vol { + return Ok(()); + } let available = &balance - &locked; let required = trade_fee.amount; debug!( @@ -132,8 +115,7 @@ pub async fn check_other_coin_balance_for_swap( }); } } else { - let platform_coin_balance: MmNumber = coin.platform_coin_balance().compat().await.map_mm_err()?.into(); - check_platform_coin_balance_for_swap(ctx, &platform_coin_balance, trade_fee, swap_uuid) + check_platform_coin_balance_for_swap(ctx, coin, trade_fee, swap_uuid) .await .map_mm_err()?; } @@ -143,10 +125,12 @@ pub async fn check_other_coin_balance_for_swap( pub async fn check_platform_coin_balance_for_swap( ctx: &MmArc, - balance: &MmNumber, + coin: &(dyn MarketCoinOps + Sync), trade_fee: TradeFee, swap_uuid: Option<&Uuid>, ) -> CheckBalanceResult<()> { + let balance = coin.platform_coin_balance().compat().await.map_mm_err()?; + let balance = MmNumber::from(balance); let ticker = trade_fee.coin.as_str(); debug!( "Check if the platform coin '{}' has sufficient balance to pay the trade fee {:?} ({})", @@ -160,7 +144,7 @@ pub async fn check_platform_coin_balance_for_swap( Some(uuid) => get_locked_amount_by_other_swaps(ctx, uuid, ticker), None => get_locked_amount(ctx, ticker), }; - let available = balance - &locked; + let available = &balance - &locked; debug!( "Platform coin: {} balance: {:?} ({}), locked: {:?} ({})", @@ -182,11 +166,6 @@ pub async fn check_platform_coin_balance_for_swap( } } -pub struct TakerFeeAdditionalInfo { - pub dex_fee: MmNumber, - pub fee_to_send_dex_fee: TradeFee, -} - #[derive(Debug, Display, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] pub enum CheckBalanceError { @@ -295,3 +274,188 @@ impl CheckBalanceError { } } } + +pub async fn check_coin_balances_for_swap( + ctx: &MmArc, + swap_uuid: Option<&Uuid>, + total_fee_helper: &(dyn SwapTotalFeeHelper + Sync), + upper_bound_volume: bool, +) -> CheckBalanceResult { + let my_coin_fees = total_fee_helper.get_my_coin_fees(upper_bound_volume).await?; + let other_coin_fees = total_fee_helper.get_other_coin_fees().await?; + let balance = check_my_coin_balance_for_swap( + ctx, + total_fee_helper.get_my_coin(), + swap_uuid, + Some(total_fee_helper.get_my_coin_volume()), + total_fee_helper.get_dex_fee(), + if !total_fee_helper.is_my_platform_fee(&my_coin_fees) { + Some(my_coin_fees.clone().amount) + } else { + None + }, + ) + .await?; + if total_fee_helper.is_my_platform_fee(&my_coin_fees) { + check_platform_coin_balance_for_swap(ctx, total_fee_helper.get_my_coin(), my_coin_fees.clone(), swap_uuid) + .await?; + } + check_other_coin_balance_for_swap(ctx, total_fee_helper.get_other_coin(), swap_uuid, other_coin_fees).await?; + Ok(balance) +} + +/// Helper to return total fees for legacy and TPU swaps, either for maker or taker, +/// to balance checking or calculating function +#[async_trait] +pub trait SwapTotalFeeHelper { + /// Returns my coin object + fn get_my_coin(&self) -> &dyn MmCoin; + + /// Returns my coin trade volume + fn get_my_coin_volume(&self) -> MmNumber; + + /// Returns dex fee for taker or None + fn get_dex_fee(&self) -> Option; + + /// Estimates fees for the party trading my_coin + async fn get_my_coin_fees(&self, upper_bound_amount: bool) -> CheckBalanceResult; + + /// Returns for other coin object + fn get_other_coin(&self) -> &dyn MmCoin; + + /// Estimates fees for the party trading other_coin + async fn get_other_coin_fees(&self) -> CheckBalanceResult; + + /// Returns true if fees for my_coin are paid in platfrom coin (different from my_coin) + fn is_my_platform_fee(&self, fees: &TradeFee) -> bool { + self.get_my_coin().ticker() != fees.coin + } + + /// Returns true if fees for other_coin are paid in platfrom coin (different from other__coin) + fn is_other_platform_fee(&self, fees: &TradeFee) -> bool { + self.get_other_coin().ticker() != fees.coin + } +} + +#[allow(clippy::result_large_err)] +pub(crate) fn create_taker_total_fee_helper<'a>( + ctx: &MmArc, + my_coin: &'a MmCoinEnum, + other_coin: &'a MmCoinEnum, + volume: MmNumber, + dex_fee: Option, + stage: FeeApproxStage, +) -> CheckBalanceResult> { + let dex_fee = dex_fee.unwrap_or_else(|| DexFee::new_from_taker_coin(my_coin.deref(), other_coin.ticker(), &volume)); + // Note that if the remote side does not support TPU the swap will downgrade to legacy swap + // so the total fee calculated by TakerSwapV2TotalFeeHelper may be not accurate + if ctx.conf["use_trading_proto_v2"].as_bool().unwrap_or_default() { + match (my_coin, other_coin) { + (MmCoinEnum::UtxoCoin(my_coin), MmCoinEnum::UtxoCoin(other_coin)) => { + Ok(Box::new(TakerSwapV2TotalFeeHelper { + my_coin: my_coin.clone(), + other_coin: other_coin.clone(), + volume, + dex_fee, + stage, + })) + }, + (MmCoinEnum::EthCoin(my_coin), MmCoinEnum::EthCoin(other_coin)) => { + Ok(Box::new(TakerSwapV2TotalFeeHelper { + my_coin: my_coin.clone(), + other_coin: other_coin.clone(), + volume, + dex_fee, + stage, + })) + }, + (MmCoinEnum::UtxoCoin(my_coin), MmCoinEnum::EthCoin(other_coin)) => { + Ok(Box::new(TakerSwapV2TotalFeeHelper { + my_coin: my_coin.clone(), + other_coin: other_coin.clone(), + volume, + dex_fee, + stage, + })) + }, + (MmCoinEnum::EthCoin(my_coin), MmCoinEnum::UtxoCoin(other_coin)) => { + Ok(Box::new(TakerSwapV2TotalFeeHelper { + my_coin: my_coin.clone(), + other_coin: other_coin.clone(), + volume, + dex_fee, + stage, + })) + }, + (_, _) => MmError::err(CheckBalanceError::InternalError( + "swap v2 not supported for this coin".to_string(), + )), + } + } else { + Ok(Box::new(LegacyTakerSwapTotalFeeHelper { + my_coin: my_coin.deref(), + other_coin: other_coin.deref(), + volume, + dex_fee, + stage, + })) + } +} + +#[allow(clippy::result_large_err)] +pub(crate) fn create_maker_total_fee_helper<'a>( + ctx: &MmArc, + my_coin: &'a MmCoinEnum, + other_coin: &'a MmCoinEnum, + volume: MmNumber, + stage: FeeApproxStage, +) -> CheckBalanceResult> { + // Note that if the remote side does not support TPU the swap will downgrade to legacy swap + // so the total fee calculated by TakerSwapV2TotalFeeHelper may be not accurate + if ctx.conf["use_trading_proto_v2"].as_bool().unwrap_or_default() { + match (my_coin, other_coin) { + (MmCoinEnum::UtxoCoin(my_coin), MmCoinEnum::UtxoCoin(other_coin)) => { + Ok(Box::new(MakerSwapV2TotalFeeHelper { + my_coin: my_coin.clone(), + other_coin: other_coin.clone(), + volume, + stage, + })) + }, + (MmCoinEnum::EthCoin(my_coin), MmCoinEnum::EthCoin(other_coin)) => { + Ok(Box::new(MakerSwapV2TotalFeeHelper { + my_coin: my_coin.clone(), + other_coin: other_coin.clone(), + volume, + stage, + })) + }, + (MmCoinEnum::UtxoCoin(my_coin), MmCoinEnum::EthCoin(other_coin)) => { + Ok(Box::new(MakerSwapV2TotalFeeHelper { + my_coin: my_coin.clone(), + other_coin: other_coin.clone(), + volume, + stage, + })) + }, + (MmCoinEnum::EthCoin(my_coin), MmCoinEnum::UtxoCoin(other_coin)) => { + Ok(Box::new(MakerSwapV2TotalFeeHelper { + my_coin: my_coin.clone(), + other_coin: other_coin.clone(), + volume, + stage, + })) + }, + (_, _) => MmError::err(CheckBalanceError::InternalError( + "swap v2 not supported for this coin".to_string(), + )), + } + } else { + Ok(Box::new(LegacyMakerSwapTotalFeeHelper { + my_coin: my_coin.deref(), + other_coin: other_coin.deref(), + volume, + stage, + })) + } +} diff --git a/mm2src/mm2_main/src/lp_swap/maker_swap.rs b/mm2src/mm2_main/src/lp_swap/maker_swap.rs index 0752cad77e..6f5196ee6e 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap.rs @@ -1,16 +1,17 @@ use super::check_balance::{ - check_my_coin_balance_for_swap, check_platform_coin_balance_for_swap, CheckBalanceError, CheckBalanceResult, + check_platform_coin_balance_for_swap, CheckBalanceError, CheckBalanceResult, SwapTotalFeeHelper, }; 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, 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, - SwapConfirmationsSettings, SwapError, SwapMsg, SwapPubkeys, SwapTxDataMsg, SwapsContext, TransactionIdentifier, - TAKER_FEE_VALIDATION_ATTEMPTS, TAKER_FEE_VALIDATION_RETRY_DELAY_SECS, WAIT_CONFIRM_INTERVAL_SEC, + broadcast_my_swap_status, broadcast_p2p_tx_msg, broadcast_swap_msg_every, check_coin_balances_for_swap, + check_other_coin_balance_for_swap, create_maker_total_fee_helper, 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, SwapConfirmationsSettings, SwapError, SwapMsg, + SwapPubkeys, SwapTxDataMsg, SwapsContext, TransactionIdentifier, TAKER_FEE_VALIDATION_ATTEMPTS, + TAKER_FEE_VALIDATION_RETRY_DELAY_SECS, WAIT_CONFIRM_INTERVAL_SEC, }; use crate::lp_dispatcher::{DispatcherContext, LpEvents}; use crate::lp_network::subscribe_to_topic; @@ -18,6 +19,7 @@ use crate::lp_ordermatch::MakerOrderBuilder; use crate::lp_swap::swap_events::{SwapStatusEvent, SwapStatusStreamer}; use crate::lp_swap::swap_v2_common::mark_swap_as_finished; use crate::lp_swap::{broadcast_swap_message, taker_payment_spend_duration, MAX_STARTED_AT_DIFF}; +use async_trait::async_trait; use coins::lp_price::fetch_swap_coins_price; use coins::{ CanRefundHtlc, CheckIfMyPaymentSentArgs, ConfirmPaymentInput, DexFee, FeeApproxStage, FoundSwapTxSpend, MmCoin, @@ -501,8 +503,7 @@ impl MakerSwap { // 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; - let get_sender_trade_fee_fut = self.maker_coin.get_sender_trade_fee(preimage_value, stage); - let maker_payment_trade_fee = match get_sender_trade_fee_fut.await { + let maker_payment_trade_fee = match self.maker_coin.get_sender_trade_fee(preimage_value, stage).await { Ok(fee) => fee, Err(e) => { return Ok(( @@ -513,8 +514,7 @@ impl MakerSwap { )) }, }; - let taker_payment_spend_trade_fee_fut = self.taker_coin.get_receiver_trade_fee(stage); - let taker_payment_spend_trade_fee = match taker_payment_spend_trade_fee_fut.compat().await { + let taker_payment_spend_trade_fee = match self.taker_coin.get_receiver_trade_fee(stage).compat().await { Ok(fee) => fee, Err(e) => { return Ok(( @@ -525,19 +525,16 @@ impl MakerSwap { )) }, }; - - let params = MakerSwapPreparedParams { - maker_payment_trade_fee: maker_payment_trade_fee.clone(), - taker_payment_spend_trade_fee: taker_payment_spend_trade_fee.clone(), - }; - match check_balance_for_maker_swap( + match check_coin_balances_for_swap( &self.ctx, - self.maker_coin.deref(), - self.taker_coin.deref(), - self.maker_amount.clone().into(), Some(&self.uuid), - Some(params), - stage, + &LegacyMakerSwapTotalFeeHelper { + my_coin: self.maker_coin.deref(), + other_coin: self.taker_coin.deref(), + volume: self.maker_amount.clone().into(), + stage, + }, + false, ) .await { @@ -2364,46 +2361,51 @@ pub async fn run_maker_swap(swap: RunMakerSwapInput, ctx: MmArc) { swap_ctx.running_swaps.lock().unwrap().remove(&uuid); } -pub struct MakerSwapPreparedParams { - pub(super) maker_payment_trade_fee: TradeFee, - pub(super) taker_payment_spend_trade_fee: TradeFee, +pub(crate) struct LegacyMakerSwapTotalFeeHelper<'a> { + pub(crate) my_coin: &'a dyn MmCoin, + pub(crate) other_coin: &'a dyn MmCoin, + pub(crate) volume: MmNumber, + pub(crate) stage: FeeApproxStage, } -pub async fn check_balance_for_maker_swap( - ctx: &MmArc, - my_coin: &dyn MmCoin, - other_coin: &dyn MmCoin, - volume: MmNumber, - swap_uuid: Option<&Uuid>, - prepared_params: Option, - stage: FeeApproxStage, -) -> CheckBalanceResult { - let (maker_payment_trade_fee, taker_payment_spend_trade_fee) = match prepared_params { - Some(MakerSwapPreparedParams { - maker_payment_trade_fee, - taker_payment_spend_trade_fee, - }) => (maker_payment_trade_fee, taker_payment_spend_trade_fee), - None => { - let preimage_value = TradePreimageValue::Exact(volume.to_decimal()); - let maker_payment_trade_fee = my_coin - .get_sender_trade_fee(preimage_value, stage) - .await - .mm_err(|e| CheckBalanceError::from_trade_preimage_error(e, my_coin.ticker()))?; - let taker_payment_spend_trade_fee = other_coin - .get_receiver_trade_fee(stage) - .compat() - .await - .mm_err(|e| CheckBalanceError::from_trade_preimage_error(e, other_coin.ticker()))?; - (maker_payment_trade_fee, taker_payment_spend_trade_fee) - }, - }; +#[async_trait] +impl SwapTotalFeeHelper for LegacyMakerSwapTotalFeeHelper<'_> { + fn get_my_coin(&self) -> &dyn MmCoin { + self.my_coin + } - let balance = - check_my_coin_balance_for_swap(ctx, my_coin, swap_uuid, volume, maker_payment_trade_fee, None).await?; - check_other_coin_balance_for_swap(ctx, other_coin, swap_uuid, taker_payment_spend_trade_fee).await?; - Ok(balance) -} + fn get_my_coin_volume(&self) -> MmNumber { + self.volume.clone() + } + + fn get_dex_fee(&self) -> Option { + None + } + + async fn get_my_coin_fees(&self, upper_bound_amount: bool) -> CheckBalanceResult { + let preimage_value = if upper_bound_amount { + TradePreimageValue::UpperBound(self.volume.clone().into()) + } else { + TradePreimageValue::Exact(self.volume.clone().into()) + }; + self.my_coin + .get_sender_trade_fee(preimage_value, self.stage) + .await + .mm_err(|e| CheckBalanceError::from_trade_preimage_error(e, self.my_coin.ticker())) + } + + fn get_other_coin(&self) -> &dyn MmCoin { + self.other_coin + } + async fn get_other_coin_fees(&self) -> CheckBalanceResult { + self.other_coin + .get_receiver_trade_fee(self.stage) + .compat() + .await + .mm_err(|e| CheckBalanceError::from_trade_preimage_error(e, self.other_coin.ticker())) + } +} pub struct MakerTradePreimage { /// The fee is paid per swap concerning the `base` coin. pub base_coin_fee: TradeFee, @@ -2439,17 +2441,15 @@ pub async fn maker_swap_trade_preimage( req.volume }; - let preimage_value = TradePreimageValue::Exact(volume.to_decimal()); - let base_coin_fee = base_coin - .get_sender_trade_fee(preimage_value, FeeApproxStage::TradePreimage) - .await - .mm_err(|e| TradePreimageRpcError::from_trade_preimage_error(e, base_coin_ticker))?; - let rel_coin_fee = rel_coin - .get_receiver_trade_fee(FeeApproxStage::TradePreimage) - .compat() - .await - .mm_err(|e| TradePreimageRpcError::from_trade_preimage_error(e, rel_coin_ticker))?; - + // Estimate tx fee for requested or calculated volume + let fee_helper = LegacyMakerSwapTotalFeeHelper { + my_coin: base_coin.deref(), + other_coin: rel_coin.deref(), + volume: volume.clone(), + stage: FeeApproxStage::TradePreimage, + }; + let base_coin_fee = fee_helper.get_my_coin_fees(false).await.map_mm_err()?; + let rel_coin_fee = fee_helper.get_other_coin_fees().await.map_mm_err()?; if req.max { // Note the `calc_max_maker_vol` returns [`CheckBalanceError::NotSufficientBalance`] error if the balance of `base_coin` is not sufficient. // So we have to check the balance of the other coin only. @@ -2457,21 +2457,9 @@ pub async fn maker_swap_trade_preimage( .await .map_mm_err()? } else { - let prepared_params = MakerSwapPreparedParams { - maker_payment_trade_fee: base_coin_fee.clone(), - taker_payment_spend_trade_fee: rel_coin_fee.clone(), - }; - check_balance_for_maker_swap( - ctx, - base_coin.deref(), - rel_coin.deref(), - volume.clone(), - None, - Some(prepared_params), - FeeApproxStage::TradePreimage, - ) - .await - .map_mm_err()?; + check_coin_balances_for_swap(ctx, None, &fee_helper, false) + .await + .map_mm_err()?; } let conf_settings = OrderConfirmationsSettings { @@ -2525,22 +2513,22 @@ pub async fn calc_max_maker_vol( let available = &MmNumber::from(balance) - &locked_by_swaps; let mut volume = available.clone(); - let preimage_value = TradePreimageValue::UpperBound(volume.to_decimal()); - let trade_fee = coin - .get_sender_trade_fee(preimage_value, stage) - .await - .mm_err(|e| CheckBalanceError::from_trade_preimage_error(e, ticker))?; + let fee_helper = create_maker_total_fee_helper( + ctx, + coin, + coin, // Send same coin as we won't need other_coin fees or dexfee here + volume.clone(), + stage, + )?; + let trade_fee = fee_helper.get_my_coin_fees(true).await?; debug!("{} trade fee {}", trade_fee.coin, trade_fee.amount.to_decimal()); let mut required_to_pay_fee = MmNumber::from(0); - if trade_fee.coin == ticker { + if coin.is_platform_coin() { volume = &volume - &trade_fee.amount; required_to_pay_fee = trade_fee.amount; } else { - let platform_coin_balance = coin.platform_coin_balance().compat().await.map_mm_err()?; - check_platform_coin_balance_for_swap(ctx, &MmNumber::from(platform_coin_balance), trade_fee.clone(), None) - .await - .map_mm_err()?; + check_platform_coin_balance_for_swap(ctx, coin.deref(), trade_fee.clone(), None).await?; } let min_tx_amount = MmNumber::from(coin.min_tx_amount()); if volume < min_tx_amount { 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 18679d945c..a2591fdafc 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs @@ -1,14 +1,14 @@ +use super::check_balance::{CheckBalanceError, CheckBalanceResult, SwapTotalFeeHelper}; use super::swap_events::{SwapStatusEvent, SwapStatusStreamer}; use super::swap_v2_common::*; use super::{ swap_v2_topic, LockedAmount, LockedAmountInfo, SavedTradeFee, SwapsContext, NEGOTIATE_SEND_INTERVAL, NEGOTIATION_TIMEOUT_SEC, }; -use crate::lp_swap::maker_swap::MakerSwapPreparedParams; use crate::lp_swap::swap_lock::SwapLock; use crate::lp_swap::swap_v2_pb::*; use crate::lp_swap::{ - broadcast_swap_v2_msg_every, check_balance_for_maker_swap, recv_swap_v2_msg, SwapConfirmationsSettings, + broadcast_swap_v2_msg_every, check_coin_balances_for_swap, recv_swap_v2_msg, SwapConfirmationsSettings, TransactionIdentifier, MAKER_SWAP_V2_TYPE, MAX_STARTED_AT_DIFF, }; use async_trait::async_trait; @@ -16,9 +16,10 @@ use bitcrypto::{dhash160, sha256}; use coins::hd_wallet::AddrToString; use coins::{ CanRefundHtlc, ConfirmPaymentInput, DexFee, FeeApproxStage, FundingTxSpend, GenTakerFundingSpendArgs, - GenTakerPaymentSpendArgs, MakerCoinSwapOpsV2, MmCoin, ParseCoinAssocTypes, RefundMakerPaymentSecretArgs, - RefundMakerPaymentTimelockArgs, SearchForFundingSpendErr, SendMakerPaymentArgs, SwapTxTypeWithSecretHash, - TakerCoinSwapOpsV2, ToBytes, TradePreimageValue, Transaction, TxPreimageWithSig, ValidateTakerFundingArgs, + GenTakerPaymentSpendArgs, GetFeeToSendMakerPaymentArgs, MakerCoinSwapOpsV2, MmCoin, ParseCoinAssocTypes, + RefundMakerPaymentSecretArgs, RefundMakerPaymentTimelockArgs, SearchForFundingSpendErr, SendMakerPaymentArgs, + SwapTxTypeWithSecretHash, TakerCoinSwapOpsV2, ToBytes, TradeFee, TradePreimageValue, Transaction, + TxPreimageWithSig, ValidateTakerFundingArgs, }; use common::executor::abortable_queue::AbortableQueue; use common::executor::{AbortableSystem, Timer}; @@ -383,7 +384,10 @@ impl MakerSwapDbRepr { } /// Represents the state machine for maker's side of the Trading Protocol Upgrade swap (v2). -pub struct MakerSwapStateMachine { +pub struct MakerSwapStateMachine< + MakerCoin: MmCoin + MakerCoinSwapOpsV2 + Clone, + TakerCoin: MmCoin + TakerCoinSwapOpsV2 + Clone, +> { /// MM2 context pub ctx: MmArc, /// Storage @@ -426,7 +430,7 @@ pub struct MakerSwapStateMachine +impl MakerSwapStateMachine { /// Timeout for taker payment's on-chain confirmation. @@ -482,8 +486,8 @@ impl StorableStateMachine - for MakerSwapStateMachine +impl + StorableStateMachine for MakerSwapStateMachine { type Storage = MakerSwapStorage; type Result = (); @@ -887,14 +891,14 @@ impl Default for Initialize { } } -impl InitialState +impl InitialState for Initialize { type StateMachine = MakerSwapStateMachine; } #[async_trait] -impl State +impl State for Initialize { type StateMachine = MakerSwapStateMachine; @@ -916,13 +920,13 @@ impl fee, Err(e) => { let reason = AbortReason::FailedToGetMakerPaymentFee(e.to_string()); @@ -930,30 +934,15 @@ impl fee, Err(e) => { let reason = AbortReason::FailedToGetTakerPaymentSpendFee(e.to_string()); return Self::change_state(Aborted::new(reason), state_machine).await; }, }; - - let prepared_params = MakerSwapPreparedParams { - maker_payment_trade_fee: maker_payment_trade_fee.clone(), - taker_payment_spend_trade_fee: taker_payment_spend_trade_fee.clone(), - }; - - if let Err(e) = check_balance_for_maker_swap( - &state_machine.ctx, - &state_machine.maker_coin, - &state_machine.taker_coin, - state_machine.maker_volume.clone(), - Some(&state_machine.uuid), - Some(prepared_params), - FeeApproxStage::StartSwap, - ) - .await + if let Err(e) = + check_coin_balances_for_swap(&state_machine.ctx, Some(&state_machine.uuid), &fee_helper, false).await { let reason = AbortReason::BalanceCheckFailure(e.to_string()); return Self::change_state(Aborted::new(reason), state_machine).await; @@ -983,7 +972,7 @@ struct Initialized { impl TransitionFrom> for Initialized {} -impl StorableState +impl StorableState for Initialized { type StateMachine = MakerSwapStateMachine; @@ -999,7 +988,7 @@ impl State +impl State for Initialized { type StateMachine = MakerSwapStateMachine; @@ -1170,7 +1159,7 @@ impl TransitionF } #[async_trait] -impl State +impl State for WaitingForTakerFunding { type StateMachine = MakerSwapStateMachine; @@ -1227,7 +1216,7 @@ impl StorableState +impl StorableState for WaitingForTakerFunding { type StateMachine = MakerSwapStateMachine; @@ -1256,7 +1245,7 @@ impl } #[async_trait] -impl State +impl State for TakerFundingReceived { type StateMachine = MakerSwapStateMachine; @@ -1336,7 +1325,7 @@ impl StorableState +impl StorableState for TakerFundingReceived { type StateMachine = MakerSwapStateMachine; @@ -1371,7 +1360,7 @@ impl } #[async_trait] -impl State +impl State for MakerPaymentSentFundingSpendGenerated { type StateMachine = MakerSwapStateMachine; @@ -1495,7 +1484,7 @@ impl StorableState +impl StorableState for MakerPaymentSentFundingSpendGenerated { type StateMachine = MakerSwapStateMachine; @@ -1566,7 +1555,7 @@ impl } #[async_trait] -impl State +impl State for MakerPaymentRefundRequired { type StateMachine = MakerSwapStateMachine; @@ -1662,7 +1651,7 @@ impl StorableState +impl StorableState for MakerPaymentRefundRequired { type StateMachine = MakerSwapStateMachine; @@ -1696,7 +1685,7 @@ impl } #[async_trait] -impl State +impl State for TakerPaymentReceived { type StateMachine = MakerSwapStateMachine; @@ -1838,7 +1827,7 @@ impl StorableState +impl StorableState for TakerPaymentReceived { type StateMachine = MakerSwapStateMachine; @@ -1876,7 +1865,7 @@ impl } #[async_trait] -impl State +impl State for TakerPaymentReceivedAndPreimageValidationSkipped { type StateMachine = MakerSwapStateMachine; @@ -1952,7 +1941,7 @@ impl StorableState +impl StorableState for TakerPaymentReceivedAndPreimageValidationSkipped { type StateMachine = MakerSwapStateMachine; @@ -1995,7 +1984,7 @@ impl } #[async_trait] -impl State +impl State for TakerPaymentSpent { type StateMachine = MakerSwapStateMachine; @@ -2026,7 +2015,7 @@ impl StorableState +impl StorableState for TakerPaymentSpent { type StateMachine = MakerSwapStateMachine; @@ -2092,7 +2081,7 @@ impl Aborted { } #[async_trait] -impl LastState +impl LastState for Aborted { type StateMachine = MakerSwapStateMachine; @@ -2105,7 +2094,7 @@ impl StorableState +impl StorableState for Aborted { type StateMachine = MakerSwapStateMachine; @@ -2146,7 +2135,7 @@ impl Completed { } } -impl StorableState +impl StorableState for Completed { type StateMachine = MakerSwapStateMachine; @@ -2157,7 +2146,7 @@ impl LastState +impl LastState for Completed { type StateMachine = MakerSwapStateMachine; @@ -2182,7 +2171,7 @@ struct MakerPaymentRefunded { reason: MakerPaymentRefundReason, } -impl StorableState +impl StorableState for MakerPaymentRefunded { type StateMachine = MakerSwapStateMachine; @@ -2203,7 +2192,7 @@ impl LastState +impl LastState for MakerPaymentRefunded { type StateMachine = MakerSwapStateMachine; @@ -2223,3 +2212,58 @@ impl TransitionFrom> for MakerPaymentRefunded { } + +pub(crate) struct MakerSwapV2TotalFeeHelper< + MakerCoin: MmCoin + MakerCoinSwapOpsV2, + TakerCoin: MmCoin + TakerCoinSwapOpsV2, +> { + pub(crate) my_coin: MakerCoin, + pub(crate) other_coin: TakerCoin, + pub(crate) volume: MmNumber, + pub(crate) stage: FeeApproxStage, +} + +#[async_trait] +impl SwapTotalFeeHelper + for MakerSwapV2TotalFeeHelper +{ + fn get_my_coin(&self) -> &dyn MmCoin { + &self.my_coin + } + + fn get_my_coin_volume(&self) -> MmNumber { + self.volume.clone() + } + + fn get_dex_fee(&self) -> Option { + None + } + + async fn get_my_coin_fees(&self, upper_bound_amount: bool) -> CheckBalanceResult { + let preimage_value = if upper_bound_amount { + TradePreimageValue::UpperBound(self.volume.clone().into()) + } else { + TradePreimageValue::Exact(self.volume.clone().into()) + }; + let fee = self + .my_coin + .get_fee_to_send_maker_payment_v2(GetFeeToSendMakerPaymentArgs { + stage: self.stage, + amount: preimage_value, + }) + .await + .mm_err(|e| CheckBalanceError::from_trade_preimage_error(e, self.my_coin.ticker()))?; + Ok(fee) + } + + fn get_other_coin(&self) -> &dyn MmCoin { + &self.other_coin + } + + async fn get_other_coin_fees(&self) -> CheckBalanceResult { + self.other_coin + .get_fee_to_spend_taker_payment(self.stage) + .await + .mm_err(|e| CheckBalanceError::from_trade_preimage_error(e, self.other_coin.ticker())) + } +} diff --git a/mm2src/mm2_main/src/lp_swap/swap_v2_common.rs b/mm2src/mm2_main/src/lp_swap/swap_v2_common.rs index 1f59532155..d250e1c264 100644 --- a/mm2src/mm2_main/src/lp_swap/swap_v2_common.rs +++ b/mm2src/mm2_main/src/lp_swap/swap_v2_common.rs @@ -365,8 +365,8 @@ pub(super) async fn swap_kickstart_coins( /// Handles the recreation and kickstart of a swap state machine. pub(super) async fn swap_kickstart_handler< T: StorableStateMachine>, - MakerCoin: MmCoin + MakerCoinSwapOpsV2, - TakerCoin: MmCoin + TakerCoinSwapOpsV2, + MakerCoin: MmCoin + MakerCoinSwapOpsV2 + Clone, + TakerCoin: MmCoin + TakerCoinSwapOpsV2 + Clone, >( swap_repr: ::DbRepr, storage: T::Storage, diff --git a/mm2src/mm2_main/src/lp_swap/taker_swap.rs b/mm2src/mm2_main/src/lp_swap/taker_swap.rs index 9c346cd6e5..a5062d31a8 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_swap.rs @@ -1,16 +1,14 @@ -use super::check_balance::{ - check_my_coin_balance_for_swap, CheckBalanceError, CheckBalanceResult, TakerFeeAdditionalInfo, -}; +use super::check_balance::{CheckBalanceError, CheckBalanceResult, SwapTotalFeeHelper}; use super::pubkey_banning::ban_pubkey_on_failed_swap; 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, - 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, WAIT_CONFIRM_INTERVAL_SEC, + broadcast_my_swap_status, broadcast_swap_message, broadcast_swap_msg_every, check_coin_balances_for_swap, + create_taker_total_fee_helper, 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, WAIT_CONFIRM_INTERVAL_SEC, }; use crate::lp_network::subscribe_to_topic; use crate::lp_ordermatch::TakerOrderBuilder; @@ -21,6 +19,7 @@ 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 async_trait::async_trait; use coins::lp_price::fetch_swap_coins_price; use coins::{ lp_coinfind, CanRefundHtlc, CheckIfMyPaymentSentArgs, ConfirmPaymentInput, DexFee, FeeApproxStage, @@ -1086,8 +1085,7 @@ impl TakerSwap { 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 { + match self.taker_coin.get_fee_to_send_taker_fee(dex_fee.clone(), stage).await { Ok(fee) => fee, Err(e) => { return Ok(( @@ -1099,8 +1097,7 @@ impl TakerSwap { }, } }; - let get_sender_trade_fee_fut = self.taker_coin.get_sender_trade_fee(preimage_value, stage); - let taker_payment_trade_fee = match get_sender_trade_fee_fut.await { + let taker_payment_trade_fee = match self.taker_coin.get_sender_trade_fee(preimage_value, stage).await { Ok(fee) => fee, Err(e) => { return Ok(( @@ -1111,8 +1108,7 @@ impl TakerSwap { )) }, }; - let maker_payment_spend_trade_fee_fut = self.maker_coin.get_receiver_trade_fee(stage); - let maker_payment_spend_trade_fee = match maker_payment_spend_trade_fee_fut.compat().await { + let maker_payment_spend_trade_fee = match self.maker_coin.get_receiver_trade_fee(stage).compat().await { Ok(fee) => fee, Err(e) => { return Ok(( @@ -1123,27 +1119,18 @@ impl TakerSwap { )) }, }; - - let params = TakerSwapPreparedParams { - dex_fee: dex_fee.total_spend_amount(), - fee_to_send_dex_fee: fee_to_send_dex_fee.clone(), - taker_payment_trade_fee: taker_payment_trade_fee.clone(), - maker_payment_spend_trade_fee: maker_payment_spend_trade_fee.clone(), - }; - let check_balance_f = check_balance_for_taker_swap( - &self.ctx, - self.taker_coin.deref(), - self.maker_coin.deref(), - self.taker_amount.clone(), - Some(&self.uuid), - Some(params), + let fee_helper = LegacyTakerSwapTotalFeeHelper { + my_coin: self.taker_coin.deref(), + other_coin: self.maker_coin.deref(), + volume: self.taker_amount.clone(), + dex_fee, stage, - ); - if let Err(e) = check_balance_f.await { + }; + if let Err(e) = check_coin_balances_for_swap(&self.ctx, Some(&self.uuid), &fee_helper, false).await { return Ok(( Some(TakerSwapCommand::Finish), vec![TakerSwapEvent::StartFailed( - ERRL!("!check_balance_for_taker_swap {}", e).into(), + ERRL!("!check_balance_for_swap {}", e).into(), )], )); } @@ -2621,45 +2608,69 @@ impl AtomicSwap for TakerSwap { } } -pub struct TakerSwapPreparedParams { - pub(super) dex_fee: MmNumber, - pub(super) fee_to_send_dex_fee: TradeFee, - pub(super) taker_payment_trade_fee: TradeFee, - pub(super) maker_payment_spend_trade_fee: TradeFee, +pub(crate) struct LegacyTakerSwapTotalFeeHelper<'a> { + pub(crate) my_coin: &'a dyn MmCoin, + pub(crate) other_coin: &'a dyn MmCoin, + pub(crate) volume: MmNumber, + pub(crate) dex_fee: DexFee, + pub(crate) stage: FeeApproxStage, } -pub async fn check_balance_for_taker_swap( - ctx: &MmArc, - my_coin: &dyn MmCoin, - other_coin: &dyn MmCoin, - volume: MmNumber, - swap_uuid: Option<&Uuid>, - prepared_params: Option, - stage: FeeApproxStage, -) -> CheckBalanceResult<()> { - let fee_params = match prepared_params { - Some(params) => params, - None => create_taker_swap_default_params(my_coin, other_coin, volume.clone(), stage).await?, - }; +#[async_trait] +impl SwapTotalFeeHelper for LegacyTakerSwapTotalFeeHelper<'_> { + fn get_my_coin(&self) -> &dyn MmCoin { + self.my_coin + } - let taker_fee = TakerFeeAdditionalInfo { - dex_fee: fee_params.dex_fee, - fee_to_send_dex_fee: fee_params.fee_to_send_dex_fee, - }; + fn get_my_coin_volume(&self) -> MmNumber { + self.volume.clone() + } - check_my_coin_balance_for_swap( - ctx, - my_coin, - swap_uuid, - volume, - fee_params.taker_payment_trade_fee, - Some(taker_fee), - ) - .await?; - if !fee_params.maker_payment_spend_trade_fee.paid_from_trading_vol { - check_other_coin_balance_for_swap(ctx, other_coin, swap_uuid, fee_params.maker_payment_spend_trade_fee).await?; + fn get_dex_fee(&self) -> Option { + Some(self.dex_fee.total_spend_amount()) + } + + async fn get_my_coin_fees(&self, upper_bound_amount: bool) -> CheckBalanceResult { + let fee_to_send_dex_fee = if matches!(self.dex_fee, DexFee::NoFee) { + TradeFee { + coin: self.my_coin.ticker().to_owned(), + amount: MmNumber::from(0), + paid_from_trading_vol: false, + } + } else { + self.my_coin + .get_fee_to_send_taker_fee(self.dex_fee.clone(), self.stage) + .await + .mm_err(|e| CheckBalanceError::from_trade_preimage_error(e, self.my_coin.ticker()))? + }; + let preimage_value = if upper_bound_amount { + TradePreimageValue::UpperBound(self.volume.to_decimal()) + } else { + TradePreimageValue::Exact(self.volume.to_decimal()) + }; + let taker_payment_trade_fee = self + .my_coin + .get_sender_trade_fee(preimage_value, self.stage) + .await + .mm_err(|e| CheckBalanceError::from_trade_preimage_error(e, self.my_coin.ticker()))?; + Ok(TradeFee { + coin: fee_to_send_dex_fee.coin, + amount: fee_to_send_dex_fee.amount + taker_payment_trade_fee.amount, + paid_from_trading_vol: fee_to_send_dex_fee.paid_from_trading_vol, + }) + } + + fn get_other_coin(&self) -> &dyn MmCoin { + self.other_coin + } + + async fn get_other_coin_fees(&self) -> CheckBalanceResult { + self.other_coin + .get_receiver_trade_fee(self.stage) + .compat() + .await + .mm_err(|e| CheckBalanceError::from_trade_preimage_error(e, self.other_coin.ticker())) } - Ok(()) } pub struct TakerTradePreimage { @@ -2670,6 +2681,7 @@ pub struct TakerTradePreimage { /// The dex fee to be paid by taker coin. pub taker_fee: TradeFee, /// The miner fee is paid to send the dex fee. + /// TODO: we don't need this for TPU pub fee_to_send_taker_fee: TradeFee, } @@ -2734,20 +2746,17 @@ pub async fn taker_swap_trade_preimage( .await .mm_err(|e| TradePreimageRpcError::from_trade_preimage_error(e, other_coin_ticker))?; - let prepared_params = TakerSwapPreparedParams { - 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(), - }; - check_balance_for_taker_swap( + check_coin_balances_for_swap( ctx, - my_coin.deref(), - other_coin.deref(), - my_coin_volume.clone(), None, - Some(prepared_params), - stage, + &LegacyTakerSwapTotalFeeHelper { + my_coin: my_coin.deref(), + other_coin: other_coin.deref(), + volume: my_coin_volume.clone(), + dex_fee, + stage, + }, + req.max, ) .await .map_mm_err()?; @@ -2797,9 +2806,13 @@ pub async fn max_taker_vol(ctx: MmArc, req: Json) -> Result>, S Ok(None) => return ERR!("No such coin: {}", req.coin), Err(err) => return ERR!("!lp_coinfind({}): {}", req.coin, err), }; - let other_coin = req.trade_with.as_ref().unwrap_or(&req.coin); - let fut = calc_max_taker_vol(&ctx, &coin, other_coin, FeeApproxStage::TradePreimageMax); - let max_vol = match fut.await { + let other_coin_str = req.trade_with.as_ref().unwrap_or(&req.coin); + let other_coin = match lp_coinfind(&ctx, other_coin_str).await { + Ok(Some(t)) => t, + Ok(None) => return ERR!("No such coin: {}", other_coin_str), + Err(err) => return ERR!("!lp_coinfind({}): {}", other_coin_str, err), + }; + let max_vol = match calc_max_taker_vol(&ctx, &coin, &other_coin, FeeApproxStage::TradePreimageMax).await { Ok(max_vol) => max_vol, Err(e) if e.get_inner().not_sufficient_balance() => { warn!("{}", e); @@ -2874,50 +2887,53 @@ pub async fn max_taker_vol(ctx: MmArc, req: Json) -> Result>, S pub async fn calc_max_taker_vol( ctx: &MmArc, coin: &MmCoinEnum, - other_coin: &str, + other_coin: &MmCoinEnum, stage: FeeApproxStage, ) -> CheckBalanceResult { let my_coin = coin.ticker(); let balance: MmNumber = coin.my_spendable_balance().compat().await.map_mm_err()?.into(); let locked = get_locked_amount(ctx, my_coin); let min_tx_amount = MmNumber::from(coin.min_tx_amount()); - let max_possible = &balance - &locked; - let preimage_value = TradePreimageValue::UpperBound(max_possible.to_decimal()); - let max_trade_fee = coin - .get_sender_trade_fee(preimage_value, stage) - .await - .mm_err(|e| CheckBalanceError::from_trade_preimage_error(e, my_coin))?; - - 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 = DexFee::new_from_taker_coin(coin.deref(), other_coin, &max_possible_2); // taker_pubkey is not known yet so we get max dex fee to calc max volume - let max_fee_to_send_taker_fee = coin - .get_fee_to_send_taker_fee(max_dex_fee.clone(), stage) - .await - .mm_err(|e| CheckBalanceError::from_trade_preimage_error(e, my_coin))?; - let min_max_possible = &max_possible_2 - &max_fee_to_send_taker_fee.amount; - + let dex_fee_rate = DexFee::dex_fee_rate(my_coin, other_coin.ticker()); + let fee_helper = create_taker_total_fee_helper( + ctx, + coin, + other_coin, + &max_possible / &(MmNumber::from(1) + dex_fee_rate), + None, + stage, + )?; + let max_vol = if coin.is_platform_coin() { + // second case (fee are paid in this coin) + let max_total_fees = fee_helper.get_my_coin_fees(true).await?; + let min_max_possible = &max_possible - &max_total_fees.amount; debug!( - "max_taker_vol case 2: min_max_possible {:?}, balance {:?}, locked {:?}, max_trade_fee {:?}, max_dex_fee {:?}, max_fee_to_send_taker_fee {:?}", + "max_taker_vol case 2: min_max_possible {:?}, balance {:?}, locked {:?}, max_total_fees {:?}, max_dex_fee {:?}", min_max_possible.to_fraction(), balance.to_fraction(), locked.to_fraction(), - max_trade_fee.amount.to_fraction(), - max_dex_fee.total_spend_amount().to_fraction(), - max_fee_to_send_taker_fee.amount.to_fraction() + max_total_fees.amount.to_fraction(), + fee_helper.get_dex_fee().map(|dex_fee| dex_fee.to_fraction()) ); - max_taker_vol_from_available(min_max_possible, my_coin, other_coin, &min_tx_amount) + if min_max_possible < MmNumber::from(0) { + return MmError::err(CheckBalanceError::NotSufficientBalance { + coin: my_coin.to_string(), + available: balance.to_decimal(), + required: max_total_fees.amount.to_decimal(), + locked_by_swaps: Some(locked.to_decimal()), + }); + } + max_taker_vol_from_available(min_max_possible, my_coin, other_coin.ticker(), &min_tx_amount) .mm_err(|e| CheckBalanceError::from_max_taker_vol_error(e, my_coin.to_owned(), locked.to_decimal()))? } else { - // first case + // first case (fee are paid in platform coin) debug!( "max_taker_vol case 1: balance {:?}, locked {:?}", balance.to_fraction(), locked.to_fraction() ); - max_taker_vol_from_available(max_possible, my_coin, other_coin, &min_tx_amount) + max_taker_vol_from_available(max_possible, my_coin, other_coin.ticker(), &min_tx_amount) .mm_err(|e| CheckBalanceError::from_max_taker_vol_error(e, my_coin.to_owned(), locked.to_decimal()))? }; // do not check if `max_vol < min_tx_amount`, because it is checked within `max_taker_vol_from_available` already @@ -2954,36 +2970,6 @@ pub fn max_taker_vol_from_available( Ok(max_vol) } -/// Get dex fee and trade fee, including fee to spend maker coin (if requested) -pub async fn create_taker_swap_default_params( - my_coin: &dyn MmCoin, - other_coin: &dyn MmCoin, - volume: MmNumber, - stage: FeeApproxStage, -) -> CheckBalanceResult { - let dex_fee = DexFee::new_from_taker_coin(my_coin, other_coin.ticker(), &volume); // taker_pubkey is not known yet so we get max dexfee to estimate max swap amount - let fee_to_send_dex_fee = my_coin - .get_fee_to_send_taker_fee(dex_fee.clone(), stage) - .await - .mm_err(|e| CheckBalanceError::from_trade_preimage_error(e, my_coin.ticker()))?; - let preimage_value = TradePreimageValue::Exact(volume.to_decimal()); - let taker_payment_trade_fee = my_coin - .get_sender_trade_fee(preimage_value, stage) - .await - .mm_err(|e| CheckBalanceError::from_trade_preimage_error(e, my_coin.ticker()))?; - let maker_payment_spend_trade_fee = other_coin - .get_receiver_trade_fee(stage) - .compat() - .await - .mm_err(|e| CheckBalanceError::from_trade_preimage_error(e, other_coin.ticker()))?; - Ok(TakerSwapPreparedParams { - dex_fee: dex_fee.total_spend_amount(), - fee_to_send_dex_fee, - taker_payment_trade_fee, - maker_payment_spend_trade_fee, - }) -} - #[cfg(all(test, not(target_arch = "wasm32")))] mod taker_swap_tests { use std::sync::atomic::AtomicUsize; 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 412bcbae1c..8898a372d1 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs @@ -1,13 +1,13 @@ +use super::check_balance::{CheckBalanceError, CheckBalanceResult, SwapTotalFeeHelper}; use super::swap_events::{SwapStatusEvent, SwapStatusStreamer}; use super::swap_v2_common::*; use super::{ - LockedAmount, LockedAmountInfo, SavedTradeFee, SwapsContext, TakerSwapPreparedParams, NEGOTIATE_SEND_INTERVAL, - NEGOTIATION_TIMEOUT_SEC, + LockedAmount, LockedAmountInfo, SavedTradeFee, SwapsContext, NEGOTIATE_SEND_INTERVAL, NEGOTIATION_TIMEOUT_SEC, }; use crate::lp_swap::swap_lock::SwapLock; use crate::lp_swap::swap_v2_pb::*; use crate::lp_swap::{ - broadcast_swap_v2_msg_every, check_balance_for_taker_swap, recv_swap_v2_msg, swap_v2_topic, + broadcast_swap_v2_msg_every, check_coin_balances_for_swap, recv_swap_v2_msg, swap_v2_topic, SwapConfirmationsSettings, TransactionIdentifier, MAX_STARTED_AT_DIFF, TAKER_SWAP_V2_TYPE, }; use async_trait::async_trait; @@ -15,9 +15,9 @@ use bitcrypto::{dhash160, sha256}; use coins::hd_wallet::AddrToString; use coins::{ CanRefundHtlc, ConfirmPaymentInput, DexFee, FeeApproxStage, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, - MakerCoinSwapOpsV2, MmCoin, ParseCoinAssocTypes, RefundFundingSecretArgs, RefundTakerPaymentArgs, - SendTakerFundingArgs, SpendMakerPaymentArgs, SwapTxTypeWithSecretHash, TakerCoinSwapOpsV2, ToBytes, TradeFee, - TradePreimageValue, Transaction, TxPreimageWithSig, ValidateMakerPaymentArgs, + GetTakerFundingFeeArgs, MakerCoinSwapOpsV2, MmCoin, ParseCoinAssocTypes, RefundFundingSecretArgs, + RefundTakerPaymentArgs, SendTakerFundingArgs, SpendMakerPaymentArgs, SwapTxTypeWithSecretHash, TakerCoinSwapOpsV2, + ToBytes, TradeFee, Transaction, TxPreimageWithSig, ValidateMakerPaymentArgs, }; use common::executor::abortable_queue::AbortableQueue; use common::executor::{AbortableSystem, Timer}; @@ -413,7 +413,10 @@ impl GetSwapCoins for TakerSwapDbRepr { } /// Represents the state machine for taker's side of the Trading Protocol Upgrade swap (v2). -pub struct TakerSwapStateMachine { +pub struct TakerSwapStateMachine< + MakerCoin: MmCoin + MakerCoinSwapOpsV2 + Clone, + TakerCoin: MmCoin + TakerCoinSwapOpsV2 + Clone, +> { /// MM2 context. pub ctx: MmArc, /// Storage. @@ -458,7 +461,7 @@ pub struct TakerSwapStateMachine +impl TakerSwapStateMachine { #[inline] @@ -510,8 +513,8 @@ impl StorableStateMachine - for TakerSwapStateMachine +impl + StorableStateMachine for TakerSwapStateMachine { type Storage = TakerSwapStorage; type Result = (); @@ -1000,14 +1003,14 @@ impl Default for Initialize { } } -impl InitialState +impl InitialState for Initialize { type StateMachine = TakerSwapStateMachine; } #[async_trait] -impl State +impl State for Initialize { type StateMachine = TakerSwapStateMachine; @@ -1029,16 +1032,16 @@ impl fee, Err(e) => { let reason = AbortReason::FailedToGetTakerPaymentFee(e.to_string()); @@ -1046,7 +1049,7 @@ impl fee, Err(e) => { let reason = AbortReason::FailedToGetMakerPaymentSpendFee(e.to_string()); @@ -1054,28 +1057,8 @@ impl { impl TransitionFrom> for Initialized {} -impl StorableState +impl StorableState for Initialized { type StateMachine = TakerSwapStateMachine; @@ -1121,7 +1104,7 @@ impl State +impl State for Initialized { type StateMachine = TakerSwapStateMachine; @@ -1321,7 +1304,7 @@ impl TransitionFr } #[async_trait] -impl State +impl State for Negotiated { type StateMachine = TakerSwapStateMachine; @@ -1364,7 +1347,7 @@ impl StorableState +impl StorableState for Negotiated { type StateMachine = TakerSwapStateMachine; @@ -1388,7 +1371,7 @@ struct TakerFundingSent State +impl State for TakerFundingSent { type StateMachine = TakerSwapStateMachine; @@ -1503,7 +1486,7 @@ impl TransitionF { } -impl StorableState +impl StorableState for TakerFundingSent { type StateMachine = TakerSwapStateMachine; @@ -1536,7 +1519,7 @@ impl { } -impl StorableState +impl StorableState for MakerPaymentAndFundingSpendPreimgReceived { type StateMachine = TakerSwapStateMachine; @@ -1563,7 +1546,7 @@ impl State +impl State for MakerPaymentAndFundingSpendPreimgReceived { type StateMachine = TakerSwapStateMachine; @@ -1726,7 +1709,7 @@ impl } #[async_trait] -impl State +impl State for TakerPaymentSent { type StateMachine = TakerSwapStateMachine; @@ -1836,7 +1819,7 @@ impl StorableState +impl StorableState for TakerPaymentSent { type StateMachine = TakerSwapStateMachine; @@ -1878,7 +1861,7 @@ impl } #[async_trait] -impl State +impl State for TakerPaymentSentAndPreimageSendingSkipped { type StateMachine = TakerSwapStateMachine; @@ -1943,7 +1926,7 @@ impl StorableState +impl StorableState for TakerPaymentSentAndPreimageSendingSkipped { type StateMachine = TakerSwapStateMachine; @@ -2001,7 +1984,7 @@ impl } #[async_trait] -impl State +impl State for TakerFundingRefundRequired { type StateMachine = TakerSwapStateMachine; @@ -2048,7 +2031,7 @@ impl StorableState +impl StorableState for TakerFundingRefundRequired { type StateMachine = TakerSwapStateMachine; @@ -2100,7 +2083,7 @@ impl } #[async_trait] -impl State +impl State for TakerPaymentRefundRequired { type StateMachine = TakerSwapStateMachine; @@ -2166,7 +2149,7 @@ impl StorableState +impl StorableState for TakerPaymentRefundRequired { type StateMachine = TakerSwapStateMachine; @@ -2199,7 +2182,7 @@ impl } #[async_trait] -impl State +impl State for MakerPaymentConfirmed { type StateMachine = TakerSwapStateMachine; @@ -2264,7 +2247,7 @@ impl StorableState +impl StorableState for MakerPaymentConfirmed { type StateMachine = TakerSwapStateMachine; @@ -2310,7 +2293,7 @@ impl } #[async_trait] -impl State +impl State for TakerPaymentSpent { type StateMachine = TakerSwapStateMachine; @@ -2366,7 +2349,7 @@ impl StorableState +impl StorableState for TakerPaymentSpent { type StateMachine = TakerSwapStateMachine; @@ -2407,7 +2390,7 @@ impl { } -impl StorableState +impl StorableState for MakerPaymentSpent { type StateMachine = TakerSwapStateMachine; @@ -2438,7 +2421,7 @@ impl State +impl State for MakerPaymentSpent { type StateMachine = TakerSwapStateMachine; @@ -2507,7 +2490,7 @@ impl Aborted { } #[async_trait] -impl LastState +impl LastState for Aborted { type StateMachine = TakerSwapStateMachine; @@ -2520,7 +2503,7 @@ impl StorableState +impl StorableState for Aborted { type StateMachine = TakerSwapStateMachine; @@ -2565,7 +2548,7 @@ impl Completed { } } -impl StorableState +impl StorableState for Completed { type StateMachine = TakerSwapStateMachine; @@ -2576,7 +2559,7 @@ impl LastState +impl LastState for Completed { type StateMachine = TakerSwapStateMachine; @@ -2601,7 +2584,7 @@ struct TakerFundingRefunded StorableState +impl StorableState for TakerFundingRefunded { type StateMachine = TakerSwapStateMachine; @@ -2622,7 +2605,7 @@ impl LastState +impl LastState for TakerFundingRefunded { type StateMachine = TakerSwapStateMachine; @@ -2650,7 +2633,7 @@ struct TakerPaymentRefunded StorableState +impl StorableState for TakerPaymentRefunded { type StateMachine = TakerSwapStateMachine; @@ -2668,7 +2651,7 @@ impl LastState +impl LastState for TakerPaymentRefunded { type StateMachine = TakerSwapStateMachine; @@ -2688,3 +2671,91 @@ impl TransitionFrom> for TakerPaymentRefunded { } + +pub(crate) struct TakerSwapV2TotalFeeHelper< + MakerCoin: MmCoin + MakerCoinSwapOpsV2, + TakerCoin: MmCoin + TakerCoinSwapOpsV2, +> { + pub(crate) my_coin: TakerCoin, + pub(crate) other_coin: MakerCoin, + pub(crate) volume: MmNumber, + pub(crate) dex_fee: DexFee, + pub(crate) stage: FeeApproxStage, +} + +#[async_trait] +impl SwapTotalFeeHelper + for TakerSwapV2TotalFeeHelper +{ + fn get_my_coin(&self) -> &dyn MmCoin { + &self.my_coin + } + + fn get_my_coin_volume(&self) -> MmNumber { + self.volume.clone() + } + + fn get_dex_fee(&self) -> Option { + Some(self.dex_fee.total_spend_amount()) + } + + async fn get_my_coin_fees(&self, upper_bound_amount: bool) -> CheckBalanceResult { + let funding_fee = self + .my_coin + .get_fee_to_send_taker_funding(GetTakerFundingFeeArgs { + dex_fee: self.dex_fee.clone(), + stage: self.stage, + premium_amount: 0.into(), + trading_amount: self.volume.clone().into(), + upper_bound_amount, + }) + .await + .mm_err(|e| CheckBalanceError::from_trade_preimage_error(e, self.my_coin.ticker()))?; + let spend_fees = self + .my_coin + .get_fee_to_spend_taker_funding(self.stage) + .await + .mm_err(|e| CheckBalanceError::from_trade_preimage_error(e, self.my_coin.ticker()))?; + // Add fees both to send taker funding and spend funding. + let mut total_amount = funding_fee.amount + spend_fees.amount; + if matches!( + self.stage, + FeeApproxStage::OrderIssueMax | FeeApproxStage::TradePreimageMax + ) { + // Also add min_tx_amount to compensate dust in the change for utxo coins, when calculating max vol. + // Why do we ever need this? Example: + // Suppose we have one input of 50.0 coins and would like to spend it as max taker vol. + // See calc_max_taker_vol fn: + // Then we get total trade fees to send the whole 50.0 as taker funding and (then spend that funding): + // Let this be 0.00000274 + 0.00000497 = 0.00000771. We subtract it from balance: 50.0 - 0.00000771 = 49.99999229 + // Because we subtract trade fees *from* the input the dust does not make any difference. + // Then we estimate the needed max vol by removing dexfee: 49.99999229 / (1 + 9/7770) = 49.9421442464713 + // + // Later we call "sell" to start a swap with this amount 49.9421442464713 + // In the "sell" we check the required balance and again get total fees. + // We again get fee to send taker funding and required fee is still 0.00000274 + // but now a change output appears (because now it's not upper bound), as 50.0 - 49.9421442464713 - 0.00000274 = 0.00000497 + // If the latter amount is dust it is added to txfee and the total estimated fee becomes greater: 0.00000771 + 0.00000497 = 0.00001268 + // so check balance may fail due to insufficient balance. + // To compensate this possible tx fee grow we add dust at the calc_max_taker_vol stage. + let min_tx_amount = MmNumber::from(self.my_coin.min_tx_amount()); + total_amount += min_tx_amount; + } + Ok(TradeFee { + coin: funding_fee.coin, + amount: total_amount, + paid_from_trading_vol: funding_fee.paid_from_trading_vol, + }) + } + + fn get_other_coin(&self) -> &dyn MmCoin { + &self.other_coin + } + + async fn get_other_coin_fees(&self) -> CheckBalanceResult { + self.other_coin + .get_fee_to_spend_maker_payment_v2(self.stage) + .await + .mm_err(|e| CheckBalanceError::from_trade_preimage_error(e, self.other_coin.ticker())) + } +} 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 4f7e40532e..2f26221dda 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs @@ -901,9 +901,7 @@ fn test_order_should_be_updated_when_matched_partially() { block_on(mm_alice.stop()).unwrap(); } -#[test] -// https://github.com/KomodoPlatform/atomicDEX-API/issues/471 -fn test_match_and_trade_setprice_max() { +fn test_match_and_trade_setprice_max_impl(is_tpu: bool) { 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", 2000.into()); let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); @@ -914,11 +912,12 @@ fn test_match_and_trade_setprice_max() { "dht": "on", // Enable DHT without delay. "passphrase": format!("0x{}", hex::encode(bob_priv_key)), "coins": coins, - "rpc_password": "pass", + "rpc_password": DEFAULT_RPC_PASSWORD, "i_am_seed": true, + "use_trading_proto_v2": is_tpu, "is_bootstrap_node": true }), - "pass".to_string(), + DEFAULT_RPC_PASSWORD.to_string(), None, ) .unwrap(); @@ -931,10 +930,11 @@ fn test_match_and_trade_setprice_max() { "dht": "on", // Enable DHT without delay. "passphrase": format!("0x{}", hex::encode(alice_priv_key)), "coins": coins, - "rpc_password": "pass", + "rpc_password": DEFAULT_RPC_PASSWORD, "seednodes": vec![format!("{}", mm_bob.ip)], + "use_trading_proto_v2": is_tpu }), - "pass".to_string(), + DEFAULT_RPC_PASSWORD.to_string(), None, ) .unwrap(); @@ -1002,8 +1002,17 @@ fn test_match_and_trade_setprice_max() { } #[test] -// https://github.com/KomodoPlatform/atomicDEX-API/issues/888 -fn test_max_taker_vol_swap() { +// https://github.com/KomodoPlatform/atomicDEX-API/issues/471 +fn test_match_and_trade_setprice_max() { + test_match_and_trade_setprice_max_impl(false) +} + +#[test] +fn test_match_and_trade_setprice_max_v2() { + test_match_and_trade_setprice_max_impl(true) +} + +fn test_max_taker_vol_swap_impl(is_tpu: bool, expected_vol: MmNumber) { 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", 50.into()); let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); @@ -1016,7 +1025,8 @@ fn test_max_taker_vol_swap() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, - "is_bootstrap_node": true + "is_bootstrap_node": true, + "use_trading_proto_v2": is_tpu }), "pass".to_string(), None, @@ -1035,9 +1045,11 @@ fn test_max_taker_vol_swap() { "coins": coins, "rpc_password": "pass", "seednodes": vec![format!("{}", mm_bob.ip)], + "use_trading_proto_v2": is_tpu }), "pass".to_string(), None, + // Note: dex fee discount: &[("MYCOIN_FEE_DISCOUNT", "")], )) .unwrap(); @@ -1079,10 +1091,8 @@ fn test_max_taker_vol_swap() { }))) .unwrap(); assert!(rc.0.is_success(), "!max_taker_vol: {}", rc.1); - let vol: MaxTakerVolResponse = serde_json::from_str(&rc.1).unwrap(); - let expected_vol = MmNumber::from((1294999865579, 25930000000)); - - let actual_vol = MmNumber::from(vol.result.clone()); + let vol_res: MaxTakerVolResponse = serde_json::from_str(&rc.1).unwrap(); + let actual_vol = MmNumber::from(vol_res.result.clone()); log!("actual vol {}", actual_vol.to_decimal()); log!("expected vol {}", expected_vol.to_decimal()); @@ -1094,7 +1104,7 @@ fn test_max_taker_vol_swap() { "base": "MYCOIN1", "rel": "MYCOIN", "price": "16", - "volume": vol.result, + "volume": vol_res.result, }))) .unwrap(); assert!(rc.0.is_success(), "!sell: {}", rc.1); @@ -1117,12 +1127,28 @@ fn test_max_taker_vol_swap() { let status_response: Json = serde_json::from_str(&rc.1).unwrap(); let events_array = status_response["result"]["events"].as_array().unwrap(); - let first_event_type = events_array[0]["event"]["type"].as_str().unwrap(); - assert_eq!("Started", first_event_type); + if is_tpu { + let first_event_type = events_array[0]["event_type"].as_str().unwrap(); + assert_eq!("Initialized", first_event_type); + } else { + let first_event_type = events_array[0]["event"]["type"].as_str().unwrap(); + assert_eq!("Started", first_event_type); + } block_on(mm_bob.stop()).unwrap(); block_on(mm_alice.stop()).unwrap(); } +#[test] +// https://github.com/KomodoPlatform/atomicDEX-API/issues/888 +fn test_max_taker_vol_swap() { + test_max_taker_vol_swap_impl(false, MmNumber::from((1294999865579, 25930000000))); +} + +#[test] +fn test_max_taker_vol_swap_v2() { + test_max_taker_vol_swap_impl(true, MmNumber::from((129499954157, 2593000000))); +} + #[test] fn test_buy_when_coins_locked_by_other_swap() { let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); @@ -2074,8 +2100,8 @@ fn test_get_max_taker_vol_dex_fee_min_tx_amount() { assert!(rc.0.is_success(), "!max_taker_vol: {}", rc.1); let json: Json = serde_json::from_str(&rc.1).unwrap(); // the result of equation x + 0.00001 (dex fee) + 0.0000485 (miner fee 2740 + 2450) = 0.00532845 - assert_eq!(json["result"]["numer"], Json::from("105331")); - assert_eq!(json["result"]["denom"], Json::from("20000000")); + assert_eq!(json["result"]["numer"], Json::from("52597")); + assert_eq!(json["result"]["denom"], Json::from("10000000")); let rc = block_on(mm_alice.rpc(&json!({ "userpass": mm_alice.userpass, @@ -2150,8 +2176,8 @@ fn test_get_max_taker_vol_dust_threshold() { assert!(rc.0.is_success(), "!max_taker_vol: {}", rc.1); let json: Json = serde_json::from_str(&rc.1).unwrap(); // the result of equation x + 0.000728 (dex fee) + 0.00004220 + 0.00003930 (miner fees) = 0.0016041, x > dust - assert_eq!(json["result"]["numer"], Json::from("3973")); - assert_eq!(json["result"]["denom"], Json::from("5000000")); + assert_eq!(json["result"]["numer"], Json::from("79253")); + assert_eq!(json["result"]["denom"], Json::from("100000000")); block_on(mm.stop()).unwrap(); } @@ -2220,19 +2246,19 @@ fn test_get_max_taker_vol_with_kmd() { block_on(mm_alice.stop()).unwrap(); } -#[test] -fn test_get_max_maker_vol() { +fn test_get_max_maker_vol_impl(is_tpu: bool, expected_volume: MmNumber) { let (_ctx, _, priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 1.into()); let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let conf = Mm2TestConf::seednode(&format!("0x{}", hex::encode(priv_key)), &coins); + let conf = if is_tpu { + Mm2TestConf::seednode_trade_v2(&format!("0x{}", hex::encode(priv_key)), &coins) + } else { + Mm2TestConf::seednode(&format!("0x{}", hex::encode(priv_key)), &coins) + }; let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); let (_dump_log, _dump_dashboard) = mm_dump(&mm.log_path); log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); - - // 1 - tx_fee (274) - let expected_volume = MmNumber::from("0.99999726"); let expected = MaxMakerVolResponse { coin: "MYCOIN1".to_string(), volume: MmNumberMultiRepr::from(expected_volume.clone()), @@ -2246,6 +2272,16 @@ fn test_get_max_maker_vol() { assert_eq!(res.result.max_base_vol, expected_volume.to_decimal()); } +#[test] +fn test_get_max_maker_vol() { + test_get_max_maker_vol_impl(true, MmNumber::from("0.99999726")); // 1 - tx_fee (274) +} + +#[test] +fn test_get_max_maker_vol_v2() { + test_get_max_maker_vol_impl(true, MmNumber::from("0.99999726")); +} + #[test] fn test_get_max_maker_vol_error() { let priv_key = random_secp256k1_secret(); 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 a51c09d2dd..52dc079eca 100644 --- a/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs @@ -41,10 +41,10 @@ use mm2_test_helpers::for_tests::{ account_balance, active_swaps, check_recent_swaps, coins_needed_for_kickstart, disable_coin, enable_erc20_token_v2, enable_eth_coin_with_tokens_v2, erc20_dev_conf, eth_dev_conf, get_locked_amount, get_new_address, get_token_info, mm_dump, my_balance, my_swap_status, nft_dev_conf, start_swaps, task_enable_eth_with_tokens, - wait_for_swap_finished, MarketMakerIt, Mm2TestConf, SwapV2TestContracts, TestNode, ETH_SEPOLIA_CHAIN_ID, + wait_for_swap_finished, MarketMakerIt, Mm2TestConf, SwapV2TestContracts, TestNode, }; #[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] -use mm2_test_helpers::for_tests::{eth_sepolia_conf, sepolia_erc20_dev_conf}; +use mm2_test_helpers::for_tests::{eth_sepolia_conf, sepolia_erc20_dev_conf, ETH_SEPOLIA_CHAIN_ID}; use mm2_test_helpers::structs::{ Bip44Chain, EnableCoinBalanceMap, EthWithTokensActivationResult, HDAccountAddressId, TokenInfo, }; diff --git a/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs b/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs index 5ed24d98c6..70822d13ab 100644 --- a/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs @@ -1013,7 +1013,9 @@ fn test_get_max_taker_vol_and_trade_with_dynamic_trade_fee(coin: QtumCoin, priv_ // Please note if we pass the exact value, the `get_sender_trade_fee` will fail with 'Not sufficient balance: Couldn't collect enough value from utxos'. // So we should deduct trade fee from the output. let max_trade_fee = block_on(coin.get_sender_trade_fee( - TradePreimageValue::UpperBound(qtum_balance.clone()), + TradePreimageValue::UpperBound( + qtum_balance.clone() / (MmNumber::from(1) + DexFee::dex_fee_rate(coin.ticker(), "MYCOIN")).to_decimal(), + ), FeeApproxStage::TradePreimageMax, )) .expect("!get_sender_trade_fee"); 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 0ced4f84d4..11c3ad5a07 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 @@ -722,9 +722,9 @@ fn test_v2_swap_utxo_utxo_impl() { let locked_alice = block_on(get_locked_amount(&mm_alice, MYCOIN1)); assert_eq!(locked_alice.coin, MYCOIN1); let expected: MmNumberMultiRepr = if SET_BURN_PUBKEY_TO_ALICE.get() { - MmNumber::from("777.00000274").into() + MmNumber::from("777.00000770").into() } else { - MmNumber::from("778.00000274").into() + MmNumber::from("778.00000770").into() }; assert_eq!(locked_alice.locked_amount, expected); @@ -851,7 +851,7 @@ fn test_v2_swap_utxo_utxo_kickstart() { // coins must be virtually locked after kickstart until swap transactions are sent let locked_alice = block_on(get_locked_amount(&mm_alice, MYCOIN1)); assert_eq!(locked_alice.coin, MYCOIN1); - let expected: MmNumberMultiRepr = MmNumber::from("778.00000274").into(); + let expected: MmNumberMultiRepr = MmNumber::from("778.00000770").into(); assert_eq!(locked_alice.locked_amount, expected); let locked_bob = block_on(get_locked_amount(&mm_bob, MYCOIN)); diff --git a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs index 71c55fec0f..0bf4546f3d 100644 --- a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs +++ b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs @@ -15,12 +15,12 @@ use mm2_test_helpers::for_tests::wait_check_stats_swap_status; use mm2_test_helpers::for_tests::{ account_balance, btc_segwit_conf, btc_with_spv_conf, btc_with_sync_starting_header, check_recent_swaps, delete_wallet, enable_qrc20, enable_utxo_v2_electrum, eth_dev_conf, find_metrics_in_json, from_env_file, - get_new_address, get_shared_db_id, get_wallet_names, mm_spat, morty_conf, my_balance, rick_conf, sign_message, - start_swaps, tbtc_conf, tbtc_segwit_conf, tbtc_with_spv_conf, test_qrc20_history_impl, tqrc20_conf, verify_message, - wait_for_swaps_finish_and_check_status, wait_till_history_has_records, MarketMakerIt, Mm2InitPrivKeyPolicy, - Mm2TestConf, Mm2TestConfForSwap, RaiiDump, DOC_ELECTRUM_ADDRS, ETH_MAINNET_NODES, ETH_MAINNET_SWAP_CONTRACT, - ETH_SEPOLIA_NODES, ETH_SEPOLIA_SWAP_CONTRACT, MARTY_ELECTRUM_ADDRS, MORTY, QRC20_ELECTRUMS, RICK, - RICK_ELECTRUM_ADDRS, TBTC_ELECTRUMS, T_BCH_ELECTRUMS, + get_new_address, get_shared_db_id, get_wallet_names, mm_dump, mm_spat, morty_conf, my_balance, rick_conf, + sign_message, start_swaps, tbtc_conf, tbtc_segwit_conf, tbtc_with_spv_conf, test_qrc20_history_impl, tqrc20_conf, + verify_message, wait_for_swaps_finish_and_check_status, wait_till_history_has_records, MarketMakerIt, + Mm2InitPrivKeyPolicy, Mm2TestConf, Mm2TestConfForSwap, RaiiDump, DOC_ELECTRUM_ADDRS, ETH_MAINNET_NODES, + ETH_MAINNET_SWAP_CONTRACT, ETH_SEPOLIA_NODES, ETH_SEPOLIA_SWAP_CONTRACT, MARTY_ELECTRUM_ADDRS, MORTY, + QRC20_ELECTRUMS, RICK, RICK_ELECTRUM_ADDRS, TBTC_ELECTRUMS, T_BCH_ELECTRUMS, }; use mm2_test_helpers::get_passphrase; use mm2_test_helpers::structs::*; @@ -2777,6 +2777,8 @@ fn test_convert_eth_address() { assert!(rc.1.contains("Address must be prefixed with 0x")); } +/// TODO: Fails on electrum "blockchain.contract.event.get_history" call +#[ignore = "electrum call failure"] #[test] #[cfg(not(target_arch = "wasm32"))] fn test_add_delegation_qtum() { @@ -2814,6 +2816,7 @@ fn test_add_delegation_qtum() { None, ) .unwrap(); + let (_mm_dump_log, _mm_dump_dashboard) = mm_dump(&mm.log_path); let json = block_on(enable_electrum( &mm, @@ -2927,6 +2930,8 @@ fn test_remove_delegation_qtum() { ); } +/// TODO: Fails on electrum "blockchain.contract.event.get_history" call +#[ignore = "electrum call failure"] #[test] #[cfg(not(target_arch = "wasm32"))] fn test_query_delegations_info_qtum() { @@ -2964,6 +2969,7 @@ fn test_query_delegations_info_qtum() { None, ) .unwrap(); + let (_mm_dump_log, _mm_dump_dashboard) = mm_dump(&mm.log_path); let json = block_on(enable_electrum( &mm, @@ -3976,6 +3982,8 @@ fn test_tx_history_tbtc_non_segwit() { } } +/// NOTE: if this test fails this may be due to activity in the DOC chain with this address +/// TODO: switch to UTXO docker container #[test] #[cfg(not(target_arch = "wasm32"))] fn test_update_maker_order() { diff --git a/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs b/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs index 86bbfbca53..1a106571b7 100644 --- a/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs @@ -1321,6 +1321,8 @@ fn setprice_min_volume_should_be_displayed_in_orderbook() { assert_eq!(min_volume, "1", "Alice MORTY/RICK ask must display correct min_volume"); } +/// Test does not work as Alice receives order with PubkeyKeepAlive message (not with MakerOrderCreated) +#[ignore = "not working due to protocol change"] #[test] fn test_order_cancellation_received_before_creation() { let coins = json!([rick_conf(), morty_conf()]); diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index 0b69ad9b55..54390430c2 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -1845,7 +1845,7 @@ where } } -#[cfg(not(target_arch = "wasm32"))] +#[cfg_attr(target_arch = "wasm32", expect(dead_code))] #[derive(Serialize, Deserialize, Debug)] struct ToWaitForLogRe { ctx: u32,