Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enhancement(utxo): improve trade and withdraw fee calculations #2083

Open
wants to merge 20 commits into
base: dev
Choose a base branch
from

Conversation

borngraced
Copy link
Member

@borngraced borngraced commented Mar 8, 2024

UTXO Fee Improvements:

  1. fee-per-kb calculation and improvement: Ensure UTXO fee calculation is based on transaction size and some improvements.
  2. Custom Fee Options: Enable users to select low, default/normal, or high priority fee levels for UTXO withdrawals.

Withdraw using UtxoPriority::Low

{
	"userpass": "SamopE160!",
	"method": "withdraw",
	"mmrpc": "2.0",
	"params": {
	   "to": "RCKfmv1X4oZHhGgb9mVD8XnkubAerWEcQ4",
	   "max": false,
	   "coin": "DOC",
	   "amount": "1000",
	   "fee": {
	      "type": "UtxoPriority",
	       "priority": "Low",
	    }
	}
}

#1835

@borngraced borngraced added the in progress Changes will be made from the author label Mar 8, 2024
@borngraced borngraced self-assigned this Mar 8, 2024
@borngraced borngraced changed the title enhancement(utxo): UTXO Per Kb for Trade Fees enhancement(utxo): improve trade and withdraw fee calculations Mar 18, 2024
@shamardy shamardy requested review from dimxy and shamardy March 22, 2024 22:01
@shamardy shamardy requested a review from dimxy May 6, 2024 14:57
@shamardy
Copy link
Collaborator

shamardy commented May 6, 2024

@borngraced please resolve conflicts in this PR

@mariocynicys mariocynicys self-requested a review May 7, 2024 12:22
Copy link
Collaborator

@mariocynicys mariocynicys left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Early review.
Will do another round once this PR is r2r.

Have a Q though: Why did we add tx_size in trade fee? Why do we keep tx_size around? It would be helpful having these answers in the PR header post, and possibly explaining the approach taken.

mm2src/coins/qrc20.rs Show resolved Hide resolved
mm2src/coins/z_coin.rs Outdated Show resolved Hide resolved
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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want to add a fee rate (rate per kb) to the gas fee?
Is the gas fee here a rate as well (per kb)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assuming that gas_fee represents the gas costs and self.get_tx_fee_per_kb() represents the per-kb costs associated with the tx size

mm2src/coins/z_coin.rs Outdated Show resolved Hide resolved
mm2src/coins/utxo/utxo_tests.rs Show resolved Hide resolved
mm2src/coins/utxo/utxo_tests.rs Show resolved Hide resolved
mm2src/coins/utxo/utxo_tests.rs Show resolved Hide resolved
@borngraced borngraced added under review and removed in progress Changes will be made from the author labels May 12, 2024
Copy link
Collaborator

@shamardy shamardy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the PR! First review iteration from my side where I discuss some modifications and proposals on how we can handle fee priority in a better way!

Comment on lines 28 to 38
/// Num of blocks applied to withdrawal transactions
/// that have a high priority, indicating a need for faster confirmation.
pub const HIGH_TX_FEE: u8 = 1;

/// Num of blocks applied to withdrawal transactions
/// that have a normal priority, indicating a moderate confirmation time.
pub const NORMAL_TX_FEE: u8 = 2;

/// Num of blocks applied to withdrawal transactions
/// that have a low priority, indicating a longer confirmation time.
pub const LOW_TX_FEE: u8 = 4;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have some concerns about the choice of the number of blocks used for transaction confirmation priority.

For high fee transactions, we could consider using 2 blocks. This is based on the below

/// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-estimatefee
/// It is recommended to set n_blocks as low as possible.
/// However, in some cases, n_blocks = 1 leads to an unreasonably high fee estimation.
/// https://github.com/KomodoPlatform/atomicDEX-API/issues/656#issuecomment-743759659
pub fn estimate_fee(&self, mode: &Option<EstimateFeeMode>, n_blocks: u32) -> UtxoRpcFut<f64> {
However, this might only be applicable to some UTXO coins and not all. For Bitcoin, the situation might be different. Therefore, it might be beneficial to get these values from the coin config where they are tested separately for each coin that supports fee estimation.

For normal fee transactions, 6 blocks seem to be a good choice. This implies that we expect the transaction to be confirmed within an hour for Bitcoin.

For low fee transactions, the goal should be to choose the lowest possible fee that will still get the transaction confirmed and not removed from the mempool. This is easier said than done. The mempool explorer provides a good API for estimating this for BTC only. It's the economyFee field, which is the lower of the low priority (1 hour fee) or 2x the minimum allowed fee.

I recommend testing the value generated from Electrum's blockchain.estimatefee against the values from the mempool explorer. The mempool explorer is one of the most reliable explorers for BTC fees. If we can't estimate good fees using Electrum for BTC, we can use the mempool API for BTC only. Although it's rate-limited, it will be rate-limited per user which is acceptable.

Lastly, please also modify the below doc comments to reflect the number of blocks used.

/// Priority levels for UTXO fee estimation for withdrawal.
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub enum UtxoFeePriority {
/// Low priority: Estimated confirmation within approximately 3 blocks.
Low,
/// Normal priority: Estimated confirmation within approximately 2 blocks.
Normal,
/// High priority: Estimated confirmation within approximately 1 block.
High,
}

P.S. We can continue this discussion on DM since it might be a bit complicated.

Copy link
Collaborator

@shamardy shamardy May 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is how mempool explorer calculates the different fee levels https://github.com/mempool/mempool/blob/db34ca6a5f37f8c420ce961f0ccd96bba9205dfd/backend/src/api/fee-api.ts#L22-L63 I guess High = 1 , Normal = 2, Low = 3 or 4 is applicable to bitcoin only and not other UTXOs.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only last note :)

In the future we can enable RBF and use lower fees for the low level while giving the ability to the user to bump the fee if it's not confirming in time. This is out of scope of this PR ofcourse.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines +1938 to +1942
/// encapsulates the priority of a withdrawal transaction, indicating the desired fee
/// level for transaction processing.
UtxoPriority {
priority: UtxoFeePriority,
},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is how I envision the GUI implementation of this part.

We present the user with the three levels of fees and let them make a choice. The GUI would potentially make three separate withdrawal requests, each using a different priority level. This would generate three different raw transaction hexes, each associated with a different fee and expected confirmation time.

Once the user selects the fee they prefer, the corresponding transaction hex would be used.

What are your thoughts on this approach? Would it be feasible to make some changes that simplifies the process for the GUI to handle this with a single request?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, here's mine view.

The api receives a priority withdrawal request from the gui, the api generates fee for the available priorities(currently 3) and send the response (each fee with it's tx hex) and finally, gui can render this three options for user to pick a choice.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The api receives a priority withdrawal request from the gui, the api generates fee for the available priorities(currently 3) and send the response (each fee with it's tx hex) and finally, gui can render this three options for user to pick a choice.

This is how I see it as well, but it might be too complicated as we would probably need to return 3 TransactionDetails objects in the response which is different from the current withdraw response.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI for eth I also added priority levels for tx fees:
For withdrawals user can add a new WithdrawFee option and set explicit fee per gas.
For swaps there is an rpc to set the desired level of low/medium/priority for the whole swap (so the API will by itself query the current fee value before each swap step).
There is also an rpc for GUI to query fees for all supported 3 levels (to show fe values to the user so he could select desired level). Initially user should start a loop in the API which periodically calculates priority fees, so the query rpc just immediately returns the recent estimated fee values.

Maybe we could do this similar both for utxo and eth.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Some explanation why a there is loop in API to obtain fees. This is how other eth wallet apps do it: API does fee estimations getting data from the eth provider. The GUI subscribes to the fee updates from API so it can in realtime notify the user about changes)

@borngraced borngraced added in progress Changes will be made from the author and removed under review labels May 28, 2024
@borngraced borngraced added under review in progress Changes will be made from the author and removed in progress Changes will be made from the author under review labels May 31, 2024
.compat()
.await?;

if ["BTC"].contains(&coin.as_ref().conf.ticker.as_str()) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this intended to check tickers like that?
I guess this would be false for coins like "BTC-segwit"
(Maybe consider adding a config param?)

@@ -1603,16 +1597,16 @@ pub async fn send_maker_spends_taker_payment<T: UtxoCommonOps + SwapOps>(
coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox)
.await
);
if fee >= payment_value {
if fee.fee >= payment_value {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when I was studying mm2 code I messed up with used several 'fee' concepts. So I'd suggest we use 'qualified' names for fees, if possible, like 'tx_fee' or 'dex_fee' (not just 'fee') to easy get what fee is meant

@@ -957,12 +974,22 @@ impl MatureUnspentList {
}
}

#[derive(Debug)]
pub struct HtlcSpendFeeResult {
pub fee: u64,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe fee_per_kb?

async fn generate_withdraw_fee_using_priority<C: UtxoCommonOps + GetUtxoListOps>(
coin: &C,
priority: &UtxoFeePriority,
priorities: &Option<UtxoFeePriorities>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need 'priorities' param? Could we not get it from the 'coin' param?

@@ -500,6 +504,17 @@ impl UtxoSyncStatusLoopHandle {
}
}

/// Priority levels for UTXO fee estimation for withdrawal.
#[derive(Clone, Debug, Deserialize)]
pub struct UtxoFeePriorities {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to me UtxoFeePriorities does not exactly matches the concept.
How about

#[derive(Clone, Debug, Deserialize)]
struct UtxoTxWaitBlocks {
    pub low_priority: u8,
    pub normal_priority: u8,
    pub high_priority: u8,
}

In the coins file the param could have a name like blocks_tx_to_wait or tx_wait_blocks

@@ -313,4 +315,8 @@ impl<'a> UtxoConfBuilder<'a> {
}

fn avg_blocktime(&self) -> Option<u64> { self.conf["avg_blocktime"].as_u64() }

fn fee_priorities(&self) -> Option<UtxoFeePriorities> {
json::from_value(self.conf["fee_priorities"].clone()).unwrap_or(None)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: we could throw a error if 'fee_priorities' is parsed incorrectly (bad or omitted values)

@dimxy
Copy link
Collaborator

dimxy commented Jun 19, 2024

I think I have a general question:
Why do we use 'fee per KB' units? I see in explorers or wallets that usually 'fee per vB' is used. Would this not be more convenient for users if we use the same? Then users may check actual fees in explorer and use it in our wallet without conversion.
(BTW maybe some users may not clearly understand the 'vByte' idea but this should not be a problem as they can just use it directly 'as is')

@laruh
Copy link
Member

laruh commented Jun 20, 2024

ps @borngraced I see that PR lint fails

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in progress Changes will be made from the author
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants