diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index aa20c9d4fb..c57f01d912 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -315,7 +315,9 @@ impl Qrc20Coin { /// 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::Fixed(amount) | ActualTxFee::Dynamic(amount) => Ok(amount + gas_fee), + ActualTxFee::Fixed(amount) | ActualTxFee::Dynamic(amount) | ActualTxFee::FixedPerKb(amount) => { + Ok(amount + gas_fee) + }, } } diff --git a/mm2src/coins/utxo.rs b/mm2src/coins/utxo.rs index 88dae3fcbe..548e151956 100644 --- a/mm2src/coins/utxo.rs +++ b/mm2src/coins/utxo.rs @@ -140,6 +140,7 @@ pub enum TxFee { Fixed(u64), /// Tell the coin that it should request the fee from daemon RPC and calculate it relying on tx size Dynamic(EstimateFeeMethod), + FixedPerKb(u64), } /// The actual "runtime" fee that is received from RPC in case of dynamic calculation @@ -149,6 +150,9 @@ pub enum ActualTxFee { Fixed(u64), /// 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), } /// Fee policy applied on transaction creation @@ -1025,6 +1029,12 @@ pub trait UtxoCoinBuilder { } async fn tx_fee(&self, rpc_client: &UtxoRpcClientEnum) -> Result { + const ONE_DOGE: u64 = 100000000; + + if self.ticker() == "DOGE" { + return Ok(TxFee::FixedPerKb(ONE_DOGE)); + } + let tx_fee = match self.conf()["txfee"].as_u64() { None => TxFee::Fixed(1000), Some(0) => { diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index e1ed0cdc5d..fae554bd2d 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -151,6 +151,7 @@ pub async fn get_tx_fee(coin: &UtxoCoinFields) -> Result Ok(ActualTxFee::FixedPerKb(*satoshis)), } } @@ -164,6 +165,8 @@ where ActualTxFee::Fixed(fee) => fee, // atomic swap payment spend transaction is slightly more than 300 bytes in average as of now ActualTxFee::Dynamic(fee_per_kb) => (fee_per_kb * SWAP_TX_SPEND_SIZE) / KILO_BYTE, + // return satoshis here as swap spend transaction size is always less than 1 kb + ActualTxFee::FixedPerKb(satoshis) => satoshis, }; if coin.as_ref().conf.force_min_relay_fee { let relay_fee = try_s!(coin.as_ref().rpc_client.get_relay_fee().compat().await); @@ -373,6 +376,20 @@ where let tx_size = transaction_bytes.len() + transaction.inputs().len() * additional_len; (f * tx_size as u64) / KILO_BYTE }, + ActualTxFee::FixedPerKb(f) => { + let transaction = UtxoTx::from(tx.clone()); + let transaction_bytes = serialize(&transaction); + // 2 bytes are used to indicate the length of signature and pubkey + // total is 107 + let additional_len = 2 + MAX_DER_SIGNATURE_LEN + COMPRESSED_PUBKEY_LEN; + let tx_size_bytes = (transaction_bytes.len() + transaction.inputs().len() * additional_len) as u64; + let tx_size_kb = if tx_size_bytes % KILO_BYTE == 0 { + tx_size_bytes / KILO_BYTE + } else { + tx_size_bytes / KILO_BYTE + 1 + }; + f * tx_size_kb + }, }; match fee_policy { @@ -1823,6 +1840,7 @@ where let amount = match fee { ActualTxFee::Fixed(f) => f, ActualTxFee::Dynamic(f) => f, + ActualTxFee::FixedPerKb(f) => f, }; Ok(TradeFee { coin: ticker, @@ -1856,36 +1874,61 @@ where { let decimals = coin.as_ref().decimals; let tx_fee = try_map!(coin.get_tx_fee().await, TradePreimageError::Other); - let dynamic_fee = match tx_fee { + match tx_fee { ActualTxFee::Fixed(fee_amount) => { let amount = big_decimal_from_sat(fee_amount as i64, decimals); return Ok(amount); }, // if it's a dynamic fee, we should generate a swap transaction to get an actual trade fee - ActualTxFee::Dynamic(fee) => 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) = try_map!( + coin.list_unspent_ordered(&coin.as_ref().my_address).await, + TradePreimageError::Other + ); - // take into account that the dynamic tx fee may increase during the swap - let dynamic_fee = coin.increase_dynamic_fee_by_stage(dynamic_fee, stage); + let actual_tx_fee = Some(ActualTxFee::Dynamic(dynamic_fee)); + let (tx, data) = generate_transaction(coin, unspents, outputs, fee_policy, actual_tx_fee, gas_fee).await?; - let outputs_count = outputs.len(); - let (unspents, _recently_sent_txs) = try_map!( - coin.list_unspent_ordered(&coin.as_ref().my_address).await, - TradePreimageError::Other - ); + 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 + } else { + // the change output is included already + data.fee_amount + }; - let actual_tx_fee = Some(ActualTxFee::Dynamic(dynamic_fee)); - let (tx, data) = generate_transaction(coin, unspents, outputs, fee_policy, actual_tx_fee, gas_fee).await?; + Ok(big_decimal_from_sat(total_fee as i64, decimals)) + }, + ActualTxFee::FixedPerKb(fee) => { + let outputs_count = outputs.len(); + let (unspents, _recently_sent_txs) = try_map!( + coin.list_unspent_ordered(&coin.as_ref().my_address).await, + TradePreimageError::Other + ); - 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 - } else { - // the change outputs is included already - data.fee_amount - }; + let (tx, data) = generate_transaction(coin, unspents, outputs, fee_policy, Some(tx_fee), gas_fee).await?; - Ok(big_decimal_from_sat(total_fee as i64, decimals)) + 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 + }; + + Ok(big_decimal_from_sat(total_fee as i64, decimals)) + }, + } } /// Maker or Taker should pay fee only for sending his payment. diff --git a/mm2src/coins/utxo/utxo_tests.rs b/mm2src/coins/utxo/utxo_tests.rs index 27348142a6..97a1eba424 100644 --- a/mm2src/coins/utxo/utxo_tests.rs +++ b/mm2src/coins/utxo/utxo_tests.rs @@ -1,6 +1,7 @@ use super::rpc_clients::{ElectrumProtocol, ListSinceBlockRes, NetworkInfo}; use super::*; use crate::utxo::rpc_clients::{GetAddressInfoRes, UtxoRpcClientOps, ValidateAddressRes, VerboseBlock}; +use crate::utxo::utxo_common::{generate_transaction, UtxoArcBuilder}; use crate::utxo::utxo_standard::{utxo_standard_coin_from_conf_and_request, UtxoStandardCoin}; use crate::{SwapOps, TradePreimageValue, WithdrawFee}; use bigdecimal::BigDecimal; @@ -2214,3 +2215,86 @@ fn firo_verbose_block_deserialize() { }); let _block: VerboseBlock = json::from_value(json).unwrap(); } + +#[test] +fn test_generate_tx_doge_fee() { + // A tx below 1kb is always 1 doge fee yes. + // But keep in mind that every output below 1 doge will incur and extra 1 doge dust fee + let config = json!({ + "coin": "DOGE", + "name": "dogecoin", + "fname": "Dogecoin", + "rpcport": 22555, + "pubtype": 30, + "p2shtype": 22, + "wiftype": 158, + "txfee": 0, + "force_min_relay_fee": true, + "dust": 100000000, + "mm2": 1, + "required_confirmations": 2, + "avg_blocktime": 1, + "protocol": { + "type": "UTXO" + } + }); + let request = json!({ + "method": "electrum", + "coin": "DOGE", + "servers": [{"url": "electrum1.cipig.net:10060"},{"url": "electrum2.cipig.net:10060"},{"url": "electrum3.cipig.net:10060"}], + }); + let ctx = MmCtxBuilder::default().into_mm_arc(); + let doge: UtxoStandardCoin = block_on(UtxoArcBuilder::new(&ctx, "DOGE", &config, &request, &[1; 32]).build()) + .unwrap() + .into(); + + let unspents = vec![UnspentInfo { + outpoint: Default::default(), + value: 1000000000000, + height: None, + }]; + let outputs = vec![TransactionOutput { + value: 100000000, + script_pubkey: vec![0; 26].into(), + }]; + let policy = FeePolicy::SendExact; + let (_, data) = block_on(generate_transaction(&doge, unspents, outputs, policy, None, None)).unwrap(); + let expected_fee = 100000000; + assert_eq!(expected_fee, data.fee_amount); + + let unspents = vec![UnspentInfo { + outpoint: Default::default(), + value: 1000000000000, + height: None, + }]; + let outputs = vec![ + TransactionOutput { + value: 100000000, + script_pubkey: vec![0; 26].into(), + } + .clone(); + 40 + ]; + let policy = FeePolicy::SendExact; + let (_, data) = block_on(generate_transaction(&doge, unspents, outputs, policy, None, None)).unwrap(); + let expected_fee = 200000000; + assert_eq!(expected_fee, data.fee_amount); + + let unspents = vec![UnspentInfo { + outpoint: Default::default(), + value: 1000000000000, + height: None, + }]; + let outputs = vec![ + TransactionOutput { + value: 100000000, + script_pubkey: vec![0; 26].into(), + } + .clone(); + 60 + ]; + let policy = FeePolicy::SendExact; + let (_, data) = block_on(generate_transaction(&doge, unspents, outputs, policy, None, None)).unwrap(); + let expected_fee = 300000000; + assert_eq!(expected_fee, data.fee_amount); +}