-
Notifications
You must be signed in to change notification settings - Fork 94
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
base: dev
Are you sure you want to change the base?
Conversation
@borngraced please resolve conflicts in this PR |
There was a problem hiding this 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.
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) |
There was a problem hiding this comment.
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)?
There was a problem hiding this comment.
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
There was a problem hiding this 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!
mm2src/coins/utxo/utxo_withdraw.rs
Outdated
/// 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; |
There was a problem hiding this comment.
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
komodo-defi-framework/mm2src/coins/utxo/rpc_clients.rs
Lines 2012 to 2016 in 9ec826a
/// 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> { |
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.
komodo-defi-framework/mm2src/coins/lp_coins.rs
Lines 1916 to 1925 in 9ec826a
/// 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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
/// encapsulates the priority of a withdrawal transaction, indicating the desired fee | ||
/// level for transaction processing. | ||
UtxoPriority { | ||
priority: UtxoFeePriority, | ||
}, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
.compat() | ||
.await?; | ||
|
||
if ["BTC"].contains(&coin.as_ref().conf.ticker.as_str()) { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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>, |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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)
I think I have a general question: |
ps @borngraced I see that PR lint fails |
UTXO Fee Improvements:
Withdraw using
UtxoPriority::Low
#1835