diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index dfdac1304a..20f3062dc2 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -5506,25 +5506,22 @@ impl MmCoin for EthCoin { fn history_sync_status(&self) -> HistorySyncState { self.history_sync_state.lock().unwrap().clone() } fn get_trade_fee(&self) -> Box + Send> { - let coin = self.clone(); + let selfi = self.clone(); Box::new( async move { - let pay_for_gas_option = coin - .get_swap_pay_for_gas_option(coin.get_swap_transaction_fee_policy()) - .await - .map_err(|e| e.to_string())?; - - let fee = calc_total_fee(U256::from(gas_limit::ETH_MAX_TRADE_GAS), &pay_for_gas_option) - .map_err(|e| e.to_string())?; - let fee_coin = match &coin.coin_type { - EthCoinType::Eth => &coin.ticker, + let gas_price = try_s!(selfi.get_gas_price().await); + let fee = gas_price * U256::from(gas_limit::ETH_MAX_TRADE_GAS); + let fee_coin = match &selfi.coin_type { + EthCoinType::Eth => &selfi.ticker, EthCoinType::Erc20 { platform, .. } => platform, EthCoinType::Nft { .. } => return ERR!("Nft Protocol is not supported yet!"), }; + Ok(TradeFee { coin: fee_coin.into(), amount: try_s!(u256_to_big_decimal(fee, ETH_DECIMALS)).into(), paid_from_trading_vol: false, + tx_size: 0, }) } .boxed() @@ -5592,6 +5589,7 @@ impl MmCoin for EthCoin { coin: fee_coin.into(), amount: amount.into(), paid_from_trading_vol: false, + tx_size: 0, }) } @@ -5618,6 +5616,7 @@ impl MmCoin for EthCoin { coin: fee_coin.into(), amount: amount.into(), paid_from_trading_vol: false, + tx_size: 0, }) }; Box::new(fut.boxed().compat()) @@ -5667,6 +5666,7 @@ impl MmCoin for EthCoin { coin: fee_coin.into(), amount: amount.into(), paid_from_trading_vol: false, + tx_size: 0, }) } diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index 4d8b97c493..0b0a12516b 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -315,6 +315,7 @@ fn get_sender_trade_preimage() { coin: "ETH".to_owned(), amount: amount.into(), paid_from_trading_vol: false, + tx_size: 0, } } @@ -382,6 +383,7 @@ fn get_erc20_sender_trade_preimage() { coin: "ETH".to_owned(), amount: amount.into(), paid_from_trading_vol: false, + tx_size: 0, } } @@ -474,6 +476,7 @@ fn get_receiver_trade_preimage() { coin: "ETH".to_owned(), amount: amount.into(), paid_from_trading_vol: false, + tx_size: 0, }; let actual = coin @@ -498,6 +501,7 @@ fn test_get_fee_to_send_taker_fee() { coin: "ETH".to_owned(), amount: amount.into(), paid_from_trading_vol: false, + tx_size: 0, }; let dex_fee_amount = u256_to_big_decimal(DEX_FEE_AMOUNT.into(), 18).expect("!u256_to_big_decimal"); diff --git a/mm2src/coins/lightning.rs b/mm2src/coins/lightning.rs index 2feb7ef014..7b5c20eba0 100644 --- a/mm2src/coins/lightning.rs +++ b/mm2src/coins/lightning.rs @@ -1342,6 +1342,7 @@ impl MmCoin for LightningCoin { coin: self.ticker().to_owned(), amount: Default::default(), paid_from_trading_vol: false, + tx_size: 0, }) } @@ -1351,6 +1352,7 @@ impl MmCoin for LightningCoin { coin: self.ticker().to_owned(), amount: Default::default(), paid_from_trading_vol: false, + tx_size: 0, })) } @@ -1364,6 +1366,7 @@ impl MmCoin for LightningCoin { coin: self.ticker().to_owned(), amount: Default::default(), paid_from_trading_vol: false, + tx_size: 0, }) } diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 463028a635..f3d20615d4 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -1936,6 +1936,17 @@ pub trait MarketCoinOps { fn is_trezor(&self) -> bool; } +/// Priority levels for UTXO fee estimation for withdrawal. +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub enum UtxoFeePriority { + /// Low priority. + Low, + /// Normal priority. + Normal, + /// High priority. + High, +} + #[derive(Clone, Debug, Deserialize, PartialEq)] #[serde(tag = "type")] pub enum EthGasLimitOption { @@ -1948,12 +1959,19 @@ pub enum EthGasLimitOption { #[derive(Clone, Debug, Deserialize, PartialEq)] #[serde(tag = "type")] pub enum WithdrawFee { + /// encapsulates the fixed fee amount for a withdrawal transaction, regardless of transaction size. UtxoFixed { amount: BigDecimal, }, + /// encapsulates the fee amount for a withdrawal transaction calculated based on the transaction size in kilobytes. UtxoPerKbyte { amount: BigDecimal, }, + /// encapsulates the priority of a withdrawal transaction, indicating the desired fee + /// level for transaction processing. + UtxoPriority { + priority: UtxoFeePriority, + }, EthGas { /// in gwei gas_price: BigDecimal, @@ -2320,6 +2338,7 @@ pub struct TradeFee { pub coin: String, pub amount: MmNumber, pub paid_from_trading_vol: bool, + pub tx_size: u64, } /// A type alias for a HashMap where the key is a String representing the coin/token ticker, @@ -4843,7 +4862,7 @@ pub async fn my_tx_history(ctx: MmArc, req: Json) -> Result>, S } /// `get_trade_fee` rpc implementation. -/// There is some consideration about this rpc: +/// There is some consideration about this rpc: /// for eth coin this rpc returns max possible trade fee (estimated for maximum possible gas limit for any kind of swap). /// However for eth coin, as part of fixing this issue https://github.com/KomodoPlatform/komodo-defi-framework/issues/1848, /// `max_taker_vol' and `trade_preimage` rpc now return more accurate required gas calculations. diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index 3ee9e7761b..1ff1b5e54b 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -10,12 +10,13 @@ use crate::utxo::tx_cache::{UtxoVerboseCacheOps, UtxoVerboseCacheShared}; use crate::utxo::utxo_builder::{UtxoCoinBuildError, UtxoCoinBuildResult, UtxoCoinBuilder, UtxoCoinBuilderCommonOps, UtxoFieldsWithGlobalHDBuilder, UtxoFieldsWithHardwareWalletBuilder, UtxoFieldsWithIguanaSecretBuilder}; -use crate::utxo::utxo_common::{self, big_decimal_from_sat, check_all_utxo_inputs_signed_by_pub, UtxoTxBuilder}; -use crate::utxo::{qtum, ActualTxFee, AdditionalTxData, AddrFromStrError, BroadcastTxErr, FeePolicy, GenerateTxError, - GetUtxoListOps, HistoryUtxoTx, HistoryUtxoTxMap, MatureUnspentList, RecentlySpentOutPointsGuard, - UnsupportedAddr, UtxoActivationParams, UtxoAddressFormat, UtxoCoinFields, UtxoCommonOps, - UtxoFromLegacyReqErr, UtxoTx, UtxoTxBroadcastOps, UtxoTxGenerationOps, VerboseTransactionFrom, - UTXO_LOCK}; +use crate::utxo::utxo_common::{self, big_decimal_from_sat, check_all_utxo_inputs_signed_by_pub, + PreImageTradeFeeResult, UtxoTxBuilder}; +use crate::utxo::{qtum, AdditionalTxData, AddrFromStrError, BroadcastTxErr, FeePolicy, GenerateTxError, + GetUtxoListOps, HistoryUtxoTx, HistoryUtxoTxMap, HtlcSpendFeeResult, MatureUnspentList, + RecentlySpentOutPointsGuard, UnsupportedAddr, UtxoActivationParams, UtxoAddressFormat, + UtxoCoinFields, UtxoCommonOps, UtxoFromLegacyReqErr, UtxoTx, UtxoTxBroadcastOps, + UtxoTxGenerationOps, VerboseTransactionFrom, UTXO_LOCK}; use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, ConfirmPaymentInput, DexFee, Eip1559Ops, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, IguanaPrivKey, MakerSwapTakerCoin, MarketCoinOps, MmCoin, MmCoinEnum, NegotiateSwapContractAddrErr, PaymentInstructionArgs, @@ -492,9 +493,7 @@ impl Qrc20Coin { /// `gas_fee` should be calculated by: gas_limit * gas_price * (count of contract calls), /// or should be sum of gas fee of all contract calls. pub async fn get_qrc20_tx_fee(&self, gas_fee: u64) -> Result { - match try_s!(self.get_tx_fee().await) { - ActualTxFee::Dynamic(amount) | ActualTxFee::FixedPerKb(amount) => Ok(amount + gas_fee), - } + Ok(try_s!(self.get_tx_fee_per_kb().await) + gas_fee) } /// Generate and send a transaction with the specified UTXO outputs. @@ -582,7 +581,7 @@ impl Qrc20Coin { &self, contract_outputs: Vec, stage: &FeeApproxStage, - ) -> TradePreimageResult { + ) -> TradePreimageResult { let decimals = self.as_ref().decimals; let mut gas_fee = 0; let mut outputs = Vec::with_capacity(contract_outputs.len()); @@ -595,7 +594,10 @@ impl Qrc20Coin { UtxoCommonOps::preimage_trade_fee_required_to_send_outputs(self, outputs, fee_policy, Some(gas_fee), stage) .await?; let gas_fee = big_decimal_from_sat(gas_fee as i64, decimals); - Ok(miner_fee + gas_fee) + Ok(PreImageTradeFeeResult { + tx_size: miner_fee.tx_size, + fee: miner_fee.fee + gas_fee, + }) } } @@ -612,7 +614,7 @@ impl UtxoTxBroadcastOps for Qrc20Coin { #[cfg_attr(test, mockable)] impl UtxoTxGenerationOps for Qrc20Coin { /// Get only QTUM transaction fee. - async fn get_tx_fee(&self) -> UtxoRpcResult { utxo_common::get_tx_fee(&self.utxo).await } + async fn get_tx_fee_per_kb(&self) -> UtxoRpcResult { utxo_common::get_tx_fee_per_kb(&self.utxo).await } async fn calc_interest_if_required( &self, @@ -653,7 +655,7 @@ impl GetUtxoListOps for Qrc20Coin { #[async_trait] #[cfg_attr(test, mockable)] impl UtxoCommonOps for Qrc20Coin { - async fn get_htlc_spend_fee(&self, tx_size: u64, stage: &FeeApproxStage) -> UtxoRpcResult { + async fn get_htlc_spend_fee(&self, tx_size: u64, stage: &FeeApproxStage) -> UtxoRpcResult { utxo_common::get_htlc_spend_fee(self, tx_size, stage).await } @@ -718,7 +720,7 @@ impl UtxoCommonOps for Qrc20Coin { fee_policy: FeePolicy, gas_fee: Option, stage: &FeeApproxStage, - ) -> TradePreimageResult { + ) -> TradePreimageResult { utxo_common::preimage_trade_fee_required_to_send_outputs( self, self.platform_ticker(), @@ -1352,6 +1354,7 @@ impl MmCoin for Qrc20Coin { coin: selfi.platform.clone(), amount: big_decimal_from_sat(fee as i64, selfi.utxo.decimals).into(), paid_from_trading_vol: false, + tx_size: 0, }) }; Box::new(fut.boxed().compat()) @@ -1400,15 +1403,19 @@ impl MmCoin for Qrc20Coin { self.preimage_trade_fee_required_to_send_outputs(vec![sender_refund_output], &stage) .await? } else { - BigDecimal::from(0) // No refund fee if not included. + // No refund fee if not included. + PreImageTradeFeeResult { + fee: 0.into(), + tx_size: 0, + } }; - let total_fee = erc20_payment_fee + sender_refund_fee; - + let qrc20_payment_fee = erc20_payment_fee.fee + sender_refund_fee.fee; Ok(TradeFee { coin: self.platform.clone(), - amount: total_fee.into(), + amount: qrc20_payment_fee.into(), paid_from_trading_vol: false, + tx_size: sender_refund_fee.tx_size, }) } @@ -1429,10 +1436,12 @@ impl MmCoin for Qrc20Coin { let total_fee = selfi .preimage_trade_fee_required_to_send_outputs(vec![output], &stage) .await?; + Ok(TradeFee { coin: selfi.platform.clone(), - amount: total_fee.into(), + amount: total_fee.fee.into(), paid_from_trading_vol: false, + tx_size: total_fee.tx_size, }) }; Box::new(fut.boxed().compat()) @@ -1456,8 +1465,9 @@ impl MmCoin for Qrc20Coin { Ok(TradeFee { coin: self.platform.clone(), - amount: total_fee.into(), + amount: total_fee.fee.into(), paid_from_trading_vol: false, + tx_size: total_fee.tx_size, }) } diff --git a/mm2src/coins/qrc20/qrc20_tests.rs b/mm2src/coins/qrc20/qrc20_tests.rs index 2caf87c3bf..49066f973c 100644 --- a/mm2src/coins/qrc20/qrc20_tests.rs +++ b/mm2src/coins/qrc20/qrc20_tests.rs @@ -1,5 +1,6 @@ use super::*; -use crate::{DexFee, TxFeeDetails, WaitForHTLCTxSpendArgs}; +use crate::utxo::KILO_BYTE; +use crate::{calculate_fee, DexFee, TxFeeDetails, WaitForHTLCTxSpendArgs}; use common::{block_on, wait_until_sec, DEX_FEE_ADDR_RAW_PUBKEY}; use crypto::Secp256k1Secret; use itertools::Itertools; @@ -23,6 +24,8 @@ const CONTRACT_CALL_GAS_FEE: i64 = (QRC20_GAS_LIMIT_DEFAULT * QRC20_GAS_PRICE_DE const SWAP_PAYMENT_GAS_FEE: i64 = (QRC20_PAYMENT_GAS_LIMIT * QRC20_GAS_PRICE_DEFAULT) as i64; const TAKER_PAYMENT_SPEND_SEARCH_INTERVAL: f64 = 1.; +fn get_expected_trade_fee_per_kb(tx_size: u64) -> i64 { calculate_fee!(tx_size, EXPECTED_TX_FEE as u64) as i64 } + pub fn qrc20_coin_for_test(priv_key: [u8; 32], fallback_swap: Option<&str>) -> (MmArc, Qrc20Coin) { let conf = json!({ "coin":"QRC20", @@ -58,8 +61,8 @@ pub fn qrc20_coin_for_test(priv_key: [u8; 32], fallback_swap: Option<&str>) -> ( (ctx, coin) } -fn check_tx_fee(coin: &Qrc20Coin, expected_tx_fee: ActualTxFee) { - let actual_tx_fee = block_on(coin.get_tx_fee()).unwrap(); +fn check_tx_fee(coin: &Qrc20Coin, expected_tx_fee: u64) { + let actual_tx_fee = block_on(coin.get_tx_fee_per_kb()).unwrap(); assert_eq!(actual_tx_fee, expected_tx_fee); } @@ -142,12 +145,29 @@ fn test_withdraw_impl_fee_details() { }; let tx_details = coin.withdraw(withdraw_req).wait().unwrap(); + let tx_size_kb = tx_details.tx.tx_hex().cloned().unwrap().len() as f64 / KILO_BYTE; + // In transaction size calculations we assume the script sig size is (2 + MAX_DER_SIGNATURE_LEN + COMPRESSED_PUBKEY_LEN) or 107 bytes + // when in reality signatures can vary by 1 or 2 bytes because of possible zero padding of r and s values of the signature. + // This is why we test for a range of values here instead of a single value. The value we use in fees calculation is the + // highest possible value of 107 to ensure we don't underestimate the fee. + assert!( + (0.297..=0.299).contains(&tx_size_kb), + "Tx size in KB {} is not within the range [{}, {}]", + tx_size_kb, + 0.297, + 0.299 + ); + let tx_size_kb = 0.299; + let fee_per_kb = block_on(coin.get_tx_fee_per_kb()).unwrap() as f64; + let expected_miner_fee_sats = fee_per_kb * tx_size_kb; + let expected_miner_fee = big_decimal_from_sat(expected_miner_fee_sats as i64, coin.utxo.decimals); + let expected: Qrc20FeeDetails = json::from_value(json!({ "coin": "QTUM", // 1000 from satoshi, // where decimals = 8, // 1000 is fixed fee - "miner_fee": "0.00001", + "miner_fee": expected_miner_fee, "gas_limit": 2_500_000, "gas_price": 40, // (gas_limit * gas_price) from satoshi in Qtum @@ -729,7 +749,7 @@ fn test_get_trade_fee() { ]; let (_ctx, coin) = qrc20_coin_for_test(priv_key, None); // check if the coin's tx fee is expected - check_tx_fee(&coin, ActualTxFee::FixedPerKb(EXPECTED_TX_FEE as u64)); + check_tx_fee(&coin, EXPECTED_TX_FEE as u64); let actual_trade_fee = coin.get_trade_fee().wait().unwrap(); let expected_trade_fee_amount = big_decimal_from_sat( @@ -740,6 +760,7 @@ fn test_get_trade_fee() { coin: "QTUM".into(), amount: expected_trade_fee_amount.into(), paid_from_trading_vol: false, + tx_size: 0, }; assert_eq!(actual_trade_fee, expected); } @@ -756,25 +777,30 @@ fn test_sender_trade_preimage_zero_allowance() { ]; let (_ctx, coin) = qrc20_coin_for_test(priv_key, None); // check if the coin's tx fee is expected - check_tx_fee(&coin, ActualTxFee::FixedPerKb(EXPECTED_TX_FEE as u64)); + check_tx_fee(&coin, EXPECTED_TX_FEE as u64); let allowance = block_on(coin.allowance(coin.swap_contract_address)).expect("!allowance"); assert_eq!(allowance, 0.into()); + let erc20_payment_fee_tx_size = 535; + let erc20_payment_fee = get_expected_trade_fee_per_kb(erc20_payment_fee_tx_size); let erc20_payment_fee_with_one_approve = big_decimal_from_sat( - CONTRACT_CALL_GAS_FEE + SWAP_PAYMENT_GAS_FEE + EXPECTED_TX_FEE, + CONTRACT_CALL_GAS_FEE + SWAP_PAYMENT_GAS_FEE + erc20_payment_fee, coin.utxo.decimals, ); - let sender_refund_fee = big_decimal_from_sat(CONTRACT_CALL_GAS_FEE + EXPECTED_TX_FEE, coin.utxo.decimals); let actual = block_on(coin.get_sender_trade_fee(TradePreimageValue::Exact(1.into()), FeeApproxStage::WithoutApprox, true)) .expect("!get_sender_trade_fee"); + let actual_tx_fee = get_expected_trade_fee_per_kb(actual.tx_size); + let sender_refund_fee = big_decimal_from_sat(actual_tx_fee + CONTRACT_CALL_GAS_FEE, coin.utxo.decimals); + // one `approve` contract call should be included into the expected trade fee let expected = TradeFee { coin: "QTUM".to_owned(), amount: (erc20_payment_fee_with_one_approve + sender_refund_fee).into(), paid_from_trading_vol: false, + tx_size: actual.tx_size, }; assert_eq!(actual, expected); } @@ -792,18 +818,22 @@ fn test_sender_trade_preimage_with_allowance() { ]; let (_ctx, coin) = qrc20_coin_for_test(priv_key, None); // check if the coin's tx fee is expected - check_tx_fee(&coin, ActualTxFee::FixedPerKb(EXPECTED_TX_FEE as u64)); + check_tx_fee(&coin, EXPECTED_TX_FEE as u64); let allowance = block_on(coin.allowance(coin.swap_contract_address)).expect("!allowance"); assert_eq!(allowance, 300_000_000.into()); + let erc20_payment_fee_without_approve_tx_size = 576; + let erc20_payment_fee = get_expected_trade_fee_per_kb(erc20_payment_fee_without_approve_tx_size); let erc20_payment_fee_without_approve = - big_decimal_from_sat(SWAP_PAYMENT_GAS_FEE + EXPECTED_TX_FEE, coin.utxo.decimals); + big_decimal_from_sat(SWAP_PAYMENT_GAS_FEE + erc20_payment_fee, coin.utxo.decimals); + + let erc20_payment_fee_tx_size = 790; + let erc20_payment_fee = get_expected_trade_fee_per_kb(erc20_payment_fee_tx_size); let erc20_payment_fee_with_two_approves = big_decimal_from_sat( - 2 * CONTRACT_CALL_GAS_FEE + SWAP_PAYMENT_GAS_FEE + EXPECTED_TX_FEE, + 2 * CONTRACT_CALL_GAS_FEE + SWAP_PAYMENT_GAS_FEE + erc20_payment_fee, coin.utxo.decimals, ); - let sender_refund_fee = big_decimal_from_sat(CONTRACT_CALL_GAS_FEE + EXPECTED_TX_FEE, coin.utxo.decimals); let actual = block_on(coin.get_sender_trade_fee( TradePreimageValue::Exact(BigDecimal::try_from(2.5).unwrap()), @@ -811,11 +841,15 @@ fn test_sender_trade_preimage_with_allowance() { true, )) .expect("!get_sender_trade_fee"); + let expected_fee = get_expected_trade_fee_per_kb(actual.tx_size); + let sender_refund_fee = big_decimal_from_sat(CONTRACT_CALL_GAS_FEE + expected_fee, coin.utxo.decimals); + // the expected fee should not include any `approve` contract call let expected = TradeFee { coin: "QTUM".to_owned(), - amount: (erc20_payment_fee_without_approve + sender_refund_fee.clone()).into(), + amount: (erc20_payment_fee_without_approve + sender_refund_fee).into(), paid_from_trading_vol: false, + tx_size: actual.tx_size, }; assert_eq!(actual, expected); @@ -825,11 +859,14 @@ fn test_sender_trade_preimage_with_allowance() { true, )) .expect("!get_sender_trade_fee"); + let expected_fee = get_expected_trade_fee_per_kb(actual.tx_size); + let sender_refund_fee = big_decimal_from_sat(CONTRACT_CALL_GAS_FEE + expected_fee, coin.utxo.decimals); // two `approve` contract calls should be included into the expected trade fee let expected = TradeFee { coin: "QTUM".to_owned(), amount: (erc20_payment_fee_with_two_approves + sender_refund_fee).into(), paid_from_trading_vol: false, + tx_size: actual.tx_size, }; assert_eq!(actual, expected); } @@ -903,18 +940,21 @@ fn test_receiver_trade_preimage() { ]; let (_ctx, coin) = qrc20_coin_for_test(priv_key, None); // check if the coin's tx fee is expected - check_tx_fee(&coin, ActualTxFee::FixedPerKb(EXPECTED_TX_FEE as u64)); + check_tx_fee(&coin, EXPECTED_TX_FEE as u64); let actual = coin .get_receiver_trade_fee(FeeApproxStage::WithoutApprox) .wait() .expect("!get_receiver_trade_fee"); // only one contract call should be included into the expected trade fee - let expected_receiver_fee = big_decimal_from_sat(CONTRACT_CALL_GAS_FEE + EXPECTED_TX_FEE, coin.utxo.decimals); + + let actual_tx_fee = get_expected_trade_fee_per_kb(actual.tx_size); + let expected_receiver_fee = big_decimal_from_sat(CONTRACT_CALL_GAS_FEE + actual_tx_fee, coin.utxo.decimals); let expected = TradeFee { coin: "QTUM".to_owned(), amount: expected_receiver_fee.into(), paid_from_trading_vol: false, + tx_size: actual.tx_size, }; assert_eq!(actual, expected); } @@ -930,7 +970,7 @@ fn test_taker_fee_tx_fee() { ]; let (_ctx, coin) = qrc20_coin_for_test(priv_key, None); // check if the coin's tx fee is expected - check_tx_fee(&coin, ActualTxFee::FixedPerKb(EXPECTED_TX_FEE as u64)); + check_tx_fee(&coin, EXPECTED_TX_FEE as u64); let expected_balance = CoinBalance { spendable: BigDecimal::from(5u32), unspendable: BigDecimal::from(0u32), @@ -944,11 +984,13 @@ fn test_taker_fee_tx_fee() { )) .expect("!get_fee_to_send_taker_fee"); // only one contract call should be included into the expected trade fee - let expected_receiver_fee = big_decimal_from_sat(CONTRACT_CALL_GAS_FEE + EXPECTED_TX_FEE, coin.utxo.decimals); + let actual_tx_fee = get_expected_trade_fee_per_kb(actual.tx_size); + let expected_receiver_fee = big_decimal_from_sat(CONTRACT_CALL_GAS_FEE + actual_tx_fee, coin.utxo.decimals); let expected = TradeFee { coin: "QTUM".to_owned(), amount: expected_receiver_fee.into(), paid_from_trading_vol: false, + tx_size: actual.tx_size, }; assert_eq!(actual, expected); } diff --git a/mm2src/coins/rpc_command/lightning/open_channel.rs b/mm2src/coins/rpc_command/lightning/open_channel.rs index fdb5b9caa9..b55307f69e 100644 --- a/mm2src/coins/rpc_command/lightning/open_channel.rs +++ b/mm2src/coins/rpc_command/lightning/open_channel.rs @@ -4,7 +4,7 @@ use crate::lightning::ln_p2p::{connect_to_ln_node, ConnectionError}; use crate::lightning::ln_serialization::NodeAddress; use crate::lightning::ln_storage::LightningStorage; use crate::utxo::utxo_common::UtxoTxBuilder; -use crate::utxo::{sat_from_big_decimal, FeePolicy, GetUtxoListOps, UtxoTxGenerationOps}; +use crate::utxo::{sat_from_big_decimal, FeePolicy, GetUtxoListOps, TxFeeType, UtxoTxGenerationOps}; use crate::{lp_coinfind_or_err, BalanceError, CoinFindError, GenerateTxError, MmCoinEnum, NumConversError, UnexpectedDerivationMethod, UtxoRpcError}; use chain::TransactionOutput; @@ -174,10 +174,10 @@ pub async fn open_channel(ctx: MmArc, req: OpenChannelRequest) -> OpenChannelRes .with_fee_policy(fee_policy); let fee = platform_coin - .get_tx_fee() + .get_tx_fee_per_kb() .await .map_err(|e| OpenChannelError::RpcError(e.to_string()))?; - tx_builder = tx_builder.with_fee(fee); + tx_builder = tx_builder.with_fee(TxFeeType::PerKb(fee)); let (unsigned, _) = tx_builder.build().await?; diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 15329e81b6..ea75fa197d 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -1697,6 +1697,7 @@ impl TendermintCoin { coin: ticker, amount: fee_amount.into(), paid_from_trading_vol: false, + tx_size: 0, }) } @@ -1740,6 +1741,7 @@ impl TendermintCoin { coin: ticker, amount: fee_amount.into(), paid_from_trading_vol: false, + tx_size: 0, }) } diff --git a/mm2src/coins/utxo.rs b/mm2src/coins/utxo.rs index ebd7f72f29..db3f387c60 100644 --- a/mm2src/coins/utxo.rs +++ b/mm2src/coins/utxo.rs @@ -113,6 +113,7 @@ use crate::coin_balance::{EnableCoinScanPolicy, EnabledCoinBalanceParams, HDAddr use crate::hd_wallet::{HDAccountOps, HDAddressOps, HDPathAccountToAddressId, HDWalletCoinOps, HDWalletOps, HDWalletStorageError}; use crate::utxo::tx_cache::UtxoVerboseCacheShared; +use crate::utxo::utxo_common::PreImageTradeFeeResult; use crate::{ParseCoinAssocTypes, ToBytes}; pub mod tx_cache; @@ -122,11 +123,11 @@ pub mod utxo_common_tests; #[cfg(test)] pub mod utxo_tests; #[cfg(target_arch = "wasm32")] pub mod utxo_wasm_tests; -const KILO_BYTE: u64 = 1000; +pub const KILO_BYTE: f64 = 1000.; /// https://bitcoin.stackexchange.com/a/77192 const MAX_DER_SIGNATURE_LEN: usize = 72; const COMPRESSED_PUBKEY_LEN: usize = 33; -const P2PKH_OUTPUT_LEN: u64 = 34; +pub const P2PKH_OUTPUT_LEN: u64 = 34; const MATURE_CONFIRMATIONS_DEFAULT: u32 = 100; const UTXO_DUST_AMOUNT: u64 = 1000; /// Block count for KMD median time past calculation @@ -274,6 +275,7 @@ pub struct AdditionalTxData { pub fee_amount: u64, pub unused_change: u64, pub kmd_rewards: Option, + pub tx_size: u64, } /// The fee set from coins config @@ -285,14 +287,16 @@ pub enum TxFee { FixedPerKb(u64), } -/// The actual "runtime" fee that is received from RPC in case of dynamic calculation +impl TxFee { + pub fn is_dynamic(&self) -> bool { matches!(self, TxFee::Dynamic(_)) } +} + #[derive(Copy, Clone, Debug, PartialEq)] -pub enum ActualTxFee { - /// fee amount per Kbyte received from coin RPC - Dynamic(u64), - /// Use specified amount per each 1 kb of transaction and also per each output less than amount. - /// Used by DOGE, but more coins might support it too. - FixedPerKb(u64), +pub enum TxFeeType { + /// Fee per kb whether it is dynamic (received from RPC) or fixed + PerKb(u64), + /// Use specified fixed amount for the whole transaction that is not dependent on transaction size + Fixed(u64), } /// Fee policy applied on transaction creation @@ -500,6 +504,17 @@ impl UtxoSyncStatusLoopHandle { } } +/// Priority levels for UTXO fee estimation for withdrawal. +#[derive(Clone, Debug, Deserialize)] +pub struct UtxoFeePriorities { + /// Low priority. Recommended (8-10 blocks atleast for btc) + pub low: u8, + /// Normal priority. Recommended (6 blocks atleast for btc) + pub normal: u8, + /// High priority. Recommended (2 blocks atleast for btc) + pub high: u8, +} + #[derive(Debug)] pub struct UtxoCoinConf { pub ticker: String, @@ -578,6 +593,8 @@ pub struct UtxoCoinConf { pub derivation_path: Option, /// The average time in seconds needed to mine a new block for this coin. pub avg_blocktime: Option, + /// The average time in seconds needed to mine a new block for this coin. + pub fee_priorities: Option, } pub struct UtxoCoinFields { @@ -851,7 +868,7 @@ pub trait UtxoTxBroadcastOps { #[async_trait] #[cfg_attr(test, mockable)] pub trait UtxoTxGenerationOps { - async fn get_tx_fee(&self) -> UtxoRpcResult; + async fn get_tx_fee_per_kb(&self) -> UtxoRpcResult; /// Calculates interest if the coin is KMD /// Adds the value to existing output to my_script_pub or creates additional interest output @@ -957,12 +974,22 @@ impl MatureUnspentList { } } +#[derive(Debug)] +pub struct HtlcSpendFeeResult { + pub fee: u64, + pub tx_size: u64, +} + +impl HtlcSpendFeeResult { + fn from(fee: u64, tx_size: u64) -> Self { Self { fee, tx_size } } +} + #[async_trait] #[cfg_attr(test, mockable)] pub trait UtxoCommonOps: AsRef + UtxoTxGenerationOps + UtxoTxBroadcastOps + Clone + Send + Sync + 'static { - async fn get_htlc_spend_fee(&self, tx_size: u64, stage: &FeeApproxStage) -> UtxoRpcResult; + async fn get_htlc_spend_fee(&self, tx_size: u64, stage: &FeeApproxStage) -> UtxoRpcResult; fn addresses_from_script(&self, script: &Script) -> Result, String>; @@ -1016,7 +1043,7 @@ pub trait UtxoCommonOps: fee_policy: FeePolicy, gas_fee: Option, stage: &FeeApproxStage, - ) -> TradePreimageResult; + ) -> TradePreimageResult; /// Increase the given `dynamic_fee` according to the fee approximation `stage`. /// The method is used to predict a possible increase in dynamic fee. diff --git a/mm2src/coins/utxo/bch.rs b/mm2src/coins/utxo/bch.rs index 2b94135933..1957b8a420 100644 --- a/mm2src/coins/utxo/bch.rs +++ b/mm2src/coins/utxo/bch.rs @@ -697,7 +697,7 @@ impl UtxoTxBroadcastOps for BchCoin { #[async_trait] #[cfg_attr(test, mockable)] impl UtxoTxGenerationOps for BchCoin { - async fn get_tx_fee(&self) -> UtxoRpcResult { utxo_common::get_tx_fee(&self.utxo_arc).await } + async fn get_tx_fee_per_kb(&self) -> UtxoRpcResult { utxo_common::get_tx_fee_per_kb(&self.utxo_arc).await } async fn calc_interest_if_required( &self, @@ -766,7 +766,7 @@ impl GetUtxoMapOps for BchCoin { #[async_trait] #[cfg_attr(test, mockable)] impl UtxoCommonOps for BchCoin { - async fn get_htlc_spend_fee(&self, tx_size: u64, stage: &FeeApproxStage) -> UtxoRpcResult { + async fn get_htlc_spend_fee(&self, tx_size: u64, stage: &FeeApproxStage) -> UtxoRpcResult { utxo_common::get_htlc_spend_fee(self, tx_size, stage).await } @@ -828,7 +828,7 @@ impl UtxoCommonOps for BchCoin { fee_policy: FeePolicy, gas_fee: Option, stage: &FeeApproxStage, - ) -> TradePreimageResult { + ) -> TradePreimageResult { utxo_common::preimage_trade_fee_required_to_send_outputs( self, self.ticker(), diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index 8b8a60d246..ac6f4d7b9a 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -314,7 +314,7 @@ impl UtxoTxBroadcastOps for QtumCoin { #[async_trait] #[cfg_attr(test, mockable)] impl UtxoTxGenerationOps for QtumCoin { - async fn get_tx_fee(&self) -> UtxoRpcResult { utxo_common::get_tx_fee(&self.utxo_arc).await } + async fn get_tx_fee_per_kb(&self) -> UtxoRpcResult { utxo_common::get_tx_fee_per_kb(&self.utxo_arc).await } async fn calc_interest_if_required( &self, @@ -380,7 +380,7 @@ impl GetUtxoMapOps for QtumCoin { #[async_trait] #[cfg_attr(test, mockable)] impl UtxoCommonOps for QtumCoin { - async fn get_htlc_spend_fee(&self, tx_size: u64, stage: &FeeApproxStage) -> UtxoRpcResult { + async fn get_htlc_spend_fee(&self, tx_size: u64, stage: &FeeApproxStage) -> UtxoRpcResult { utxo_common::get_htlc_spend_fee(self, tx_size, stage).await } @@ -445,7 +445,7 @@ impl UtxoCommonOps for QtumCoin { fee_policy: FeePolicy, gas_fee: Option, stage: &FeeApproxStage, - ) -> TradePreimageResult { + ) -> TradePreimageResult { utxo_common::preimage_trade_fee_required_to_send_outputs( self, self.ticker(), diff --git a/mm2src/coins/utxo/rpc_clients.rs b/mm2src/coins/utxo/rpc_clients.rs index e8f86bc8e0..961f83ec08 100644 --- a/mm2src/coins/utxo/rpc_clients.rs +++ b/mm2src/coins/utxo/rpc_clients.rs @@ -1360,7 +1360,7 @@ impl From for BestBlock { } #[allow(clippy::upper_case_acronyms)] -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub enum EstimateFeeMode { ECONOMICAL, CONSERVATIVE, diff --git a/mm2src/coins/utxo/slp.rs b/mm2src/coins/utxo/slp.rs index c559449c22..fd5e61f7f9 100644 --- a/mm2src/coins/utxo/slp.rs +++ b/mm2src/coins/utxo/slp.rs @@ -10,8 +10,8 @@ use crate::utxo::bch::BchCoin; use crate::utxo::bchd_grpc::{check_slp_transaction, validate_slp_utxos, ValidateSlpUtxosErr}; use crate::utxo::rpc_clients::{UnspentInfo, UtxoRpcClientEnum, UtxoRpcError, UtxoRpcResult}; use crate::utxo::utxo_common::{self, big_decimal_from_sat_unsigned, payment_script, UtxoTxBuilder}; -use crate::utxo::{generate_and_send_tx, sat_from_big_decimal, ActualTxFee, AdditionalTxData, BroadcastTxErr, - FeePolicy, GenerateTxError, RecentlySpentOutPointsGuard, UtxoCoinConf, UtxoCoinFields, +use crate::utxo::{generate_and_send_tx, sat_from_big_decimal, AdditionalTxData, BroadcastTxErr, FeePolicy, + GenerateTxError, RecentlySpentOutPointsGuard, TxFeeType, UtxoCoinConf, UtxoCoinFields, UtxoCommonOps, UtxoTx, UtxoTxBroadcastOps, UtxoTxGenerationOps}; use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, ConfirmPaymentInput, DerivationMethod, DexFee, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, @@ -1077,7 +1077,7 @@ impl UtxoTxBroadcastOps for SlpToken { #[async_trait] impl UtxoTxGenerationOps for SlpToken { - async fn get_tx_fee(&self) -> UtxoRpcResult { self.platform_coin.get_tx_fee().await } + async fn get_tx_fee_per_kb(&self) -> UtxoRpcResult { self.platform_coin.get_tx_fee_per_kb().await } async fn calc_interest_if_required( &self, @@ -1670,11 +1670,11 @@ impl MmCoin for SlpToken { match req.fee { Some(WithdrawFee::UtxoFixed { amount }) => { let fixed = sat_from_big_decimal(&amount, platform_decimals)?; - tx_builder = tx_builder.with_fee(ActualTxFee::FixedPerKb(fixed)) + tx_builder = tx_builder.with_fee(TxFeeType::Fixed(fixed)) }, Some(WithdrawFee::UtxoPerKbyte { amount }) => { - let dynamic = sat_from_big_decimal(&amount, platform_decimals)?; - tx_builder = tx_builder.with_fee(ActualTxFee::Dynamic(dynamic)); + let per_kb = sat_from_big_decimal(&amount, platform_decimals)?; + tx_builder = tx_builder.with_fee(TxFeeType::PerKb(per_kb)); }, Some(fee_policy) => { let error = format!( @@ -1810,8 +1810,9 @@ impl MmCoin for SlpToken { .await?; Ok(TradeFee { coin: self.platform_coin.ticker().into(), - amount: fee.into(), + amount: fee.fee.into(), paid_from_trading_vol: false, + tx_size: fee.tx_size, }) } @@ -1823,12 +1824,14 @@ impl MmCoin for SlpToken { .platform_coin .get_htlc_spend_fee(SLP_HTLC_SPEND_SIZE, &FeeApproxStage::WithoutApprox) .await?; - let amount = - (big_decimal_from_sat_unsigned(htlc_fee, coin.platform_decimals()) + coin.platform_dust_dec()).into(); + let amount = (big_decimal_from_sat_unsigned(htlc_fee.fee, coin.platform_decimals()) + + coin.platform_dust_dec()) + .into(); Ok(TradeFee { coin: coin.platform_coin.ticker().into(), amount, paid_from_trading_vol: false, + tx_size: htlc_fee.tx_size, }) }; @@ -1859,8 +1862,9 @@ impl MmCoin for SlpToken { .await?; Ok(TradeFee { coin: self.platform_coin.ticker().into(), - amount: fee.into(), + amount: fee.fee.into(), paid_from_trading_vol: false, + tx_size: fee.tx_size, }) } diff --git a/mm2src/coins/utxo/utxo_builder/utxo_conf_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_conf_builder.rs index befbae70f9..19867557fc 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_conf_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_conf_builder.rs @@ -1,6 +1,6 @@ use crate::utxo::rpc_clients::EstimateFeeMode; -use crate::utxo::{parse_hex_encoded_u32, UtxoCoinConf, DEFAULT_DYNAMIC_FEE_VOLATILITY_PERCENT, KMD_MTP_BLOCK_COUNT, - MATURE_CONFIRMATIONS_DEFAULT}; +use crate::utxo::{parse_hex_encoded_u32, UtxoCoinConf, UtxoFeePriorities, DEFAULT_DYNAMIC_FEE_VOLATILITY_PERCENT, + KMD_MTP_BLOCK_COUNT, MATURE_CONFIRMATIONS_DEFAULT}; use crate::UtxoActivationParams; use bitcrypto::ChecksumType; use crypto::{Bip32Error, HDPathToCoin}; @@ -113,6 +113,7 @@ impl<'a> UtxoConfBuilder<'a> { let derivation_path = self.derivation_path()?; let avg_blocktime = self.avg_blocktime(); let spv_conf = self.spv_conf()?; + let fee_priorities = self.fee_priorities(); Ok(UtxoCoinConf { ticker: self.ticker.to_owned(), @@ -145,6 +146,7 @@ impl<'a> UtxoConfBuilder<'a> { spv_conf, derivation_path, avg_blocktime, + fee_priorities, }) } @@ -313,4 +315,8 @@ impl<'a> UtxoConfBuilder<'a> { } fn avg_blocktime(&self) -> Option { self.conf["avg_blocktime"].as_u64() } + + fn fee_priorities(&self) -> Option { + json::from_value(self.conf["fee_priorities"].clone()).unwrap_or(None) + } } diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index be54d53fe1..368cf44104 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -89,20 +89,27 @@ lazy_static! { }); } +/// Calculates a transaction fee for a UTXO, +/// taking a fee per kilobyte (`fee_per_kb`) and transaction size in bytes (`tx_size`). +#[macro_export] +macro_rules! calculate_fee { + ($fee_per_kb:expr, $tx_size:expr) => { + ((($fee_per_kb * $tx_size) as f64) / KILO_BYTE).ceil() + }; +} + pub const HISTORY_TOO_LARGE_ERR_CODE: i64 = -1; -pub async fn get_tx_fee(coin: &UtxoCoinFields) -> UtxoRpcResult { +pub async fn get_tx_fee_per_kb(coin: &UtxoCoinFields) -> UtxoRpcResult { let conf = &coin.conf; match &coin.tx_fee { TxFee::Dynamic(method) => { - let fee = coin - .rpc_client + coin.rpc_client .estimate_fee_sat(coin.decimals, method, &conf.estimate_fee_mode, conf.estimate_fee_blocks) .compat() - .await?; - Ok(ActualTxFee::Dynamic(fee)) + .await }, - TxFee::FixedPerKb(satoshis) => Ok(ActualTxFee::FixedPerKb(*satoshis)), + TxFee::FixedPerKb(satoshis) => Ok(*satoshis), } } @@ -271,24 +278,15 @@ pub async fn get_htlc_spend_fee( coin: &T, tx_size: u64, stage: &FeeApproxStage, -) -> UtxoRpcResult { - let coin_fee = coin.get_tx_fee().await?; - let mut fee = match coin_fee { - // atomic swap payment spend transaction is slightly more than 300 bytes in average as of now - ActualTxFee::Dynamic(fee_per_kb) => { - let fee_per_kb = increase_dynamic_fee_by_stage(&coin, fee_per_kb, stage); - (fee_per_kb * tx_size) / KILO_BYTE - }, - // return satoshis here as swap spend transaction size is always less than 1 kb - ActualTxFee::FixedPerKb(satoshis) => { - let tx_size_kb = if tx_size % KILO_BYTE == 0 { - tx_size / KILO_BYTE - } else { - tx_size / KILO_BYTE + 1 - }; - satoshis * tx_size_kb - }, - }; +) -> UtxoRpcResult { + let mut fee_per_kb = coin.get_tx_fee_per_kb().await?; + if coin.as_ref().tx_fee.is_dynamic() { + fee_per_kb = increase_dynamic_fee_by_stage(&coin, fee_per_kb, stage); + } + drop_mutability!(fee_per_kb); + + let mut fee = calculate_fee!(fee_per_kb, tx_size) as u64; + if coin.as_ref().conf.force_min_relay_fee { let relay_fee = coin.as_ref().rpc_client.get_relay_fee().compat().await?; let relay_fee_sat = sat_from_big_decimal(&relay_fee, coin.as_ref().decimals)?; @@ -296,7 +294,8 @@ pub async fn get_htlc_spend_fee( fee = relay_fee_sat; } } - Ok(fee) + + Ok(HtlcSpendFeeResult::from(fee, tx_size)) } pub fn addresses_from_script(coin: &T, script: &Script) -> Result, String> { @@ -468,7 +467,7 @@ pub struct UtxoTxBuilder<'a, T: AsRef + UtxoTxGenerationOps> { /// The available inputs that *can* be included in the resulting tx available_inputs: Vec, fee_policy: FeePolicy, - fee: Option, + fee: Option, gas_fee: Option, tx: TransactionInputSigner, change: u64, @@ -537,7 +536,7 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { self } - pub fn with_fee(mut self, fee: ActualTxFee) -> Self { + pub fn with_fee(mut self, fee: TxFeeType) -> Self { self.fee = Some(fee); self } @@ -554,24 +553,15 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { fn update_fee_and_check_completeness( &mut self, from_addr_format: &UtxoAddressFormat, - actual_tx_fee: &ActualTxFee, + actual_tx_fee: &TxFeeType, ) -> bool { self.tx_fee = match &actual_tx_fee { - ActualTxFee::Dynamic(f) => { - let transaction = UtxoTx::from(self.tx.clone()); - let v_size = tx_size_in_v_bytes(from_addr_format, &transaction); - (f * v_size as u64) / KILO_BYTE - }, - ActualTxFee::FixedPerKb(f) => { - let transaction = UtxoTx::from(self.tx.clone()); - let v_size = tx_size_in_v_bytes(from_addr_format, &transaction) as u64; - let v_size_kb = if v_size % KILO_BYTE == 0 { - v_size / KILO_BYTE - } else { - v_size / KILO_BYTE + 1 - }; - f * v_size_kb + TxFeeType::PerKb(f) => { + let tx_size = UtxoTx::from(self.tx.clone()); + let v_size = tx_size_in_v_bytes(from_addr_format, &tx_size) as u64; + calculate_fee!(f, v_size) as u64 }, + TxFeeType::Fixed(f) => *f, }; match self.fee_policy { @@ -581,9 +571,9 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { self.change = self.sum_inputs - outputs_plus_fee; if self.change > self.dust() { // there will be change output - if let ActualTxFee::Dynamic(ref f) = actual_tx_fee { - self.tx_fee += (f * P2PKH_OUTPUT_LEN) / KILO_BYTE; - outputs_plus_fee += (f * P2PKH_OUTPUT_LEN) / KILO_BYTE; + if let TxFeeType::PerKb(ref f) = actual_tx_fee { + self.tx_fee += calculate_fee!(f, P2PKH_OUTPUT_LEN) as u64; + outputs_plus_fee += calculate_fee!(f, P2PKH_OUTPUT_LEN) as u64; } } if let Some(min_relay) = self.min_relay_fee { @@ -602,8 +592,8 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { if self.sum_inputs >= self.sum_outputs_value { self.change = self.sum_inputs - self.sum_outputs_value; if self.change > self.dust() { - if let ActualTxFee::Dynamic(ref f) = actual_tx_fee { - self.tx_fee += (f * P2PKH_OUTPUT_LEN) / KILO_BYTE; + if let TxFeeType::PerKb(f) = actual_tx_fee { + self.tx_fee += calculate_fee!(f, P2PKH_OUTPUT_LEN) as u64 } } if let Some(min_relay) = self.min_relay_fee { @@ -640,7 +630,7 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { let actual_tx_fee = match self.fee { Some(fee) => fee, - None => coin.get_tx_fee().await?, + None => TxFeeType::PerKb(coin.get_tx_fee_per_kb().await?), }; true_or!(!self.tx.outputs.is_empty(), GenerateTxError::EmptyOutputs); @@ -737,6 +727,8 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { change }; + let transaction = UtxoTx::from(self.tx.clone()); + let tx_size = tx_size_in_v_bytes(from.addr_format(), &transaction) as u64; let data = AdditionalTxData { fee_amount: self.tx_fee, received_by_me, @@ -744,6 +736,7 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { unused_change, // will be changed if the ticker is KMD kmd_rewards: None, + tx_size, }; Ok(coin @@ -853,6 +846,7 @@ pub async fn calc_interest_if_required( } let rewards_amount = big_decimal_from_sat_unsigned(interest, coin.as_ref().decimals); data.kmd_rewards = Some(KmdRewardsDetails::claimed_by_me(rewards_amount)); + Ok((unsigned, data)) } @@ -1012,10 +1006,10 @@ async fn gen_taker_funding_spend_preimage( coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) .await? }, - FundingSpendFeeSetting::UseExact(f) => f, + FundingSpendFeeSetting::UseExact(f) => HtlcSpendFeeResult::from(f, args.funding_tx.serialized_size() as u64), }; - let fee_plus_dust = fee + coin.as_ref().dust_amount; + let fee_plus_dust = fee.fee + coin.as_ref().dust_amount; if funding_amount < fee_plus_dust { return MmError::err(TxGenError::TxFeeTooHigh(format!( "Fee + dust {} is larger than funding amount {}", @@ -1024,7 +1018,7 @@ async fn gen_taker_funding_spend_preimage( } let payment_output = TransactionOutput { - value: funding_amount - fee, + value: funding_amount - fee.fee, script_pubkey: Builder::build_p2sh(&AddressHashEnum::AddressHash(dhash160(&payment_redeem_script))).to_bytes(), }; @@ -1106,12 +1100,12 @@ pub async fn validate_taker_funding_spend_preimage( let actual_fee = funding_amount - payment_amount; - let fee_div = expected_fee as f64 / actual_fee as f64; + let fee_div = expected_fee.fee as f64 / actual_fee as f64; if !(0.9..=1.1).contains(&fee_div) { return MmError::err(ValidateTakerFundingSpendPreimageError::UnexpectedPreimageFee(format!( "Too large difference between expected {} and actual {} fees", - expected_fee, actual_fee + expected_fee.fee, actual_fee ))); } @@ -1270,7 +1264,7 @@ async fn gen_taker_payment_spend_preimage( .value - outputs[0].value - outputs[1].value - - tx_fee; + - tx_fee.fee; outputs.push(TransactionOutput { value: maker_value, script_pubkey: script.to_bytes(), @@ -1408,13 +1402,13 @@ pub async fn sign_and_broadcast_taker_payment_spend( .await ); - if miner_fee + coin.as_ref().dust_amount + dex_fee_sat > payment_output.value { + if miner_fee.fee + coin.as_ref().dust_amount + dex_fee_sat > payment_output.value { return TX_PLAIN_ERR!("Payment amount is too small to cover miner fee + dust + dex_fee_sat"); } let maker_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err().await); let maker_output = TransactionOutput { - value: payment_output.value - miner_fee - dex_fee_sat, + value: payment_output.value - miner_fee.fee - dex_fee_sat, script_pubkey: try_tx_s!(output_script(&maker_address)).to_bytes(), }; signer.outputs.push(maker_output); @@ -1603,16 +1597,16 @@ pub async fn send_maker_spends_taker_payment( coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) .await ); - if fee >= payment_value { + if fee.fee >= payment_value { return TX_PLAIN_ERR!( "HTLC spend fee {} is greater than transaction output {}", - fee, + fee.fee, payment_value ); } let script_pubkey = output_script(&my_address).map(|script| script.to_bytes())?; let output = TransactionOutput { - value: payment_value - fee, + value: payment_value - fee.fee, script_pubkey, }; @@ -1707,16 +1701,16 @@ pub fn create_maker_payment_spend_preimage( .await ); - if fee >= payment_value { + if fee.fee >= payment_value { return TX_PLAIN_ERR!( "HTLC spend fee {} is greater than transaction output {}", - fee, + fee.fee, payment_value ); } let script_pubkey = output_script(&my_address).map(|script| script.to_bytes())?; let output = TransactionOutput { - value: payment_value - fee, + value: payment_value - fee.fee, script_pubkey, }; @@ -1766,16 +1760,16 @@ pub fn create_taker_payment_refund_preimage( coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WatcherPreimage) .await ); - if fee >= payment_value { + if fee.fee >= payment_value { return TX_PLAIN_ERR!( "HTLC spend fee {} is greater than transaction output {}", - fee, + fee.fee, payment_value ); } let script_pubkey = output_script(&my_address).map(|script| script.to_bytes())?; let output = TransactionOutput { - value: payment_value - fee, + value: payment_value - fee.fee, script_pubkey, }; @@ -1825,16 +1819,16 @@ pub async fn send_taker_spends_maker_payment( coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) .await ); - if fee >= payment_value { + if fee.fee >= payment_value { return TX_PLAIN_ERR!( "HTLC spend fee {} is greater than transaction output {}", - fee, + fee.fee, payment_value ); } let script_pubkey = output_script(&my_address).map(|script| script.to_bytes())?; let output = TransactionOutput { - value: payment_value - fee, + value: payment_value - fee.fee, script_pubkey, }; @@ -1879,16 +1873,16 @@ pub async fn refund_htlc_payment( coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) .await ); - if fee >= payment_value { + if fee.fee >= payment_value { return TX_PLAIN_ERR!( "HTLC spend fee {} is greater than transaction output {}", - fee, + fee.fee, payment_value ); } let script_pubkey = output_script(&my_address).map(|script| script.to_bytes())?; let output = TransactionOutput { - value: payment_value - fee, + value: payment_value - fee.fee, script_pubkey, }; @@ -3796,20 +3790,22 @@ pub fn get_trade_fee(coin: T) -> Box f, - ActualTxFee::FixedPerKb(f) => f, - }; + let fee = try_s!(coin.get_tx_fee_per_kb().await); Ok(TradeFee { coin: ticker, - amount: big_decimal_from_sat(amount as i64, decimals).into(), + amount: big_decimal_from_sat(fee as i64, decimals).into(), paid_from_trading_vol: false, + tx_size: 0, }) }; Box::new(fut.boxed().compat()) } +pub struct PreImageTradeFeeResult { + pub fee: BigDecimal, + pub tx_size: u64, +} + /// To ensure the `get_sender_trade_fee(x) <= get_sender_trade_fee(y)` condition is satisfied for any `x < y`, /// we should include a `change` output into the result fee. Imagine this case: /// Let `sum_inputs = 11000` and `total_tx_fee: { 200, if there is no the change output; 230, if there is the change output }`. @@ -3829,84 +3825,58 @@ pub async fn preimage_trade_fee_required_to_send_outputs( fee_policy: FeePolicy, gas_fee: Option, stage: &FeeApproxStage, -) -> TradePreimageResult +) -> TradePreimageResult where T: UtxoCommonOps + GetUtxoListOps, { let decimals = coin.as_ref().decimals; - let tx_fee = coin.get_tx_fee().await?; // [`FeePolicy::DeductFromOutput`] is used if the value is [`TradePreimageValue::UpperBound`] only let is_amount_upper_bound = matches!(fee_policy, FeePolicy::DeductFromOutput(_)); let my_address = coin.as_ref().derivation_method.single_addr_or_err().await?; - match tx_fee { - // if it's a dynamic fee, we should generate a swap transaction to get an actual trade fee - ActualTxFee::Dynamic(fee) => { - // take into account that the dynamic tx fee may increase during the swap - let dynamic_fee = coin.increase_dynamic_fee_by_stage(fee, stage); - - let outputs_count = outputs.len(); - let (unspents, _recently_sent_txs) = coin.get_unspent_ordered_list(&my_address).await?; + let mut tx_fee_per_kb = coin.get_tx_fee_per_kb().await?; + if coin.as_ref().tx_fee.is_dynamic() { + tx_fee_per_kb = coin.increase_dynamic_fee_by_stage(tx_fee_per_kb, stage) + }; - let actual_tx_fee = ActualTxFee::Dynamic(dynamic_fee); + let outputs_count = outputs.len(); + let (unspents, _recently_sent_txs) = coin.get_unspent_ordered_list(&my_address).await?; - let mut tx_builder = UtxoTxBuilder::new(coin) - .await - .add_available_inputs(unspents) - .add_outputs(outputs) - .with_fee_policy(fee_policy) - .with_fee(actual_tx_fee); - if let Some(gas) = gas_fee { - tx_builder = tx_builder.with_gas_fee(gas); - } - let (tx, data) = tx_builder.build().await.mm_err(|e| { - TradePreimageError::from_generate_tx_error(e, ticker.to_owned(), decimals, is_amount_upper_bound) - })?; + let mut tx_builder = UtxoTxBuilder::new(coin) + .await + .add_available_inputs(unspents) + .add_outputs(outputs) + .with_fee_policy(fee_policy) + .with_fee(TxFeeType::PerKb(tx_fee_per_kb)); + if let Some(gas) = gas_fee { + tx_builder = tx_builder.with_gas_fee(gas); + } + let (tx, data) = tx_builder.build().await.mm_err(|e| { + TradePreimageError::from_generate_tx_error(e, ticker.to_owned(), decimals, is_amount_upper_bound) + })?; - let total_fee = if tx.outputs.len() == outputs_count { - // take into account the change output - data.fee_amount + (dynamic_fee * P2PKH_OUTPUT_LEN) / KILO_BYTE + let total_fee = if tx.outputs.len() == outputs_count { + if coin.as_ref().tx_fee.is_dynamic() { + data.fee_amount + ((tx_fee_per_kb * P2PKH_OUTPUT_LEN) as f64 / KILO_BYTE) as u64 + } else { + // take into account the change output if tx_size_kb(tx with change) > tx_size_kb(tx without change) + let tx = UtxoTx::from(tx); + let tx_bytes_len = serialize(&tx).len(); + if (tx_bytes_len as f64 % KILO_BYTE + P2PKH_OUTPUT_LEN as f64) > KILO_BYTE { + data.fee_amount + tx_fee_per_kb } else { - // the change output is included already data.fee_amount - }; - - Ok(big_decimal_from_sat(total_fee as i64, decimals)) - }, - ActualTxFee::FixedPerKb(fee) => { - let outputs_count = outputs.len(); - let (unspents, _recently_sent_txs) = coin.get_unspent_ordered_list(&my_address).await?; - - let mut tx_builder = UtxoTxBuilder::new(coin) - .await - .add_available_inputs(unspents) - .add_outputs(outputs) - .with_fee_policy(fee_policy) - .with_fee(tx_fee); - if let Some(gas) = gas_fee { - tx_builder = tx_builder.with_gas_fee(gas); } - let (tx, data) = tx_builder.build().await.mm_err(|e| { - TradePreimageError::from_generate_tx_error(e, ticker.to_string(), decimals, is_amount_upper_bound) - })?; - - let total_fee = if tx.outputs.len() == outputs_count { - // take into account the change output if tx_size_kb(tx with change) > tx_size_kb(tx without change) - let tx = UtxoTx::from(tx); - let tx_bytes = serialize(&tx); - if tx_bytes.len() as u64 % KILO_BYTE + P2PKH_OUTPUT_LEN > KILO_BYTE { - data.fee_amount + fee - } else { - data.fee_amount - } - } else { - // the change output is included already - data.fee_amount - }; + } + } else { + // the change output is included already + data.fee_amount + }; - Ok(big_decimal_from_sat(total_fee as i64, decimals)) - }, - } + Ok(PreImageTradeFeeResult { + fee: big_decimal_from_sat(total_fee as i64, decimals), + tx_size: data.tx_size, + }) } /// Maker or Taker should pay fee only for sending his payment. @@ -3945,13 +3915,14 @@ where ) .map_to_mm(TradePreimageError::InternalError)?; let gas_fee = None; - let fee_amount = coin + let fee = coin .preimage_trade_fee_required_to_send_outputs(outputs, fee_policy, gas_fee, &stage) .await?; Ok(TradeFee { coin: coin.as_ref().conf.ticker.clone(), - amount: fee_amount.into(), + amount: fee.fee.clone().into(), paid_from_trading_vol: false, + tx_size: fee.tx_size, }) } @@ -3959,11 +3930,12 @@ where pub fn get_receiver_trade_fee(coin: T) -> TradePreimageFut { let fut = async move { let amount_sat = get_htlc_spend_fee(&coin, DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox).await?; - let amount = big_decimal_from_sat_unsigned(amount_sat, coin.as_ref().decimals).into(); + let amount = big_decimal_from_sat_unsigned(amount_sat.fee, coin.as_ref().decimals).into(); Ok(TradeFee { coin: coin.as_ref().conf.ticker.clone(), amount, paid_from_trading_vol: true, + tx_size: amount_sat.tx_size, }) }; Box::new(fut.boxed().compat()) @@ -3987,8 +3959,9 @@ where .await?; Ok(TradeFee { coin: coin.ticker().to_owned(), - amount: fee_amount.into(), + amount: fee_amount.fee.into(), paid_from_trading_vol: false, + tx_size: fee_amount.tx_size, }) } @@ -4844,16 +4817,16 @@ where coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) .await ); - if fee >= payment_value { + if fee.fee >= payment_value { return TX_PLAIN_ERR!( "HTLC spend fee {} is greater than transaction output {}", - fee, + fee.fee, payment_value ); } let script_pubkey = output_script(&my_address).map(|script| script.to_bytes())?; let output = TransactionOutput { - value: payment_value - fee, + value: payment_value - fee.fee, script_pubkey, }; @@ -5036,16 +5009,16 @@ pub async fn spend_maker_payment_v2( coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) .await ); - if fee >= payment_value { + if fee.fee >= payment_value { return TX_PLAIN_ERR!( "HTLC spend fee {} is greater than transaction output {}", - fee, + fee.fee, payment_value ); } let script_pubkey = try_tx_s!(output_script(&my_address)).to_bytes(); let output = TransactionOutput { - value: payment_value - fee, + value: payment_value - fee.fee, script_pubkey, }; @@ -5097,16 +5070,16 @@ where coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) .await ); - if fee >= payment_value { + if fee.fee >= payment_value { return TX_PLAIN_ERR!( "HTLC spend fee {} is greater than transaction output {}", - fee, + fee.fee, payment_value ); } let script_pubkey = try_tx_s!(output_script(&my_address)).to_bytes(); let output = TransactionOutput { - value: payment_value - fee, + value: payment_value - fee.fee, script_pubkey, }; diff --git a/mm2src/coins/utxo/utxo_common_tests.rs b/mm2src/coins/utxo/utxo_common_tests.rs index 851f4a0644..c20d8cf2ad 100644 --- a/mm2src/coins/utxo/utxo_common_tests.rs +++ b/mm2src/coins/utxo/utxo_common_tests.rs @@ -133,6 +133,7 @@ pub(super) fn utxo_coin_fields_for_test( spv_conf: None, derivation_path: None, avg_blocktime: None, + fee_priorities: None, }, decimals: TEST_COIN_DECIMALS, dust_amount: UTXO_DUST_AMOUNT, diff --git a/mm2src/coins/utxo/utxo_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index 03c889d790..b2d0bbbbc4 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -109,7 +109,7 @@ impl UtxoTxBroadcastOps for UtxoStandardCoin { #[async_trait] #[cfg_attr(test, mockable)] impl UtxoTxGenerationOps for UtxoStandardCoin { - async fn get_tx_fee(&self) -> UtxoRpcResult { utxo_common::get_tx_fee(&self.utxo_arc).await } + async fn get_tx_fee_per_kb(&self) -> UtxoRpcResult { utxo_common::get_tx_fee_per_kb(&self.utxo_arc).await } async fn calc_interest_if_required( &self, @@ -176,7 +176,7 @@ impl GetUtxoMapOps for UtxoStandardCoin { #[async_trait] #[cfg_attr(test, mockable)] impl UtxoCommonOps for UtxoStandardCoin { - async fn get_htlc_spend_fee(&self, tx_size: u64, stage: &FeeApproxStage) -> UtxoRpcResult { + async fn get_htlc_spend_fee(&self, tx_size: u64, stage: &FeeApproxStage) -> UtxoRpcResult { utxo_common::get_htlc_spend_fee(self, tx_size, stage).await } @@ -237,7 +237,7 @@ impl UtxoCommonOps for UtxoStandardCoin { fee_policy: FeePolicy, gas_fee: Option, stage: &FeeApproxStage, - ) -> TradePreimageResult { + ) -> TradePreimageResult { utxo_common::preimage_trade_fee_required_to_send_outputs( self, self.ticker(), diff --git a/mm2src/coins/utxo/utxo_tests.rs b/mm2src/coins/utxo/utxo_tests.rs index 3b11824444..0c18d5f8b8 100644 --- a/mm2src/coins/utxo/utxo_tests.rs +++ b/mm2src/coins/utxo/utxo_tests.rs @@ -20,16 +20,16 @@ use crate::utxo::spv::SimplePaymentVerification; #[cfg(not(target_arch = "wasm32"))] use crate::utxo::utxo_block_header_storage::{BlockHeaderStorage, SqliteBlockHeadersStorage}; use crate::utxo::utxo_builder::{UtxoArcBuilder, UtxoCoinBuilder, UtxoCoinBuilderCommonOps}; -use crate::utxo::utxo_common::UtxoTxBuilder; +use crate::utxo::utxo_common::{tx_size_in_v_bytes, UtxoTxBuilder}; #[cfg(not(target_arch = "wasm32"))] use crate::utxo::utxo_common_tests::TEST_COIN_DECIMALS; use crate::utxo::utxo_common_tests::{self, utxo_coin_fields_for_test, utxo_coin_from_fields, TEST_COIN_NAME}; use crate::utxo::utxo_hd_wallet::UtxoHDAccount; use crate::utxo::utxo_standard::{utxo_standard_coin_with_priv_key, UtxoStandardCoin}; use crate::utxo::utxo_tx_history_v2::{UtxoTxDetailsParams, UtxoTxHistoryOps}; -use crate::{BlockHeightAndTime, CoinBalance, ConfirmPaymentInput, DexFee, IguanaPrivKey, PrivKeyBuildPolicy, - SearchForSwapTxSpendInput, SpendPaymentArgs, StakingInfosDetails, SwapOps, TradePreimageValue, - TxFeeDetails, TxMarshalingErr, ValidateFeeArgs, INVALID_SENDER_ERR_LOG}; +use crate::{calculate_fee, BlockHeightAndTime, CoinBalance, ConfirmPaymentInput, DexFee, IguanaPrivKey, + PrivKeyBuildPolicy, SearchForSwapTxSpendInput, SpendPaymentArgs, StakingInfosDetails, SwapOps, + TradePreimageValue, TxFeeDetails, TxMarshalingErr, ValidateFeeArgs, INVALID_SENDER_ERR_LOG}; #[cfg(not(target_arch = "wasm32"))] use crate::{WaitForHTLCTxSpendArgs, WithdrawFee}; use chain::{BlockHeader, BlockHeaderBits, OutPoint}; @@ -213,18 +213,19 @@ fn test_generate_transaction() { let outputs = vec![TransactionOutput { script_pubkey: vec![].into(), - value: 98001, + value: 98781, }]; let builder = block_on(UtxoTxBuilder::new(&coin)) .add_available_inputs(unspents) .add_outputs(outputs); let generated = block_on(builder.build()).unwrap(); + // the change that is less than dust must be included to miner fee // so no extra outputs should appear in generated transaction assert_eq!(generated.0.outputs.len(), 1); - assert_eq!(generated.1.fee_amount, 1000); + assert_eq!(generated.1.fee_amount, 220); assert_eq!(generated.1.unused_change, 999); assert_eq!(generated.1.received_by_me, 0); assert_eq!(generated.1.spent_by_me, 100000); @@ -251,11 +252,11 @@ fn test_generate_transaction() { let generated = block_on(builder.build()).unwrap(); assert_eq!(generated.0.outputs.len(), 1); - assert_eq!(generated.1.fee_amount, 1000); + assert_eq!(generated.1.fee_amount, 211); assert_eq!(generated.1.unused_change, 0); - assert_eq!(generated.1.received_by_me, 99000); + assert_eq!(generated.1.received_by_me, 99789); assert_eq!(generated.1.spent_by_me, 100000); - assert_eq!(generated.0.outputs[0].value, 99000); + assert_eq!(generated.0.outputs[0].value, 99789); let unspents = vec![UnspentInfo { value: 100000, @@ -905,11 +906,24 @@ fn test_withdraw_kmd_rewards_impl( memo: None, ibc_source_channel: None, }; + let tx_details = coin.withdraw(withdraw_req).wait().unwrap(); + let tx_size_kb = tx_details.tx.tx_hex().unwrap().len() as f64 / KILO_BYTE; + // In transaction size calculations we assume the script sig size is (2 + MAX_DER_SIGNATURE_LEN + COMPRESSED_PUBKEY_LEN) or 107 bytes + // when in reality signatures can vary by 1 or 2 bytes because of possible zero padding of r and s values of the signature. + // This is why we test for a range of values here instead of a single value. The value we use in fees calculation is the + // highest possible value of 107 to ensure we don't underestimate the fee. + assert!( + (0.243..=0.245).contains(&tx_size_kb), + "Tx size in KB {} is not within the range [{}, {}]", + tx_size_kb, + 0.243, + 0.245 + ); + let tx_size_kb = 0.245; let expected_fee = TxFeeDetails::Utxo(UtxoFeeDetails { coin: Some("KMD".into()), - amount: "0.00001".parse().unwrap(), + amount: big_decimal_from_sat((1000. * tx_size_kb) as i64, coin.as_ref().decimals), }); - let tx_details = coin.withdraw(withdraw_req).wait().unwrap(); assert_eq!(tx_details.fee_details, Some(expected_fee)); let expected_rewards = expected_rewards.map(|amount| KmdRewardsDetails { @@ -984,11 +998,24 @@ fn test_withdraw_rick_rewards_none() { memo: None, ibc_source_channel: None, }; + let tx_details = coin.withdraw(withdraw_req).wait().unwrap(); + let tx_size_kb = tx_details.tx.tx_hex().unwrap().len() as f64 / KILO_BYTE; + // In transaction size calculations we assume the script sig size is (2 + MAX_DER_SIGNATURE_LEN + COMPRESSED_PUBKEY_LEN) or 107 bytes + // when in reality signatures can vary by 1 or 2 bytes because of possible zero padding of r and s values of the signature. + // This is why we test for a range of values here instead of a single value. The value we use in fees calculation is the + // highest possible value of 107 to ensure we don't underestimate the fee. + assert!( + (0.243..=0.245).contains(&tx_size_kb), + "Tx size in KB {} is not within the range [{}, {}]", + tx_size_kb, + 0.243, + 0.245 + ); + let tx_size_kb = 0.245; let expected_fee = TxFeeDetails::Utxo(UtxoFeeDetails { coin: Some(TEST_COIN_NAME.into()), - amount: "0.00001".parse().unwrap(), + amount: big_decimal_from_sat((1000. * tx_size_kb) as i64, coin.as_ref().decimals), }); - let tx_details = coin.withdraw(withdraw_req).wait().unwrap(); assert_eq!(tx_details.fee_details, Some(expected_fee)); assert_eq!(tx_details.kmd_rewards, None); } @@ -1190,7 +1217,7 @@ fn test_generate_transaction_relay_fee_is_used_when_dynamic_fee_is_lower() { let builder = block_on(UtxoTxBuilder::new(&coin)) .add_available_inputs(unspents) .add_outputs(outputs) - .with_fee(ActualTxFee::Dynamic(100)); + .with_fee(TxFeeType::PerKb(100)); let generated = block_on(builder.build()).unwrap(); assert_eq!(generated.0.outputs.len(), 1); @@ -1234,7 +1261,7 @@ fn test_generate_transaction_relay_fee_is_used_when_dynamic_fee_is_lower_and_ded .add_available_inputs(unspents) .add_outputs(outputs) .with_fee_policy(FeePolicy::DeductFromOutput(0)) - .with_fee(ActualTxFee::Dynamic(100)); + .with_fee(TxFeeType::PerKb(100)); let generated = block_on(tx_builder.build()).unwrap(); assert_eq!(generated.0.outputs.len(), 1); @@ -1282,7 +1309,7 @@ fn test_generate_tx_fee_is_correct_when_dynamic_fee_is_larger_than_relay() { let builder = block_on(UtxoTxBuilder::new(&coin)) .add_available_inputs(unspents) .add_outputs(outputs) - .with_fee(ActualTxFee::Dynamic(1000)); + .with_fee(TxFeeType::PerKb(1000)); let generated = block_on(builder.build()).unwrap(); @@ -2804,6 +2831,9 @@ fn firo_lelantus_tx_details() { #[test] fn test_generate_tx_doge_fee() { + // Choose a tx fee per kb equal to minimum relay fee to test that the minimum relay fee is used when transaction is below 1kb + const TXFEE_PER_KB: u64 = 100_000; + // A tx below 1kb is always 0,01 doge fee per kb. let config = json!({ "coin": "DOGE", @@ -2813,7 +2843,13 @@ fn test_generate_tx_doge_fee() { "pubtype": 30, "p2shtype": 22, "wiftype": 158, - "txfee": 1000000, + // The minimum relay fee for doge was changed from 1000000 sats to 100000 sats since the below issues were resolved + // The transaction size is below 1 kb so the fee should be 0.001 doge (100000 sats) + // https://github.com/KomodoPlatform/atomicDEX-API/issues/829 + // https://github.com/KomodoPlatform/atomicDEX-API/pull/830 + // https://github.com/KomodoPlatform/atomicDEX-API/commit/faf944ea721bd87816b9b3b1cdf02d4bf3f4c6ea + // https://github.com/dogecoin/dogecoin/discussions/2347 + "txfee": TXFEE_PER_KB, "force_min_relay_fee": true, "mm2": 1, "required_confirmations": 2, @@ -2836,6 +2872,8 @@ fn test_generate_tx_doge_fee() { )) .unwrap(); + let min_relay_fee = block_on(doge.as_ref().rpc_client.get_relay_fee().compat()).unwrap(); + let min_relay_fee_sat = sat_from_big_decimal(&min_relay_fee, doge.as_ref().decimals).unwrap(); let unspents = vec![UnspentInfo { outpoint: Default::default(), value: 1000000000000, @@ -2849,8 +2887,12 @@ fn test_generate_tx_doge_fee() { let builder = block_on(UtxoTxBuilder::new(&doge)) .add_available_inputs(unspents) .add_outputs(outputs); - let (_, data) = block_on(builder.build()).unwrap(); - let expected_fee = 1000000; + let (input_signer, data) = block_on(builder.build()).unwrap(); + let transaction = UtxoTx::from(input_signer); + let v_size = tx_size_in_v_bytes(&UtxoAddressFormat::Standard, &transaction) as u64; + assert!(v_size < KILO_BYTE as u64); + // The fee should be min_relay_fee_sat because the tx size is below 1 kb + let expected_fee = min_relay_fee_sat; assert_eq!(expected_fee, data.fee_amount); let unspents = vec![UnspentInfo { @@ -2870,8 +2912,11 @@ fn test_generate_tx_doge_fee() { let builder = block_on(UtxoTxBuilder::new(&doge)) .add_available_inputs(unspents) .add_outputs(outputs); - let (_, data) = block_on(builder.build()).unwrap(); - let expected_fee = 2000000; + let (input_signer, data) = block_on(builder.build()).unwrap(); + let transaction = UtxoTx::from(input_signer); + let v_size = tx_size_in_v_bytes(&UtxoAddressFormat::Standard, &transaction) as u64; + assert!(v_size > KILO_BYTE as u64); + let expected_fee = calculate_fee!(TXFEE_PER_KB, v_size) as u64; assert_eq!(expected_fee, data.fee_amount); let unspents = vec![UnspentInfo { @@ -2891,8 +2936,11 @@ fn test_generate_tx_doge_fee() { let builder = block_on(UtxoTxBuilder::new(&doge)) .add_available_inputs(unspents) .add_outputs(outputs); - let (_, data) = block_on(builder.build()).unwrap(); - let expected_fee = 3000000; + let (input_signer, data) = block_on(builder.build()).unwrap(); + let transaction = UtxoTx::from(input_signer); + let v_size = tx_size_in_v_bytes(&UtxoAddressFormat::Standard, &transaction) as u64; + assert!(v_size > KILO_BYTE as u64); + let expected_fee = calculate_fee!(TXFEE_PER_KB, v_size) as u64; assert_eq!(expected_fee, data.fee_amount); } @@ -3379,6 +3427,10 @@ fn test_withdraw_p2pk_balance() { let coin = utxo_coin_for_test(UtxoRpcClientEnum::Native(client), None, false); + const VALUE: u64 = 1000000000; + const TX_SIZE: u64 = 211; + const TXFEE_PER_KB: u64 = 1000; + UtxoStandardCoin::get_unspent_ordered_list.mock_safe(|coin, _| { let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); let unspents = vec![UnspentInfo { @@ -3386,7 +3438,7 @@ fn test_withdraw_p2pk_balance() { hash: 1.into(), index: 0, }, - value: 1000000000, + value: VALUE, height: Default::default(), // Use a p2pk output script for this UTXO script: output_script_p2pk( @@ -3401,8 +3453,9 @@ fn test_withdraw_p2pk_balance() { // Create a dummy p2pkh address to withdraw the coins to. let my_p2pkh_address = block_on(coin.as_ref().derivation_method.unwrap_single_addr()); + let total_value_sent = BigDecimal::from(1); let withdraw_req = WithdrawRequest { - amount: 1.into(), + amount: total_value_sent.clone(), from: None, to: my_p2pkh_address.to_string(), coin: TEST_COIN_NAME.into(), @@ -3419,8 +3472,13 @@ fn test_withdraw_p2pk_balance() { let expected_script = Builder::build_p2pkh(my_p2pkh_address.hash()); assert_eq!(output_script, expected_script); + let mut expected_fee = calculate_fee!(TXFEE_PER_KB, TX_SIZE) as u64; + expected_fee += calculate_fee!(TXFEE_PER_KB, P2PKH_OUTPUT_LEN) as u64; + let amount_sent = sat_from_big_decimal(&total_value_sent, TEST_COIN_DECIMALS).unwrap(); + let expected_balance = VALUE - amount_sent - expected_fee; + // And it should have this value (p2pk balance - amount sent - fees). - assert_eq!(transaction.outputs[1].value, 899999000); + assert_eq!(transaction.outputs[1].value, expected_balance); } /// `UtxoStandardCoin` has to check UTXO maturity if `check_utxo_maturity` is `true`. diff --git a/mm2src/coins/utxo/utxo_withdraw.rs b/mm2src/coins/utxo/utxo_withdraw.rs index 8721a6a433..849178da8c 100644 --- a/mm2src/coins/utxo/utxo_withdraw.rs +++ b/mm2src/coins/utxo/utxo_withdraw.rs @@ -1,13 +1,14 @@ use crate::rpc_command::init_withdraw::{WithdrawInProgressStatus, WithdrawTaskHandleShared}; use crate::utxo::utxo_common::{big_decimal_from_sat, UtxoTxBuilder}; -use crate::utxo::{output_script, sat_from_big_decimal, ActualTxFee, Address, FeePolicy, GetUtxoListOps, PrivKeyPolicy, +use crate::utxo::{output_script, sat_from_big_decimal, Address, FeePolicy, GetUtxoListOps, PrivKeyPolicy, TxFeeType, UtxoAddressFormat, UtxoCoinFields, UtxoCommonOps, UtxoFeeDetails, UtxoTx, UTXO_LOCK}; use crate::{CoinWithDerivationMethod, GetWithdrawSenderAddress, MarketCoinOps, TransactionData, TransactionDetails, - WithdrawError, WithdrawFee, WithdrawRequest, WithdrawResult}; + UtxoFeePriority, WithdrawError, WithdrawFee, WithdrawRequest, WithdrawResult}; + use async_trait::async_trait; use chain::TransactionOutput; use common::log::info; -use common::now_sec; +use common::{now_sec, Future01CompatExt}; use crypto::hw_rpc_task::HwRpcTaskAwaitingStatus; use crypto::trezor::trezor_rpc_task::{TrezorRequestStatuses, TrezorRpcTaskProcessor}; use crypto::trezor::{TrezorError, TrezorProcessingError}; @@ -15,6 +16,7 @@ use crypto::{from_hw_error, CryptoCtx, CryptoCtxError, DerivationPath, HwError, use keys::{AddressFormat, KeyPair, Private, Public as PublicKey}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; +use mm2_net::transport::slurp_url; use rpc::v1::types::ToTxHash; use rpc_task::RpcTaskError; use script::{SignatureVersion, TransactionInputSigner}; @@ -25,6 +27,10 @@ use utxo_signer::sign_params::{OutputDestination, SendingOutputInfo, SpendingInp use utxo_signer::{with_key_pair, UtxoSignTxError}; use utxo_signer::{SignPolicy, UtxoSignerOps}; +use super::UtxoFeePriorities; + +const MEMPOOL_BTC_FEE_RATE: &str = "https://mempool.space/api/v1/fees/recommended"; + impl From for WithdrawError { fn from(sign_err: UtxoSignTxError) -> Self { match sign_err { @@ -158,11 +164,18 @@ where match req.fee { Some(WithdrawFee::UtxoFixed { ref amount }) => { let fixed = sat_from_big_decimal(amount, decimals)?; - tx_builder = tx_builder.with_fee(ActualTxFee::FixedPerKb(fixed)); + tx_builder = tx_builder.with_fee(TxFeeType::Fixed(fixed)); }, Some(WithdrawFee::UtxoPerKbyte { ref amount }) => { let dynamic = sat_from_big_decimal(amount, decimals)?; - tx_builder = tx_builder.with_fee(ActualTxFee::Dynamic(dynamic)); + tx_builder = tx_builder.with_fee(TxFeeType::PerKb(dynamic)); + }, + Some(WithdrawFee::UtxoPriority { ref priority, .. }) => { + // handle the withdrawal fee calculation based on the priority of the transaction. + let fee = + generate_withdraw_fee_using_priority(coin, priority, &coin.as_ref().conf.fee_priorities, decimals) + .await?; + tx_builder = tx_builder.with_fee(TxFeeType::PerKb(fee)); }, Some(ref fee_policy) => { let error = format!( @@ -213,6 +226,69 @@ where } } +/// Checks if fee priorities are defined in the coin's configuration, +/// retrieves the fee estimate from mempool, and adjusts the fee for Bitcoin if necessary. +async fn generate_withdraw_fee_using_priority( + coin: &C, + priority: &UtxoFeePriority, + priorities: &Option, + decimals: u8, +) -> Result> { + // Check if priorities are defined in coin config. + let Some(detail) = priorities else { + return MmError::err(WithdrawError::InternalError(format!("fee_priorities not found in {} config", coin.as_ref().conf.ticker))) + }; + let n_blocks = match priority { + UtxoFeePriority::Low => detail.low, + UtxoFeePriority::Normal => detail.normal, + UtxoFeePriority::High => detail.high, + }; + let mut estimate_fee_sat = coin + .as_ref() + .rpc_client + .estimate_fee_sat( + decimals, + &crate::utxo::rpc_clients::EstimateFeeMethod::Standard, + &None, + n_blocks.into(), + ) + .compat() + .await?; + + if ["BTC"].contains(&coin.as_ref().conf.ticker.as_str()) { + if let UtxoFeePriority::Low = priority { + let mem_pool_fee = fetch_btc_mempool_low_fee_sat(decimals) + .await + .map_err(WithdrawError::InternalError)?; + info!("mem_pool_fee: {mem_pool_fee} - estimate_fee_sat: {estimate_fee_sat}"); + if mem_pool_fee < estimate_fee_sat { + estimate_fee_sat = mem_pool_fee; + } + } + } + + Ok(estimate_fee_sat) +} + +#[derive(Debug, Deserialize)] +struct BTCMempoolFeeRate { + #[serde(rename = "economyFee")] + economy_fee: u8, +} + +/// Fetches the current low (economy) fee rate for Bitcoin transactions from the mempool. +async fn fetch_btc_mempool_low_fee_sat(decimals: u8) -> Result { + let (status, _, body) = try_s!(slurp_url(MEMPOOL_BTC_FEE_RATE).await); + if !status.is_success() { + return Err("Fetch BTC mempool fee rate was unsuccessful".to_owned()); + }; + let fee_rate: BTCMempoolFeeRate = try_s!(serde_json::from_slice(&body)); + let fee_rate = (fee_rate.economy_fee as f64 * 10.0_f64.powf(decimals as f64)) as u64; + Ok(fee_rate) +} + +/// estimates the fee for a transaction using the provided RPC client, priority level +/// and then updates the transaction builder with the calculated fee. pub struct InitUtxoWithdraw { ctx: MmArc, coin: Coin, diff --git a/mm2src/coins/z_coin.rs b/mm2src/coins/z_coin.rs index d9a416d204..823b6c9065 100644 --- a/mm2src/coins/z_coin.rs +++ b/mm2src/coins/z_coin.rs @@ -16,10 +16,10 @@ use crate::utxo::rpc_clients::{ElectrumRpcRequest, UnspentInfo, UtxoRpcClientEnu use crate::utxo::utxo_builder::UtxoCoinBuildError; use crate::utxo::utxo_builder::{UtxoCoinBuilder, UtxoCoinBuilderCommonOps, UtxoFieldsWithGlobalHDBuilder, UtxoFieldsWithHardwareWalletBuilder, UtxoFieldsWithIguanaSecretBuilder}; -use crate::utxo::utxo_common::{addresses_from_script, big_decimal_from_sat}; -use crate::utxo::utxo_common::{big_decimal_from_sat_unsigned, payment_script}; -use crate::utxo::{sat_from_big_decimal, utxo_common, ActualTxFee, AdditionalTxData, AddrFromStrError, Address, - BroadcastTxErr, FeePolicy, GetUtxoListOps, HistoryUtxoTx, HistoryUtxoTxMap, MatureUnspentList, +use crate::utxo::utxo_common::{big_decimal_from_sat, big_decimal_from_sat_unsigned, payment_script, + PreImageTradeFeeResult}; +use crate::utxo::{sat_from_big_decimal, utxo_common, AdditionalTxData, AddrFromStrError, Address, BroadcastTxErr, + FeePolicy, GetUtxoListOps, HistoryUtxoTx, HistoryUtxoTxMap, HtlcSpendFeeResult, MatureUnspentList, RecentlySpentOutPointsGuard, UtxoActivationParams, UtxoAddressFormat, UtxoArc, UtxoCoinFields, UtxoCommonOps, UtxoRpcMode, UtxoTxBroadcastOps, UtxoTxGenerationOps, VerboseTransactionFrom}; use crate::utxo::{UnsupportedAddr, UtxoFeeDetails}; @@ -365,12 +365,8 @@ impl ZCoin { } async fn get_one_kbyte_tx_fee(&self) -> UtxoRpcResult { - let fee = self.get_tx_fee().await?; - match fee { - ActualTxFee::Dynamic(fee) | ActualTxFee::FixedPerKb(fee) => { - Ok(big_decimal_from_sat_unsigned(fee, self.decimals())) - }, - } + let fee = self.get_tx_fee_per_kb().await?; + Ok(big_decimal_from_sat_unsigned(fee, self.decimals())) } /// Generates a tx sending outputs from our address @@ -479,7 +475,9 @@ impl ZCoin { fee_amount: sat_from_big_decimal(&tx_fee, self.decimals())?, unused_change: 0, kmd_rewards: None, + tx_size: tx.tx_hex().len() as u64, }; + Ok((tx, additional_data, sync_guard)) } @@ -540,7 +538,9 @@ impl ZCoin { if let Some(spent_output) = prev_tx.vout.get(input.prevout.n() as usize) { transparent_input_amount += spent_output.value; - if let Ok(addresses) = addresses_from_script(self, &spent_output.script_pubkey.0.clone().into()) { + if let Ok(addresses) = + utxo_common::addresses_from_script(self, &spent_output.script_pubkey.0.clone().into()) + { from.extend(addresses.into_iter().map(|a| a.to_string())); } } @@ -553,7 +553,7 @@ impl ZCoin { let mut to = HashSet::new(); for out in z_tx.vout.iter() { - if let Ok(addresses) = addresses_from_script(self, &out.script_pubkey.0.clone().into()) { + if let Ok(addresses) = utxo_common::addresses_from_script(self, &out.script_pubkey.0.clone().into()) { to.extend(addresses.into_iter().map(|a| a.to_string())); } } @@ -1729,6 +1729,7 @@ impl MmCoin for ZCoin { coin: self.ticker().to_owned(), amount: self.get_one_kbyte_tx_fee().await?.into(), paid_from_trading_vol: false, + tx_size: 0, }) } @@ -1745,6 +1746,7 @@ impl MmCoin for ZCoin { coin: self.ticker().to_owned(), amount: self.get_one_kbyte_tx_fee().await?.into(), paid_from_trading_vol: false, + tx_size: 0, }) } @@ -1787,7 +1789,7 @@ impl MmCoin for ZCoin { #[async_trait] impl UtxoTxGenerationOps for ZCoin { - async fn get_tx_fee(&self) -> UtxoRpcResult { utxo_common::get_tx_fee(&self.utxo_arc).await } + async fn get_tx_fee_per_kb(&self) -> UtxoRpcResult { utxo_common::get_tx_fee_per_kb(&self.utxo_arc).await } async fn calc_interest_if_required( &self, @@ -1837,7 +1839,7 @@ impl GetUtxoListOps for ZCoin { #[async_trait] impl UtxoCommonOps for ZCoin { - async fn get_htlc_spend_fee(&self, tx_size: u64, stage: &FeeApproxStage) -> UtxoRpcResult { + async fn get_htlc_spend_fee(&self, tx_size: u64, stage: &FeeApproxStage) -> UtxoRpcResult { utxo_common::get_htlc_spend_fee(self, tx_size, stage).await } @@ -1904,7 +1906,7 @@ impl UtxoCommonOps for ZCoin { fee_policy: FeePolicy, gas_fee: Option, stage: &FeeApproxStage, - ) -> TradePreimageResult { + ) -> TradePreimageResult { utxo_common::preimage_trade_fee_required_to_send_outputs( self, self.ticker(), diff --git a/mm2src/mm2_main/src/lp_swap.rs b/mm2src/mm2_main/src/lp_swap.rs index 184f8e3c05..ac97d11367 100644 --- a/mm2src/mm2_main/src/lp_swap.rs +++ b/mm2src/mm2_main/src/lp_swap.rs @@ -1056,6 +1056,7 @@ impl From for TradeFee { coin: orig.coin, amount: orig.amount.into(), paid_from_trading_vol: orig.paid_from_trading_vol, + tx_size: 0, } } } diff --git a/mm2src/mm2_main/src/lp_swap/taker_swap.rs b/mm2src/mm2_main/src/lp_swap/taker_swap.rs index 4e087ad527..b12a752084 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_swap.rs @@ -2470,6 +2470,7 @@ pub async fn taker_swap_trade_preimage( coin: my_coin_ticker.to_owned(), amount: dex_amount.total_spend_amount(), paid_from_trading_vol: false, + tx_size: 0, }; let fee_to_send_taker_fee = my_coin 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 256503350b..b626d66c63 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs @@ -954,6 +954,7 @@ impl