diff --git a/mm2src/coins/lightning/ln_platform.rs b/mm2src/coins/lightning/ln_platform.rs index e7d57cb217..0d289aa8d0 100644 --- a/mm2src/coins/lightning/ln_platform.rs +++ b/mm2src/coins/lightning/ln_platform.rs @@ -568,7 +568,7 @@ impl FeeEstimator for Platform { ConfirmationTarget::Normal => self.confirmations_targets.normal, ConfirmationTarget::HighPriority => self.confirmations_targets.high_priority, }; - let fee_per_kb = tokio::task::block_in_place(move || { + let fee_rate = tokio::task::block_in_place(move || { block_on_f01(self.rpc_client().estimate_fee_sat( platform_coin.decimals(), // Todo: when implementing Native client detect_fee_method should be used for Native and @@ -582,16 +582,16 @@ impl FeeEstimator for Platform { // Set default fee to last known fee for the corresponding confirmation target match confirmation_target { - ConfirmationTarget::Background => self.latest_fees.set_background_fees(fee_per_kb), - ConfirmationTarget::Normal => self.latest_fees.set_normal_fees(fee_per_kb), - ConfirmationTarget::HighPriority => self.latest_fees.set_high_priority_fees(fee_per_kb), + ConfirmationTarget::Background => self.latest_fees.set_background_fees(fee_rate), + ConfirmationTarget::Normal => self.latest_fees.set_normal_fees(fee_rate), + ConfirmationTarget::HighPriority => self.latest_fees.set_high_priority_fees(fee_rate), }; // Must be no smaller than 253 (ie 1 satoshi-per-byte rounded up to ensure later round-downs don’t put us below 1 satoshi-per-byte). // https://docs.rs/lightning/0.0.101/lightning/chain/chaininterface/trait.FeeEstimator.html#tymethod.get_est_sat_per_1000_weight // This has changed in rust-lightning v0.0.110 as LDK currently wraps get_est_sat_per_1000_weight to ensure that the value returned is // no smaller than 253. https://github.com/lightningdevkit/rust-lightning/pull/1552 - (fee_per_kb as f64 / 4.0).ceil() as u32 + (fee_rate as f64 / 4.0).ceil() as u32 } } diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 785711e3f2..e1adf1cdad 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -2144,8 +2144,8 @@ pub trait MarketCoinOps { /// Is privacy coin like zcash or pirate fn is_privacy(&self) -> bool { false } - /// Is KMD coin - fn is_kmd(&self) -> bool { false } + /// Returns `true` for coins (like KMD) that should use direct DEX fee burning via OP_RETURN. + fn should_burn_directly(&self) -> bool { false } /// Should burn part of dex fee coin fn should_burn_dex_fee(&self) -> bool; @@ -3834,7 +3834,7 @@ impl DexFee { let dex_fee = trade_amount * &rate; let min_tx_amount = MmNumber::from(taker_coin.min_tx_amount()); - if taker_coin.is_kmd() { + if taker_coin.should_burn_directly() { // use a special dex fee option for kmd return Self::calc_dex_fee_for_op_return(dex_fee, min_tx_amount); } diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index 3049a56ec2..ac10ba3151 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -11,11 +11,10 @@ use crate::utxo::utxo_builder::{UtxoCoinBuildError, UtxoCoinBuildResult, UtxoCoi 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::{qtum, ActualFeeRate, AddrFromStrError, BroadcastTxErr, FeePolicy, GenerateTxError, GetUtxoListOps, + HistoryUtxoTx, HistoryUtxoTxMap, MatureUnspentList, RecentlySpentOutPointsGuard, UnsupportedAddr, + UtxoActivationParams, UtxoAddressFormat, UtxoCoinFields, UtxoCommonOps, UtxoFromLegacyReqErr, + UtxoTx, UtxoTxBroadcastOps, UtxoTxGenerationOps, VerboseTransactionFrom, UTXO_LOCK}; use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, ConfirmPaymentInput, DexFee, Eip1559Ops, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, IguanaPrivKey, MarketCoinOps, MmCoin, NegotiateSwapContractAddrErr, PrivKeyBuildPolicy, PrivKeyPolicyNotAllowed, RawTransactionFut, @@ -489,8 +488,8 @@ 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), + match try_s!(self.get_fee_rate().await) { + ActualFeeRate::Dynamic(amount) | ActualFeeRate::FixedPerKb(amount) => Ok(amount + gas_fee), } } @@ -545,10 +544,9 @@ impl Qrc20Coin { self.utxo.conf.fork_id, )?; - let miner_fee = data.fee_amount + data.unused_change; Ok(GenerateQrc20TxResult { signed, - miner_fee, + miner_fee: data.fee_amount, gas_fee, }) } @@ -609,17 +607,13 @@ 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_fee_rate(&self) -> UtxoRpcResult { utxo_common::get_fee_rate(&self.utxo).await } - async fn calc_interest_if_required( - &self, - unsigned: TransactionInputSigner, - data: AdditionalTxData, - my_script_pub: ScriptBytes, - dust: u64, - ) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)> { - utxo_common::calc_interest_if_required(self, unsigned, data, my_script_pub, dust).await + async fn calc_interest_if_required(&self, unsigned: &mut TransactionInputSigner) -> UtxoRpcResult { + utxo_common::calc_interest_if_required(self, unsigned).await } + + fn supports_interest(&self) -> bool { utxo_common::is_kmd(self) } } #[async_trait] diff --git a/mm2src/coins/qrc20/qrc20_tests.rs b/mm2src/coins/qrc20/qrc20_tests.rs index caf5546de2..01cf4adcad 100644 --- a/mm2src/coins/qrc20/qrc20_tests.rs +++ b/mm2src/coins/qrc20/qrc20_tests.rs @@ -59,8 +59,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: ActualFeeRate) { + let actual_tx_fee = block_on(coin.get_fee_rate()).unwrap(); assert_eq!(actual_tx_fee, expected_tx_fee); } @@ -712,7 +712,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, ActualFeeRate::FixedPerKb(EXPECTED_TX_FEE as u64)); let actual_trade_fee = block_on_f01(coin.get_trade_fee()).unwrap(); let expected_trade_fee_amount = big_decimal_from_sat( @@ -739,7 +739,7 @@ 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, ActualFeeRate::FixedPerKb(EXPECTED_TX_FEE as u64)); let allowance = block_on(coin.allowance(coin.swap_contract_address)).expect("!allowance"); assert_eq!(allowance, 0.into()); @@ -775,7 +775,7 @@ 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, ActualFeeRate::FixedPerKb(EXPECTED_TX_FEE as u64)); let allowance = block_on(coin.allowance(coin.swap_contract_address)).expect("!allowance"); assert_eq!(allowance, 300_000_000.into()); @@ -886,7 +886,7 @@ 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, ActualFeeRate::FixedPerKb(EXPECTED_TX_FEE as u64)); let actual = block_on_f01(coin.get_receiver_trade_fee(FeeApproxStage::WithoutApprox)).expect("!get_receiver_trade_fee"); @@ -911,7 +911,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, ActualFeeRate::FixedPerKb(EXPECTED_TX_FEE as u64)); let expected_balance = CoinBalance { spendable: BigDecimal::from(5u32), unspendable: BigDecimal::from(0u32), diff --git a/mm2src/coins/rpc_command/lightning/open_channel.rs b/mm2src/coins/rpc_command/lightning/open_channel.rs index fdb5b9caa9..095ee1c4ee 100644 --- a/mm2src/coins/rpc_command/lightning/open_channel.rs +++ b/mm2src/coins/rpc_command/lightning/open_channel.rs @@ -174,7 +174,7 @@ pub async fn open_channel(ctx: MmArc, req: OpenChannelRequest) -> OpenChannelRes .with_fee_policy(fee_policy); let fee = platform_coin - .get_tx_fee() + .get_fee_rate() .await .map_err(|e| OpenChannelError::RpcError(e.to_string()))?; tx_builder = tx_builder.with_fee(fee); diff --git a/mm2src/coins/test_coin.rs b/mm2src/coins/test_coin.rs index ce2511b5b0..278492638c 100644 --- a/mm2src/coins/test_coin.rs +++ b/mm2src/coins/test_coin.rs @@ -109,7 +109,7 @@ impl MarketCoinOps for TestCoin { fn min_trading_vol(&self) -> MmNumber { MmNumber::from("0.00777") } - fn is_kmd(&self) -> bool { &self.ticker == "KMD" } + fn should_burn_directly(&self) -> bool { &self.ticker == "KMD" } fn should_burn_dex_fee(&self) -> bool { false } diff --git a/mm2src/coins/utxo.rs b/mm2src/coins/utxo.rs index 35e7281d4e..6fd2f28fe4 100644 --- a/mm2src/coins/utxo.rs +++ b/mm2src/coins/utxo.rs @@ -260,29 +260,61 @@ pub struct AdditionalTxData { pub received_by_me: u64, pub spent_by_me: u64, pub fee_amount: u64, - pub unused_change: u64, pub kmd_rewards: Option, } /// The fee set from coins config #[derive(Debug)] -pub enum TxFee { +pub enum FeeRate { /// Tell the coin that it should request the fee from daemon RPC and calculate it relying on tx size Dynamic(EstimateFeeMethod), /// Tell the coin that it has fixed tx fee per kb. FixedPerKb(u64), } -/// The actual "runtime" fee that is received from RPC in case of dynamic calculation +/// The actual "runtime" tx fee rate (per kb) that is received from RPC in case of dynamic calculation +/// or fixed tx fee rate #[derive(Copy, Clone, Debug, PartialEq)] -pub enum ActualTxFee { +pub enum ActualFeeRate { /// 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. + /// Use specified fee amount per each 1 kb of transaction and also per each output less than the fee amount. /// Used by DOGE, but more coins might support it too. FixedPerKb(u64), } +impl ActualFeeRate { + fn get_tx_fee(&self, tx_size: u64) -> u64 { + match self { + ActualFeeRate::Dynamic(fee_rate) => (fee_rate * tx_size) / KILO_BYTE, + // return fee_rate here as swap spend transaction size is always less than 1 kb + ActualFeeRate::FixedPerKb(fee_rate) => { + let tx_size_kb = if tx_size % KILO_BYTE == 0 { + tx_size / KILO_BYTE + } else { + tx_size / KILO_BYTE + 1 + }; + fee_rate * tx_size_kb + }, + } + } + + /// Return extra tx fee for the change output as p2pkh + fn get_tx_fee_for_change(&self, tx_size: u64) -> u64 { + match self { + ActualFeeRate::Dynamic(fee_rate) => (*fee_rate * P2PKH_OUTPUT_LEN) / KILO_BYTE, + ActualFeeRate::FixedPerKb(fee_rate) => { + // take into account the change output if tx_size_kb(tx with change) > tx_size_kb(tx without change) + if tx_size % KILO_BYTE + P2PKH_OUTPUT_LEN > KILO_BYTE { + *fee_rate + } else { + 0 + } + }, + } + } +} + /// Fee policy applied on transaction creation pub enum FeePolicy { /// Send the exact amount specified in output(s), fee is added to spent input amount @@ -577,7 +609,7 @@ pub struct UtxoCoinFields { /// Emercoin has 6 /// Bitcoin Diamond has 7 pub decimals: u8, - pub tx_fee: TxFee, + pub tx_fee: FeeRate, /// Minimum transaction value at which the value is not less than fee pub dust_amount: u64, /// RPC client @@ -839,18 +871,15 @@ pub trait UtxoTxBroadcastOps { #[async_trait] #[cfg_attr(test, mockable)] pub trait UtxoTxGenerationOps { - async fn get_tx_fee(&self) -> UtxoRpcResult; + async fn get_fee_rate(&self) -> UtxoRpcResult; /// Calculates interest if the coin is KMD /// Adds the value to existing output to my_script_pub or creates additional interest output /// returns transaction and data as is if the coin is not KMD - async fn calc_interest_if_required( - &self, - mut unsigned: TransactionInputSigner, - mut data: AdditionalTxData, - my_script_pub: Bytes, - dust: u64, - ) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)>; + async fn calc_interest_if_required(&self, unsigned: &mut TransactionInputSigner) -> UtxoRpcResult; + + /// Returns `true` if this coin supports Komodo-style interest accrual; otherwise, returns `false`. + fn supports_interest(&self) -> bool; } /// The UTXO address balance scanner. @@ -1747,7 +1776,6 @@ where { let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err().await); let key_pair = try_tx_s!(coin.as_ref().priv_key_policy.activated_key_or_err()); - let mut builder = UtxoTxBuilder::new(coin) .await .add_available_inputs(unspents) diff --git a/mm2src/coins/utxo/bch.rs b/mm2src/coins/utxo/bch.rs index e38a59ce27..76a2f5d708 100644 --- a/mm2src/coins/utxo/bch.rs +++ b/mm2src/coins/utxo/bch.rs @@ -700,17 +700,13 @@ 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_fee_rate(&self) -> UtxoRpcResult { utxo_common::get_fee_rate(&self.utxo_arc).await } - async fn calc_interest_if_required( - &self, - unsigned: TransactionInputSigner, - data: AdditionalTxData, - my_script_pub: Bytes, - dust: u64, - ) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)> { - utxo_common::calc_interest_if_required(self, unsigned, data, my_script_pub, dust).await + async fn calc_interest_if_required(&self, unsigned: &mut TransactionInputSigner) -> UtxoRpcResult { + utxo_common::calc_interest_if_required(self, unsigned).await } + + fn supports_interest(&self) -> bool { utxo_common::is_kmd(self) } } #[async_trait] diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index 82da94209b..e3214552e6 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -313,17 +313,13 @@ 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_fee_rate(&self) -> UtxoRpcResult { utxo_common::get_fee_rate(&self.utxo_arc).await } - async fn calc_interest_if_required( - &self, - unsigned: TransactionInputSigner, - data: AdditionalTxData, - my_script_pub: Bytes, - dust: u64, - ) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)> { - utxo_common::calc_interest_if_required(self, unsigned, data, my_script_pub, dust).await + async fn calc_interest_if_required(&self, unsigned: &mut TransactionInputSigner) -> UtxoRpcResult { + utxo_common::calc_interest_if_required(self, unsigned).await } + + fn supports_interest(&self) -> bool { utxo_common::is_kmd(self) } } #[async_trait] diff --git a/mm2src/coins/utxo/qtum_delegation.rs b/mm2src/coins/utxo/qtum_delegation.rs index b83289d97c..3723109720 100644 --- a/mm2src/coins/utxo/qtum_delegation.rs +++ b/mm2src/coins/utxo/qtum_delegation.rs @@ -294,10 +294,9 @@ impl QtumCoin { let signed = sign_tx(unsigned, key_pair, utxo.conf.signature_version, utxo.conf.fork_id)?; - let miner_fee = data.fee_amount + data.unused_change; let generated_tx = GenerateQrc20TxResult { signed, - miner_fee, + miner_fee: data.fee_amount, gas_fee, }; diff --git a/mm2src/coins/utxo/slp.rs b/mm2src/coins/utxo/slp.rs index 1ca37e3eba..5229f07180 100644 --- a/mm2src/coins/utxo/slp.rs +++ b/mm2src/coins/utxo/slp.rs @@ -10,9 +10,9 @@ 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, - UtxoCommonOps, UtxoTx, UtxoTxBroadcastOps, UtxoTxGenerationOps}; +use crate::utxo::{generate_and_send_tx, sat_from_big_decimal, ActualFeeRate, BroadcastTxErr, FeePolicy, + GenerateTxError, RecentlySpentOutPointsGuard, UtxoCoinConf, UtxoCoinFields, UtxoCommonOps, UtxoTx, + UtxoTxBroadcastOps, UtxoTxGenerationOps}; use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, ConfirmPaymentInput, DerivationMethod, DexFee, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MarketCoinOps, MmCoin, NegotiateSwapContractAddrErr, NumConversError, PrivKeyPolicyNotAllowed, RawTransactionFut, RawTransactionRequest, RawTransactionResult, @@ -1073,19 +1073,13 @@ 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_fee_rate(&self) -> UtxoRpcResult { self.platform_coin.get_fee_rate().await } - async fn calc_interest_if_required( - &self, - unsigned: TransactionInputSigner, - data: AdditionalTxData, - my_script_pub: Bytes, - dust: u64, - ) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)> { - self.platform_coin - .calc_interest_if_required(unsigned, data, my_script_pub, dust) - .await + async fn calc_interest_if_required(&self, unsigned: &mut TransactionInputSigner) -> UtxoRpcResult { + self.platform_coin.calc_interest_if_required(unsigned).await } + + fn supports_interest(&self) -> bool { self.platform_coin.supports_interest() } } #[async_trait] @@ -1541,11 +1535,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(ActualFeeRate::FixedPerKb(fixed)) }, Some(WithdrawFee::UtxoPerKbyte { amount }) => { let dynamic = sat_from_big_decimal(&amount, platform_decimals)?; - tx_builder = tx_builder.with_fee(ActualTxFee::Dynamic(dynamic)); + tx_builder = tx_builder.with_fee(ActualFeeRate::Dynamic(dynamic)); }, Some(fee_policy) => { let error = format!( diff --git a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs index b916afc232..80fd41c3ee 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs @@ -5,7 +5,7 @@ use crate::utxo::rpc_clients::{ElectrumClient, ElectrumClientSettings, ElectrumC use crate::utxo::tx_cache::{UtxoVerboseCacheOps, UtxoVerboseCacheShared}; use crate::utxo::utxo_block_header_storage::BlockHeaderStorage; use crate::utxo::utxo_builder::utxo_conf_builder::{UtxoConfBuilder, UtxoConfError}; -use crate::utxo::{output_script, ElectrumBuilderArgs, RecentlySpentOutPoints, TxFee, UtxoCoinConf, UtxoCoinFields, +use crate::utxo::{output_script, ElectrumBuilderArgs, FeeRate, RecentlySpentOutPoints, UtxoCoinConf, UtxoCoinFields, UtxoHDWallet, UtxoRpcMode, UtxoSyncStatus, UtxoSyncStatusLoopHandle, UTXO_DUST_AMOUNT}; use crate::{BlockchainNetwork, CoinTransportMetrics, DerivationMethod, HistorySyncState, IguanaPrivKey, PrivKeyBuildPolicy, PrivKeyPolicy, PrivKeyPolicyNotAllowed, RpcClientType, @@ -467,9 +467,9 @@ pub trait UtxoCoinBuilderCommonOps { Ok(self.conf()["decimals"].as_u64().unwrap_or(8) as u8) } - async fn tx_fee(&self, rpc_client: &UtxoRpcClientEnum) -> UtxoCoinBuildResult { + async fn tx_fee(&self, rpc_client: &UtxoRpcClientEnum) -> UtxoCoinBuildResult { let tx_fee = match self.conf()["txfee"].as_u64() { - None => TxFee::FixedPerKb(1000), + None => FeeRate::FixedPerKb(1000), Some(0) => { let fee_method = match &rpc_client { UtxoRpcClientEnum::Electrum(_) => EstimateFeeMethod::Standard, @@ -479,9 +479,9 @@ pub trait UtxoCoinBuilderCommonOps { .await .map_to_mm(UtxoCoinBuildError::ErrorDetectingFeeMethod)?, }; - TxFee::Dynamic(fee_method) + FeeRate::Dynamic(fee_method) }, - Some(fee) => TxFee::FixedPerKb(fee), + Some(fee) => FeeRate::FixedPerKb(fee), }; Ok(tx_fee) } diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index db7ad1f65a..2e792f05ac 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -78,9 +78,9 @@ pub const DEFAULT_SWAP_VOUT: usize = 0; pub const DEFAULT_SWAP_VIN: usize = 0; const MIN_BTC_TRADING_VOL: &str = "0.00777"; -macro_rules! true_or { +macro_rules! return_err_if { ($cond: expr, $etype: expr) => { - if !$cond { + if $cond { return Err(MmError::new($etype)); } }; @@ -95,18 +95,18 @@ lazy_static! { pub const HISTORY_TOO_LARGE_ERR_CODE: i64 = -1; -pub async fn get_tx_fee(coin: &UtxoCoinFields) -> UtxoRpcResult { +pub async fn get_fee_rate(coin: &UtxoCoinFields) -> UtxoRpcResult { let conf = &coin.conf; match &coin.tx_fee { - TxFee::Dynamic(method) => { - let fee = coin + FeeRate::Dynamic(method) => { + let fee_rate = coin .rpc_client .estimate_fee_sat(coin.decimals, method, &conf.estimate_fee_mode, conf.estimate_fee_blocks) .compat() .await?; - Ok(ActualTxFee::Dynamic(fee)) + Ok(ActualFeeRate::Dynamic(fee_rate)) }, - TxFee::FixedPerKb(satoshis) => Ok(ActualTxFee::FixedPerKb(*satoshis)), + FeeRate::FixedPerKb(satoshis) => Ok(ActualFeeRate::FixedPerKb(*satoshis)), } } @@ -270,37 +270,22 @@ where pub fn derivation_method(coin: &UtxoCoinFields) -> &DerivationMethod { &coin.derivation_method } -/// returns the fee required to be paid for HTLC spend transaction +/// returns the tx fee required to be paid for HTLC spend transaction 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 + let fee_rate = coin.get_fee_rate().await?; + let fee_rate = match fee_rate { + ActualFeeRate::Dynamic(dynamic_fee_rate) => { + // increase dynamic fee for a chance if it grows in the swap + ActualFeeRate::Dynamic(increase_dynamic_fee_by_stage(coin, dynamic_fee_rate, stage)) }, + ActualFeeRate::FixedPerKb(_) => fee_rate, }; - 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)?; - if fee < relay_fee_sat { - fee = relay_fee_sat; - } - } - Ok(fee) + let min_relay_fee_rate = get_min_relay_rate(coin).await?; + Ok(get_tx_fee_with_relay_fee(&fee_rate, tx_size, min_relay_fee_rate)) } pub fn addresses_from_script(coin: &T, script: &Script) -> Result, String> { @@ -469,18 +454,21 @@ pub fn output_script_checked(coin: &UtxoCoinFields, addr: &Address) -> MmResult< pub struct UtxoTxBuilder<'a, T: AsRef + UtxoTxGenerationOps> { coin: &'a T, from: Option
, + /// The required inputs that *must* be added in the resulting tx + required_inputs: Vec, /// The available inputs that *can* be included in the resulting tx available_inputs: Vec, + outputs: Vec, fee_policy: FeePolicy, - fee: Option, + fee: Option, gas_fee: Option, tx: TransactionInputSigner, - change: u64, sum_inputs: u64, - sum_outputs_value: u64, - tx_fee: u64, - min_relay_fee: Option, + sum_outputs: u64, + tx_fee_needed: u64, + min_relay_fee_rate: Option, dust: Option, + interest: u64, } impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { @@ -489,16 +477,18 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { tx: coin.as_ref().transaction_preimage(), coin, from: coin.as_ref().derivation_method.single_addr().await, + required_inputs: vec![], available_inputs: vec![], + outputs: vec![], fee_policy: FeePolicy::SendExact, fee: None, gas_fee: None, - change: 0, sum_inputs: 0, - sum_outputs_value: 0, - tx_fee: 0, - min_relay_fee: None, + sum_outputs: 0, + tx_fee_needed: 0, + min_relay_fee_rate: None, dust: None, + interest: 0, } } @@ -513,14 +503,7 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { } pub fn add_required_inputs(mut self, inputs: impl IntoIterator) -> Self { - self.tx - .inputs - .extend(inputs.into_iter().map(|input| UnsignedTransactionInput { - previous_output: input.outpoint, - prev_script: input.script, - sequence: SEQUENCE_FINAL, - amount: input.value, - })); + self.required_inputs.extend(inputs); self } @@ -532,7 +515,7 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { } pub fn add_outputs(mut self, outputs: impl IntoIterator) -> Self { - self.tx.outputs.extend(outputs); + self.outputs.extend(outputs); self } @@ -541,7 +524,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: ActualFeeRate) -> Self { self.fee = Some(fee); self } @@ -554,75 +537,136 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { self } - /// Recalculates fee and checks whether transaction is complete (inputs collected cover the outputs) - fn update_fee_and_check_completeness( - &mut self, - from_addr_format: &UtxoAddressFormat, - actual_tx_fee: &ActualTxFee, - ) -> 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 + fn required_amount(&self) -> u64 { + let mut sum_output = self + .outputs + .iter() + .fold(0u64, |required, output| required + output.value); + match self.fee_policy { + FeePolicy::SendExact => { + sum_output += self.total_tx_fee_needed(); }, + FeePolicy::DeductFromOutput(_) => {}, }; + sum_output + } + fn add_tx_inputs(&mut self, amount: u64) -> u64 { + self.tx.inputs.clear(); + let mut total = 0u64; + for utxo in &self.required_inputs { + self.tx.inputs.push(UnsignedTransactionInput { + previous_output: utxo.outpoint, + prev_script: utxo.script.clone(), + sequence: SEQUENCE_FINAL, + amount: utxo.value, + }); + total += utxo.value; + } + for utxo in &self.available_inputs { + if total >= amount { + break; + } + self.tx.inputs.push(UnsignedTransactionInput { + previous_output: utxo.outpoint, + prev_script: utxo.script.clone(), + sequence: SEQUENCE_FINAL, + amount: utxo.value, + }); + total += utxo.value; + } + total + } + + fn add_tx_outputs(&mut self) -> u64 { + self.tx.outputs.clear(); + let mut total = 0u64; + for output in self.outputs.clone() { + total += output.value; + self.tx.outputs.push(output); + } + total + } + + fn make_kmd_rewards_data(coin: &T, interest: u64) -> Option { + let rewards_amount = big_decimal_from_sat_unsigned(interest, coin.as_ref().decimals); + if coin.supports_interest() { + Some(KmdRewardsDetails::claimed_by_me(rewards_amount)) + } else { + None + } + } + + /// Adds change output. + /// Returns change value and dust change + fn add_change(&mut self, change_script_pubkey: &Bytes) -> u64 { + let sum_output_with_fee = self.sum_outputs + self.total_tx_fee_needed(); + if self.sum_inputs < sum_output_with_fee { + return 0u64; + } + let change = self.sum_inputs + self.interest - sum_output_with_fee; + if change < self.dust() { + return 0u64; + }; + self.tx.outputs.push({ + TransactionOutput { + value: change, + script_pubkey: change_script_pubkey.clone(), + } + }); + change + } + + /// Recalculates tx fee for tx size. + /// If needed, checks if tx fee is not less than min relay tx fee + fn update_tx_fee(&mut self, from_addr_format: &UtxoAddressFormat, fee_rate: &ActualFeeRate) { + let transaction = UtxoTx::from(self.tx.clone()); + let v_size = tx_size_in_v_bytes(from_addr_format, &transaction) as u64; + self.tx_fee_needed = get_tx_fee_with_relay_fee(fee_rate, v_size, self.min_relay_fee_rate); + } + + /// Deduct tx fee from output if requested by fee_policy + fn deduct_txfee_from_output(&mut self) -> MmResult { match self.fee_policy { - FeePolicy::SendExact => { - let mut outputs_plus_fee = self.sum_outputs_value + self.tx_fee; - if self.sum_inputs >= outputs_plus_fee { - 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 Some(min_relay) = self.min_relay_fee { - if self.tx_fee < min_relay { - let fee_diff = min_relay - self.tx_fee; - outputs_plus_fee += fee_diff; - self.tx_fee += fee_diff; - } - } - self.sum_inputs >= outputs_plus_fee - } else { - false - } - }, - FeePolicy::DeductFromOutput(_) => { - 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 Some(min_relay) = self.min_relay_fee { - if self.tx_fee < min_relay { - self.tx_fee = min_relay; - } - } - true - } else { - false - } + FeePolicy::SendExact => Ok(0), + FeePolicy::DeductFromOutput(i) => { + let tx_fee = self.total_tx_fee_needed(); + let min_output = tx_fee + self.dust(); + let val = self.tx.outputs[i].value; + return_err_if!(val < min_output, GenerateTxError::DeductFeeFromOutputFailed { + output_idx: i, + output_value: val, + required: min_output, + }); + self.tx.outputs[i].value -= tx_fee; + Ok(tx_fee) }, } } + fn validate_not_dust(&self) -> MmResult<(), GenerateTxError> { + for output in self.outputs.iter() { + let script: Script = output.script_pubkey.clone().into(); + if script.opcodes().next() != Some(Ok(Opcode::OP_RETURN)) { + return_err_if!(output.value < self.dust(), GenerateTxError::OutputValueLessThanDust { + value: output.value, + dust: self.dust() + }); + } + } + Ok(()) + } + + fn sum_received_by_me(&self, change_script_pubkey: &Bytes) -> u64 { + self.tx.outputs.iter().fold(0u64, |received_by_me, output| { + if &output.script_pubkey == change_script_pubkey { + received_by_me + output.value + } else { + received_by_me + } + }) + } + fn dust(&self) -> u64 { match self.dust { Some(dust) => dust, @@ -630,143 +674,98 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { } } + fn total_tx_fee_needed(&self) -> u64 { self.tx_fee_needed + self.gas_fee.unwrap_or(0u64) } + + fn tx_fee_fact(&self) -> MmResult { + (self.sum_inputs + self.interest) + .checked_sub(self.gas_fee.unwrap_or_default()) + .or_mm_err(|| GenerateTxError::Internal("gas_fee underflow".to_owned()))? + .checked_sub(self.sum_outputs) + .or_mm_err(|| GenerateTxError::Internal("sum_outputs underflow".to_owned())) + } + /// Generates unsigned transaction (TransactionInputSigner) from specified utxos and outputs. /// sends the change (inputs amount - outputs amount) to the [`UtxoTxBuilder::from`] address. /// Also returns additional transaction data pub async fn build(mut self) -> GenerateTxResult { let coin = self.coin; - let dust: u64 = self.dust(); let from = self .from .clone() .or_mm_err(|| GenerateTxError::Internal("'from' address is not specified".to_owned()))?; let change_script_pubkey = output_script(&from).map(|script| script.to_bytes())?; - let actual_tx_fee = match self.fee { + let actual_fee_rate = match self.fee { Some(fee) => fee, - None => coin.get_tx_fee().await?, + None => coin.get_fee_rate().await?, }; - true_or!(!self.tx.outputs.is_empty(), GenerateTxError::EmptyOutputs); + return_err_if!(self.outputs.is_empty(), GenerateTxError::EmptyOutputs); - let mut received_by_me = 0; - for output in self.tx.outputs.iter() { - let script: Script = output.script_pubkey.clone().into(); - if script.opcodes().next() != Some(Ok(Opcode::OP_RETURN)) { - true_or!(output.value >= dust, GenerateTxError::OutputValueLessThanDust { - value: output.value, - dust - }); - } - self.sum_outputs_value += output.value; - if output.script_pubkey == change_script_pubkey { - received_by_me += output.value; - } - } - - if let Some(gas_fee) = self.gas_fee { - self.sum_outputs_value += gas_fee; - } + self.validate_not_dust()?; - true_or!( - !self.available_inputs.is_empty() || !self.tx.inputs.is_empty(), + return_err_if!( + self.available_inputs.is_empty() && self.tx.inputs.is_empty(), GenerateTxError::EmptyUtxoSet { - required: self.sum_outputs_value + required: self.required_amount() } ); - self.min_relay_fee = if coin.as_ref().conf.force_min_relay_fee { - let fee_dec = coin.as_ref().rpc_client.get_relay_fee().compat().await?; - let min_relay_fee = sat_from_big_decimal(&fee_dec, coin.as_ref().decimals)?; - Some(min_relay_fee) - } else { - None - }; - - // The function `update_fee_and_check_completeness` checks if the total value of the current inputs - // (added using add_required_inputs or directly) is enough to cover the transaction outputs and fees. - // If it returns `true`, it indicates that no additional inputs are needed from the available inputs, - // and we can skip the loop that adds these additional inputs. - if !self.update_fee_and_check_completeness(from.addr_format(), &actual_tx_fee) { - for utxo in self.available_inputs.clone() { - self.tx.inputs.push(UnsignedTransactionInput { - previous_output: utxo.outpoint, - prev_script: utxo.script, - sequence: SEQUENCE_FINAL, - amount: utxo.value, - }); - self.sum_inputs += utxo.value; + self.min_relay_fee_rate = get_min_relay_rate(coin).await?; - if self.update_fee_and_check_completeness(from.addr_format(), &actual_tx_fee) { - break; - } + let mut one_time_fee_update = false; + loop { + let required_amount_0 = self.required_amount(); + self.sum_inputs = self.add_tx_inputs(required_amount_0); + self.sum_outputs = self.add_tx_outputs(); + self.interest = coin.calc_interest_if_required(&mut self.tx).await?; + + // try once tx_fee without the change output (if maybe txfee fits between total inputs and outputs) + if !one_time_fee_update { + self.update_tx_fee(from.addr_format(), &actual_fee_rate); + one_time_fee_update = true; } - } - - match self.fee_policy { - FeePolicy::SendExact => self.sum_outputs_value += self.tx_fee, - FeePolicy::DeductFromOutput(i) => { - let min_output = self.tx_fee + dust; - let val = self.tx.outputs[i].value; - true_or!(val >= min_output, GenerateTxError::DeductFeeFromOutputFailed { - output_idx: i, - output_value: val, - required: min_output, - }); - self.tx.outputs[i].value -= self.tx_fee; - if self.tx.outputs[i].script_pubkey == change_script_pubkey { - received_by_me -= self.tx_fee; - } - }, - }; - true_or!( - self.sum_inputs >= self.sum_outputs_value, - GenerateTxError::NotEnoughUtxos { + return_err_if!(self.sum_inputs < required_amount_0, GenerateTxError::NotEnoughUtxos { sum_utxos: self.sum_inputs, - required: self.sum_outputs_value - } - ); - - let change = self.sum_inputs - self.sum_outputs_value; - let unused_change = if change > dust { - self.tx.outputs.push({ - TransactionOutput { - value: change, - script_pubkey: change_script_pubkey.clone(), - } + required: self.required_amount(), // send updated required amount, with txfee }); - received_by_me += change; - 0 - } else { - change - }; + + self.sum_outputs = self + .sum_outputs + .checked_sub(self.deduct_txfee_from_output()?) + .or_mm_err(|| GenerateTxError::Internal("sum_outputs underflow".to_owned()))?; + let change = self.add_change(&change_script_pubkey); + self.sum_outputs += change; + self.update_tx_fee(from.addr_format(), &actual_fee_rate); // recalculate txfee with the change output, if added + if self.sum_inputs + self.interest >= self.sum_outputs + self.total_tx_fee_needed() { + break; + } + } let data = AdditionalTxData { - fee_amount: self.tx_fee, - received_by_me, + fee_amount: self.tx_fee_fact()?, // we return only txfee here (w/o gas_fee) + received_by_me: self.sum_received_by_me(&change_script_pubkey), spent_by_me: self.sum_inputs, - unused_change, // will be changed if the ticker is KMD - kmd_rewards: None, + kmd_rewards: Self::make_kmd_rewards_data(coin, self.interest), }; - Ok(coin - .calc_interest_if_required(self.tx, data, change_script_pubkey, dust) - .await?) + Ok((self.tx, data)) } /// Generates unsigned transaction (TransactionInputSigner) from specified utxos and outputs. /// Adds or updates inputs with UnspentInfo /// Does not do any checks or add any outputs pub async fn build_unchecked(mut self) -> Result> { + self.sum_outputs = 0u64; for output in self.tx.outputs.iter() { - self.sum_outputs_value += output.value; + self.sum_outputs += output.value; } - true_or!( - !self.available_inputs.is_empty() || !self.tx.inputs.is_empty(), + return_err_if!( + self.available_inputs.is_empty() && self.tx.inputs.is_empty(), GenerateTxError::EmptyUtxoSet { - required: self.sum_outputs_value + required: self.sum_outputs } ); @@ -803,61 +802,62 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { /// returns transaction and data as is if the coin is not KMD pub async fn calc_interest_if_required( coin: &T, - mut unsigned: TransactionInputSigner, - mut data: AdditionalTxData, - my_script_pub: Bytes, - dust: u64, -) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)> { - if coin.as_ref().conf.ticker != "KMD" { - return Ok((unsigned, data)); + unsigned: &mut TransactionInputSigner, +) -> UtxoRpcResult { + if !coin.supports_interest() { + return Ok(0); } unsigned.lock_time = coin.get_current_mtp().await?; let mut interest = 0; + let prev_hashes = unsigned + .inputs + .iter() + .map(|input| input.previous_output.hash.reversed().into()) + .collect::>(); + let prev_txns = get_verbose_transactions_from_cache_or_rpc(coin.as_ref(), prev_hashes).await?; for input in unsigned.inputs.iter() { let prev_hash = input.previous_output.hash.reversed().into(); - let tx = coin - .as_ref() - .rpc_client - .get_verbose_transaction(&prev_hash) - .compat() - .await?; + let tx = prev_txns + .get(&prev_hash) + .ok_or(MmError::new(UtxoRpcError::Internal("previous tx not found".to_owned())))? + .to_inner(); if let Ok(output_interest) = kmd_interest(tx.height, input.amount, tx.locktime as u64, unsigned.lock_time as u64) { interest += output_interest; }; } - if interest > 0 { - data.received_by_me += interest; - let mut output_to_me = unsigned - .outputs - .iter_mut() - .find(|out| out.script_pubkey == my_script_pub); - // add calculated interest to existing output to my address - // or create the new one if it's not found - match output_to_me { - Some(ref mut output) => output.value += interest, - None => { - let maybe_change_output_value = interest + data.unused_change; - if maybe_change_output_value > dust { - let change_output = TransactionOutput { - script_pubkey: my_script_pub, - value: maybe_change_output_value, - }; - unsigned.outputs.push(change_output); - data.unused_change = 0; - } else { - data.unused_change += interest; - } - }, - }; - } else { + if interest == 0 { // if interest is zero attempt to set the lowest possible lock_time to claim it later unsigned.lock_time = now_sec_u32() - 3600 + 777 * 2; } - 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)) + Ok(interest) +} + +pub fn is_kmd(coin: &T) -> bool { &coin.as_ref().conf.ticker == "KMD" } + +/// Helper to get min relay fee rate and convert to sat +async fn get_min_relay_rate + UtxoTxGenerationOps>(coin: &T) -> UtxoRpcResult> { + if coin.as_ref().conf.force_min_relay_fee { + let fee_dec = coin.as_ref().rpc_client.get_relay_fee().compat().await?; + let min_relay_fee_rate = sat_from_big_decimal(&fee_dec, coin.as_ref().decimals)?; + Ok(Some(min_relay_fee_rate)) + } else { + Ok(None) + } +} + +/// Helper to get tx fee if min relay rate is known +fn get_tx_fee_with_relay_fee(fee_rate: &ActualFeeRate, tx_size: u64, min_relay_fee_rate: Option) -> u64 { + let tx_fee = fee_rate.get_tx_fee(tx_size); + if let Some(min_relay_fee_rate) = min_relay_fee_rate { + let min_relay_dynamic_fee_rate = ActualFeeRate::Dynamic(min_relay_fee_rate); + let min_relay_tx_fee = min_relay_dynamic_fee_rate.get_tx_fee(tx_size); + if tx_fee < min_relay_tx_fee { + return min_relay_tx_fee; + } + } + tx_fee } pub struct P2SHSpendingTxInput<'a> { @@ -3900,7 +3900,7 @@ pub async fn calc_interest_of_tx( tx: &UtxoTx, input_transactions: &mut HistoryUtxoTxMap, ) -> UtxoRpcResult { - if coin.as_ref().conf.ticker != "KMD" { + if !coin.supports_interest() { let error = format!("Expected KMD ticker, found {}", coin.as_ref().conf.ticker); return MmError::err(UtxoRpcError::Internal(error)); } @@ -3935,10 +3935,10 @@ pub fn get_trade_fee(coin: T) -> Box f, - ActualTxFee::FixedPerKb(f) => f, + ActualFeeRate::Dynamic(f) => f, + ActualFeeRate::FixedPerKb(f) => f, }; Ok(TradeFee { coin: ticker, @@ -3973,28 +3973,27 @@ where T: UtxoCommonOps + GetUtxoListOps, { let decimals = coin.as_ref().decimals; - let tx_fee = coin.get_tx_fee().await?; + let fee_rate = coin.get_fee_rate().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 { + match fee_rate { // 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); + ActualFeeRate::Dynamic(fee_rate) => { + // take into account that the dynamic tx fee rate may increase during the swap + let dynamic_fee_rate = coin.increase_dynamic_fee_by_stage(fee_rate, stage); let outputs_count = outputs.len(); let (unspents, _recently_sent_txs) = coin.get_unspent_ordered_list(&my_address).await?; - let actual_tx_fee = ActualTxFee::Dynamic(dynamic_fee); - + let actual_fee_rate = ActualFeeRate::Dynamic(dynamic_fee_rate); 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); + .with_fee(actual_fee_rate); if let Some(gas) = gas_fee { tx_builder = tx_builder.with_gas_fee(gas); } @@ -4002,26 +4001,26 @@ where TradePreimageError::from_generate_tx_error(e, ticker.to_owned(), decimals, is_amount_upper_bound) })?; + // We need to add extra tx fee for the absent change output for e.g. to ensure max_taker_vol is calculated correctly + // (If we do not do this then in a swap the change output may appear and we may not have sufficient balance to pay taker fee) 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 + data.fee_amount + actual_fee_rate.get_tx_fee_for_change(0) } else { // the change output is included already data.fee_amount }; - Ok(big_decimal_from_sat(total_fee as i64, decimals)) }, - ActualTxFee::FixedPerKb(fee) => { + ActualFeeRate::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); + .with_fee(fee_rate); if let Some(gas) = gas_fee { tx_builder = tx_builder.with_gas_fee(gas); } @@ -4029,20 +4028,17 @@ where TradePreimageError::from_generate_tx_error(e, ticker.to_string(), decimals, is_amount_upper_bound) })?; + // We need to add extra tx fee for the absent change output for e.g. to ensure max_taker_vol is calculated correctly + // (If we do not do this then in a swap the change output may appear and we may not have sufficient balance to pay taker fee) 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 - } + // take into account the change output + data.fee_amount + fee_rate.get_tx_fee_for_change(tx_bytes.len() as u64) } else { // the change output is included already data.fee_amount }; - Ok(big_decimal_from_sat(total_fee as i64, decimals)) }, } diff --git a/mm2src/coins/utxo/utxo_common_tests.rs b/mm2src/coins/utxo/utxo_common_tests.rs index d432207d8e..4203f4d8ba 100644 --- a/mm2src/coins/utxo/utxo_common_tests.rs +++ b/mm2src/coins/utxo/utxo_common_tests.rs @@ -136,7 +136,7 @@ pub(super) fn utxo_coin_fields_for_test( }, decimals: TEST_COIN_DECIMALS, dust_amount: UTXO_DUST_AMOUNT, - tx_fee: TxFee::FixedPerKb(1000), + tx_fee: FeeRate::FixedPerKb(1000), rpc_client, priv_key_policy, derivation_method, diff --git a/mm2src/coins/utxo/utxo_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index 99a97d846f..d8191dfef6 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -110,17 +110,13 @@ 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_fee_rate(&self) -> UtxoRpcResult { utxo_common::get_fee_rate(&self.utxo_arc).await } - async fn calc_interest_if_required( - &self, - unsigned: TransactionInputSigner, - data: AdditionalTxData, - my_script_pub: Bytes, - dust: u64, - ) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)> { - utxo_common::calc_interest_if_required(self, unsigned, data, my_script_pub, dust).await + async fn calc_interest_if_required(&self, unsigned: &mut TransactionInputSigner) -> UtxoRpcResult { + utxo_common::calc_interest_if_required(self, unsigned).await } + + fn supports_interest(&self) -> bool { utxo_common::is_kmd(self) } } #[async_trait] @@ -915,7 +911,7 @@ impl MarketCoinOps for UtxoStandardCoin { fn min_trading_vol(&self) -> MmNumber { utxo_common::min_trading_vol(self.as_ref()) } - fn is_kmd(&self) -> bool { &self.utxo_arc.conf.ticker == "KMD" } + fn should_burn_directly(&self) -> bool { &self.utxo_arc.conf.ticker == "KMD" } fn should_burn_dex_fee(&self) -> bool { utxo_common::should_burn_dex_fee() } diff --git a/mm2src/coins/utxo/utxo_tests.rs b/mm2src/coins/utxo/utxo_tests.rs index 7b9d39d38b..16831a0579 100644 --- a/mm2src/coins/utxo/utxo_tests.rs +++ b/mm2src/coins/utxo/utxo_tests.rs @@ -50,6 +50,7 @@ use mm2_test_helpers::electrums::doc_electrums; use mm2_test_helpers::for_tests::{electrum_servers_rpc, mm_ctx_with_custom_db, DOC_ELECTRUM_ADDRS, MARTY_ELECTRUM_ADDRS, T_BCH_ELECTRUMS}; use mocktopus::mocking::*; +use rand::{rngs::ThreadRng, Rng}; use rpc::v1::types::H256 as H256Json; use serialization::{deserialize, CoinVariant, CompactInteger, Reader}; use spv_validation::conf::{BlockHeaderValidationParams, SPVBlockHeader}; @@ -227,8 +228,7 @@ fn test_generate_transaction() { // 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.unused_change, 999); + assert_eq!(generated.1.fee_amount, 1000 + 999); assert_eq!(generated.1.received_by_me, 0); assert_eq!(generated.1.spent_by_me, 100000); @@ -255,7 +255,6 @@ fn test_generate_transaction() { assert_eq!(generated.0.outputs.len(), 1); assert_eq!(generated.1.fee_amount, 1000); - assert_eq!(generated.1.unused_change, 0); assert_eq!(generated.1.received_by_me, 99000); assert_eq!(generated.1.spent_by_me, 100000); assert_eq!(generated.0.outputs[0].value, 99000); @@ -710,9 +709,9 @@ fn test_withdraw_impl_sat_per_kb_fee_amount_equal_to_max() { ..Default::default() }; let tx_details = block_on_f01(coin.withdraw(withdraw_req)).unwrap(); - // The resulting transaction size might be 210 or 211 bytes depending on signature size - // MM2 always expects the worst case during fee calculation - // 0.1 * 211 / 1000 = 0.0211 + // The resulting transaction size might be 210 or 211 bytes (no change output) depending on signature size + // MM2 always expects the worst case during fee calculation: + // tx_fee = 0.1 * 211 / 1000 = 0.0211 let expected_fee = Some( UtxoFeeDetails { coin: Some(TEST_COIN_NAME.into()), @@ -895,10 +894,10 @@ fn test_withdraw_kmd_rewards_impl( }); UtxoStandardCoin::get_current_mtp .mock_safe(move |_fields| MockResult::Return(Box::pin(futures::future::ok(current_mtp)))); - NativeClient::get_verbose_transaction.mock_safe(move |_coin, txid| { - let expected: H256Json = <[u8; 32]>::from_hex(tx_hash).unwrap().into(); - assert_eq!(*txid, expected); - MockResult::Return(Box::new(futures01::future::ok(verbose.clone()))) + NativeClient::get_verbose_transactions.mock_safe(move |_coin, txids| { + let expected = <[u8; 32]>::from_hex(tx_hash).unwrap().into(); + assert_eq!(txids, &[expected]); + MockResult::Return(Box::new(futures01::future::ok([verbose.clone()].into()))) }); let client = NativeClient(Arc::new(NativeClientImpl::default())); @@ -1197,19 +1196,162 @@ 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(ActualFeeRate::Dynamic(100)); let generated = block_on(builder.build()).unwrap(); - assert_eq!(generated.0.outputs.len(), 1); + assert_eq!(generated.0.outputs.len(), 2); // generated transaction fee must be equal to relay fee if calculated dynamic fee is lower than relay - assert_eq!(generated.1.fee_amount, 100000000); - assert_eq!(generated.1.unused_change, 0); - assert_eq!(generated.1.received_by_me, 0); + assert_eq!(generated.1.fee_amount, 22000000); + assert_eq!(generated.1.received_by_me, 78000000); assert_eq!(generated.1.spent_by_me, 1000000000); assert!(unsafe { GET_RELAY_FEE_CALLED }); } +/// Test the transaction builder calculations (with random generated values) +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_generate_transaction_random_values() { + let client = NativeClientImpl::default(); + let mut rng = rand::thread_rng(); + + // tx_size for zcash, no shielded + let est_tx_size = |n_inputs: usize, n_outputs: usize| { + 4 + 4 + + 1 + + (n_inputs as u64) * (1 + 1 + 72 + 1 + 33 + 32 + 4 + 4) + + 1 + + (n_outputs as u64) * (1 + 25 + 8) + + 4 + + 4 + + 8 + + 1 + + 1 + + 1 + }; + + let make_random_vec_u64 = |rng: &mut ThreadRng, max_size: usize, max_value: u64| { + let vsize = rng.gen_range(1, max_size); + let mut v = vec![]; + for _i in 0..vsize { + v.push(rng.gen_range(0, max_value)) + } + v + }; + + NativeClient::get_relay_fee + .mock_safe(|_| MockResult::Return(Box::new(futures01::future::ok("0.0".parse().unwrap())))); + let client = UtxoRpcClientEnum::Native(NativeClient(Arc::new(client))); + let mut coin = utxo_coin_fields_for_test(client, None, false); + coin.conf.force_min_relay_fee = false; + let coin = utxo_coin_from_fields(coin); + + for _i in 0..9999 { + let input_vals = make_random_vec_u64(&mut rng, 100, 100_000); + let output_vals = make_random_vec_u64(&mut rng, 100, 100_000); + let dust = rng.gen_range(0, 1000); + let fee_rate = rng.gen_range(0, 1000); + + let mut total_inputs = 0_u64; + let mut unspents = vec![]; + for val in &input_vals { + unspents.push(UnspentInfo { + value: *val, + outpoint: OutPoint::default(), + height: Default::default(), + script: Vec::new().into(), + }); + total_inputs += *val; + } + + let mut has_dust_output = false; + let mut outputs = vec![]; + let mut total_outputs = 0_u64; + for val in &output_vals { + outputs.push(TransactionOutput { + script_pubkey: "76a914124b0846223ef78130b8e544b9afc3b09988238688ac".into(), + value: *val, + }); + if *val < dust { + has_dust_output = true; + } + total_outputs += *val; + } + + let builder = block_on(UtxoTxBuilder::new(&coin)) + .add_available_inputs(unspents) + .add_outputs(outputs.clone()) + .with_dust(dust) + .with_fee(ActualFeeRate::Dynamic(fee_rate)); + + let result = block_on(builder.build()); + if has_dust_output { + let is_err_dust = matches!( + result.unwrap_err().get_inner(), + GenerateTxError::OutputValueLessThanDust { value: _, dust: _ } + ); + assert!(is_err_dust); + continue; + } + if let Err(ref err) = result { + let tx_size_max = est_tx_size(input_vals.len(), output_vals.len() + 1); + let tx_fee_max = fee_rate * tx_size_max / 1000; + if matches!(err.get_inner(), GenerateTxError::NotEnoughUtxos { + sum_utxos: _, + required: _ + }) { + assert!(total_inputs < total_outputs + tx_fee_max); + continue; + } + panic!("unexpected utxo builder err"); + } + + let generated = result.unwrap(); + + // generated transaction has no change output but dust + assert!(generated.0.outputs.len() >= output_vals.len() && generated.0.outputs.len() <= output_vals.len() + 1); + let fact_inputs = generated.0.inputs.iter().fold(0u64, |acc, input| acc + input.amount); + // total w/o change: + let fact_outputs_no_change = generated + .0 + .outputs + .iter() + .take(output_vals.len()) + .fold(0u64, |acc, output| acc + output.value); + + assert_eq!(generated.1.spent_by_me, fact_inputs); + + assert_eq!(total_outputs, fact_outputs_no_change); + + assert_eq!( + generated.1.spent_by_me, + generated.1.fee_amount + generated.1.received_by_me + total_outputs + ); + + let tx_size = est_tx_size(generated.0.inputs.len(), generated.0.outputs.len()); + let estimated_txfee = fee_rate * tx_size / 1000; + //println!("generated.1.fee_amount={} estimated_txfee={} received_by_me={} output_vals.len={} generated.0.outputs.len={} dust={}, fee_rate={}", + // generated.1.fee_amount, estimated_txfee, generated.1.received_by_me, output_vals.len(), generated.0.outputs.len(), dust, fee_rate); + + const CHANGE_OUTPUT_SIZE: u64 = 1 + 25 + 8; + let max_overpay = dust + fee_rate * CHANGE_OUTPUT_SIZE / 1000; // could be slight overpay due to dust change removed from tx + if generated.1.fee_amount > estimated_txfee { + println!( + "overpay detected: generated.1.fee_amount={} estimated_txfee={}", + generated.1.fee_amount, estimated_txfee + ); + } + assert!(generated.1.fee_amount >= estimated_txfee && generated.1.fee_amount <= estimated_txfee + max_overpay); + + let received_by_me = if generated.0.outputs.len() > output_vals.len() { + generated.0.outputs.last().unwrap().value + } else { + 0u64 + }; + assert_eq!(generated.1.received_by_me, received_by_me); + } +} + #[test] #[cfg(not(target_arch = "wasm32"))] // https://github.com/KomodoPlatform/atomicDEX-API/issues/1037 @@ -1241,16 +1383,15 @@ 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(ActualFeeRate::Dynamic(100)); let generated = block_on(tx_builder.build()).unwrap(); assert_eq!(generated.0.outputs.len(), 1); - // `output (= 10.0) - fee_amount (= 1.0)` - assert_eq!(generated.0.outputs[0].value, 900000000); + // `output (= 10.0) - tx_fee (= 186 byte * 100000000 / 1000)` + assert_eq!(generated.0.outputs[0].value, 981400000); - // generated transaction fee must be equal to relay fee if calculated dynamic fee is lower than relay - assert_eq!(generated.1.fee_amount, 100000000); - assert_eq!(generated.1.unused_change, 0); + // generated transaction fee must be equal to relay fee if calculated dynamic fee is lower than relay fee + assert_eq!(generated.1.fee_amount, 18600000); assert_eq!(generated.1.received_by_me, 0); assert_eq!(generated.1.spent_by_me, 1000000000); assert!(unsafe { GET_RELAY_FEE_CALLED }); @@ -1289,7 +1430,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(ActualFeeRate::Dynamic(1000)); let generated = block_on(builder.build()).unwrap(); @@ -1298,7 +1439,6 @@ fn test_generate_tx_fee_is_correct_when_dynamic_fee_is_larger_than_relay() { // resulting signed transaction size would be 3032 bytes so fee is 3032 sat assert_eq!(generated.1.fee_amount, 3032); - assert_eq!(generated.1.unused_change, 0); assert_eq!(generated.1.received_by_me, 999996968); assert_eq!(generated.1.spent_by_me, 20000000000); assert!(unsafe { GET_RELAY_FEE_CALLED }); @@ -2633,7 +2773,7 @@ fn test_get_sender_trade_fee_dynamic_tx_fee() { Some("bob passphrase max taker vol with dynamic trade fee"), false, ); - coin_fields.tx_fee = TxFee::Dynamic(EstimateFeeMethod::Standard); + coin_fields.tx_fee = FeeRate::Dynamic(EstimateFeeMethod::Standard); let coin = utxo_coin_from_fields(coin_fields); let my_balance = block_on_f01(coin.my_spendable_balance()).expect("!my_balance"); let expected_balance = BigDecimal::from_str("2.22222").expect("!BigDecimal::from_str"); @@ -3673,6 +3813,365 @@ fn test_split_qtum() { log!("Res = {:?}", res); } +#[test] +fn test_raven_low_tx_fee_okay() { + let config = json!({ + "coin": "RVN", + "name": "raven", + "fname": "RavenCoin", + "sign_message_prefix": "Raven Signed Message:\n", + "rpcport": 8766, + "pubtype": 60, + "p2shtype": 122, + "wiftype": 128, + "segwit": true, + "txfee": 1000000, + "mm2": 1, + "required_confirmations": 3, + "avg_blocktime": 60, + "protocol": { + "type": "UTXO" + }, + "derivation_path": "m/44'/175'", + "trezor_coin": "Ravencoin", + "links": { + "github": "https://github.com/RavenProject/Ravencoin", + "homepage": "https://ravencoin.org" + } + }); + let request = json!({ + "method": "electrum", + "coin": "RVN", + "servers": [{"url": "electrum1.cipig.net:10060"},{"url": "electrum2.cipig.net:10060"},{"url": "electrum3.cipig.net:10060"}], + }); + let ctx = MmCtxBuilder::default().into_mm_arc(); + let params = UtxoActivationParams::from_legacy_req(&request).unwrap(); + + let priv_key = Secp256k1Secret::from([1; 32]); + let raven = block_on(utxo_standard_coin_with_priv_key( + &ctx, "RVN", &config, ¶ms, priv_key, + )) + .unwrap(); + + let unspents = vec![ + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("be3f13e94d4c58293c2fbee40dd70714c3f833a10ab05b6a328b558bb72c38a7").unwrap(), + index: 2, + }, + value: 10618039482, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("2f2eb00dad863079fc20f0c5356bb72e18f3346c126cc3f2e3654360af930f85").unwrap(), + index: 0, + }, + value: 15105673480, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("4a806e97f1fa33439d58ce5fad32c5be1e1f1a59d742050a42f237b33f2196ab").unwrap(), + index: 0, + }, + value: 15376032861, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("c0f855886343247051bb42b39f75ff35690ad0fb67a08dba5e9f8b680f6fecf3").unwrap(), + index: 0, + }, + value: 29999000000, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("0e75a62d6bb49c6312a5a1f3635d4bfc39c3d1549a35dc07b253ec1b1dd3b835").unwrap(), + index: 0, + }, + value: 31916552049, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("921554ccd2e50729b521422d3ad22ae00b5721f888e35fca8d2c8ee7a7506490").unwrap(), + index: 0, + }, + value: 33542311009, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("9df4256f2e3d0a65745402e7233f309767a2a629755cb3841ff0f47ce90553be").unwrap(), + index: 0, + }, + value: 35133858231, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("bf3e69728fa9a41ab06da0e595da63bc0fbe055c04f0e7125c320b3255067a3b").unwrap(), + index: 0, + }, + value: 46177879500, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("c62efa3598fec9332746d0657b7bd2a1974efe637da549ddeb84c952535e214b").unwrap(), + index: 2, + }, + value: 155455117689, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("9b676bc6a81e4e801a37b48f11f3834c0b1fd49ff420e104563e0895f0517946").unwrap(), + index: 2, + }, + value: 251289432230, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("210525a94adc033a745bfae158d931464a720b60bd708d00415fa38d7aa1bed1").unwrap(), + index: 0, + }, + value: 260317094896, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("d78d731e8dfc9fc1591da45da7622b13a3e395a73fd3178e6b832cd30436ed14").unwrap(), + index: 0, + }, + value: 460964136766, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("02143bce641ef1f70354085dfdff6f1031db019df561aa09b06835fbcf41b8a4").unwrap(), + index: 0, + }, + value: 515274184960, + height: None, + script: Vec::new().into(), + }, + ]; + let outputs = vec![ + TransactionOutput { + value: 1742160278745, + script_pubkey: "a9147484c59a11d053535314d5a1047005952f7fdf1e87".into(), + }, + TransactionOutput { + value: 0, + script_pubkey: "6a140e7d2af72dc4363283f4b50e1cfe6775a1ad81c1".into(), + }, + TransactionOutput { + value: 119006034408, + script_pubkey: "76a914124b0846223ef78130b8e544b9afc3b09988238688ac".into(), + }, + ]; + let builder = block_on(UtxoTxBuilder::new(&raven)) + .add_available_inputs(unspents) + .add_outputs(outputs); + let (_, data) = block_on(builder.build()).unwrap(); + let expected_fee = 3000000; + assert_eq!(expected_fee, data.fee_amount); +} + +/// Test to validate fix for https://github.com/KomodoPlatform/komodo-defi-framework/issues/2313 +#[test] +fn test_raven_low_tx_fee_error() { + let config = json!({ + "coin": "RVN", + "name": "raven", + "fname": "RavenCoin", + "sign_message_prefix": "Raven Signed Message:\n", + "rpcport": 8766, + "pubtype": 60, + "p2shtype": 122, + "wiftype": 128, + "segwit": true, + "txfee": 1000000, + "mm2": 1, + "required_confirmations": 3, + "avg_blocktime": 60, + "protocol": { + "type": "UTXO" + }, + "derivation_path": "m/44'/175'", + "trezor_coin": "Ravencoin", + "links": { + "github": "https://github.com/RavenProject/Ravencoin", + "homepage": "https://ravencoin.org" + } + }); + let request = json!({ + "method": "electrum", + "coin": "RVN", + "servers": [{"url": "electrum1.cipig.net:10060"},{"url": "electrum2.cipig.net:10060"},{"url": "electrum3.cipig.net:10060"}], + }); + let ctx = MmCtxBuilder::default().into_mm_arc(); + let params = UtxoActivationParams::from_legacy_req(&request).unwrap(); + + let priv_key = Secp256k1Secret::from([1; 32]); + let raven = block_on(utxo_standard_coin_with_priv_key( + &ctx, "RVN", &config, ¶ms, priv_key, + )) + .unwrap(); + + let unspents = vec![ + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("fde4ef4f23edc53085460559702783f7128d4b9bacd6898ffae2234576e7feb9").unwrap(), + index: 2, + }, + value: 11014394719, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("2f2eb00dad863079fc20f0c5356bb72e18f3346c126cc3f2e3654360af930f85").unwrap(), + index: 0, + }, + value: 15105673480, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("4a806e97f1fa33439d58ce5fad32c5be1e1f1a59d742050a42f237b33f2196ab").unwrap(), + index: 0, + }, + value: 15376032861, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("c0f855886343247051bb42b39f75ff35690ad0fb67a08dba5e9f8b680f6fecf3").unwrap(), + index: 0, + }, + value: 29999000000, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("0e75a62d6bb49c6312a5a1f3635d4bfc39c3d1549a35dc07b253ec1b1dd3b835").unwrap(), + index: 0, + }, + value: 31916552049, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("921554ccd2e50729b521422d3ad22ae00b5721f888e35fca8d2c8ee7a7506490").unwrap(), + index: 0, + }, + value: 33542311009, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("9df4256f2e3d0a65745402e7233f309767a2a629755cb3841ff0f47ce90553be").unwrap(), + index: 0, + }, + value: 35133858231, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("bf3e69728fa9a41ab06da0e595da63bc0fbe055c04f0e7125c320b3255067a3b").unwrap(), + index: 0, + }, + value: 46177879500, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("c62efa3598fec9332746d0657b7bd2a1974efe637da549ddeb84c952535e214b").unwrap(), + index: 2, + }, + value: 155455117689, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("9b676bc6a81e4e801a37b48f11f3834c0b1fd49ff420e104563e0895f0517946").unwrap(), + index: 2, + }, + value: 251289432230, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("210525a94adc033a745bfae158d931464a720b60bd708d00415fa38d7aa1bed1").unwrap(), + index: 0, + }, + value: 260317094896, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("d78d731e8dfc9fc1591da45da7622b13a3e395a73fd3178e6b832cd30436ed14").unwrap(), + index: 0, + }, + value: 460964136766, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("02143bce641ef1f70354085dfdff6f1031db019df561aa09b06835fbcf41b8a4").unwrap(), + index: 0, + }, + value: 515274184960, + height: None, + script: Vec::new().into(), + }, + ]; + let outputs = vec![ + TransactionOutput { + value: 1752628943415, + script_pubkey: "a91417ad3c3cd6e32aede379ac0efa42e310ba30b81d87".into(), + }, + TransactionOutput { + value: 0, + script_pubkey: "6a145786f27ae947255c21e47a3d3fe0d4e132f34e6c".into(), + }, + ]; + let builder = block_on(UtxoTxBuilder::new(&raven)) + .add_available_inputs(unspents) + .add_outputs(outputs); + let (_, data) = block_on(builder.build()).unwrap(); + let expected_fee = 3000000; + assert_eq!(expected_fee, data.fee_amount); +} + /// `QtumCoin` hasn't to check UTXO maturity if `check_utxo_maturity` is `false`. /// https://github.com/KomodoPlatform/atomicDEX-API/issues/1181 #[test] diff --git a/mm2src/coins/utxo/utxo_withdraw.rs b/mm2src/coins/utxo/utxo_withdraw.rs index 29ef2f5963..353ac230d7 100644 --- a/mm2src/coins/utxo/utxo_withdraw.rs +++ b/mm2src/coins/utxo/utxo_withdraw.rs @@ -1,7 +1,7 @@ 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, - UtxoAddressFormat, UtxoCoinFields, UtxoCommonOps, UtxoFeeDetails, UtxoTx, UTXO_LOCK}; +use crate::utxo::{output_script, sat_from_big_decimal, ActualFeeRate, Address, FeePolicy, GetUtxoListOps, + PrivKeyPolicy, UtxoAddressFormat, UtxoCoinFields, UtxoCommonOps, UtxoFeeDetails, UtxoTx, UTXO_LOCK}; use crate::{CoinWithDerivationMethod, GetWithdrawSenderAddress, MarketCoinOps, TransactionData, TransactionDetails, UnexpectedDerivationMethod, WithdrawError, WithdrawFee, WithdrawRequest, WithdrawResult}; use async_trait::async_trait; @@ -180,11 +180,11 @@ 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(ActualFeeRate::FixedPerKb(fixed)); }, Some(WithdrawFee::UtxoPerKbyte { ref amount }) => { - let dynamic = sat_from_big_decimal(amount, decimals)?; - tx_builder = tx_builder.with_fee(ActualTxFee::Dynamic(dynamic)); + let dynamic_fee_rate = sat_from_big_decimal(amount, decimals)?; + tx_builder = tx_builder.with_fee(ActualFeeRate::Dynamic(dynamic_fee_rate)); }, Some(ref fee_policy) => { let error = format!( @@ -206,10 +206,9 @@ where // Finish by generating `TransactionDetails` from the signed transaction. self.on_finishing()?; - let fee_amount = data.fee_amount + data.unused_change; let fee_details = UtxoFeeDetails { coin: Some(ticker.clone()), - amount: big_decimal_from_sat(fee_amount as i64, decimals), + amount: big_decimal_from_sat(data.fee_amount as i64, decimals), }; let tx_hex = match coin.addr_format() { UtxoAddressFormat::Segwit => serialize_with_flags(&signed, SERIALIZE_TRANSACTION_WITNESS).into(), diff --git a/mm2src/coins/z_coin.rs b/mm2src/coins/z_coin.rs index 6a44bf2ddb..ffad4d09ea 100644 --- a/mm2src/coins/z_coin.rs +++ b/mm2src/coins/z_coin.rs @@ -19,7 +19,7 @@ use crate::utxo::utxo_builder::{UtxoCoinBuilder, UtxoCoinBuilderCommonOps, UtxoF 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, +use crate::utxo::{sat_from_big_decimal, utxo_common, ActualFeeRate, AdditionalTxData, AddrFromStrError, Address, BroadcastTxErr, FeePolicy, GetUtxoListOps, HistoryUtxoTx, HistoryUtxoTxMap, MatureUnspentList, RecentlySpentOutPointsGuard, UtxoActivationParams, UtxoAddressFormat, UtxoArc, UtxoCoinFields, UtxoCommonOps, UtxoRpcMode, UtxoTxBroadcastOps, UtxoTxGenerationOps, VerboseTransactionFrom}; @@ -57,7 +57,6 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::{BigDecimal, MmNumber}; #[cfg(test)] use mocktopus::macros::*; -use primitives::bytes::Bytes; use rpc::v1::types::{Bytes as BytesJson, Transaction as RpcTransaction, H256 as H256Json}; use script::{Builder as ScriptBuilder, Opcode, Script, TransactionInputSigner}; use serde_json::Value as Json; @@ -371,9 +370,9 @@ impl ZCoin { } async fn get_one_kbyte_tx_fee(&self) -> UtxoRpcResult { - let fee = self.get_tx_fee().await?; + let fee = self.get_fee_rate().await?; match fee { - ActualTxFee::Dynamic(fee) | ActualTxFee::FixedPerKb(fee) => { + ActualFeeRate::Dynamic(fee) | ActualFeeRate::FixedPerKb(fee) => { Ok(big_decimal_from_sat_unsigned(fee, self.decimals())) }, } @@ -483,7 +482,6 @@ impl ZCoin { received_by_me, spent_by_me: sat_from_big_decimal(&total_input_amount, self.decimals())?, fee_amount: sat_from_big_decimal(&tx_fee, self.decimals())?, - unused_change: 0, kmd_rewards: None, }; Ok((tx, additional_data, sync_guard)) @@ -1726,17 +1724,13 @@ 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_fee_rate(&self) -> UtxoRpcResult { utxo_common::get_fee_rate(&self.utxo_arc).await } - async fn calc_interest_if_required( - &self, - unsigned: TransactionInputSigner, - data: AdditionalTxData, - my_script_pub: Bytes, - dust: u64, - ) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)> { - utxo_common::calc_interest_if_required(self, unsigned, data, my_script_pub, dust).await + async fn calc_interest_if_required(&self, unsigned: &mut TransactionInputSigner) -> UtxoRpcResult { + utxo_common::calc_interest_if_required(self, unsigned).await } + + fn supports_interest(&self) -> bool { utxo_common::is_kmd(self) } } #[async_trait]