feat(wallet): Use mempool histogram for fee estimation (#358)

* feat(wallet): Use mempool.space as a secondary fee estimation source

* fix: warn if mempool client cannot be instantiated

* make clippy happy

* nitpick: rename clippy_check to clippy in justfile

* rename `estimate_fee_rate_from_mempool` to `estimate_fee_rate_from_histogram` for clarity

* dprint fmt

* make clippy happy

* change teacing level back to debug!

* change log levels

* refactors

* refactor: estimate_fee and min_relay_fee

* serde camel case

Co-authored-by: Byron Hambly <byron@hambly.dev>

* refactors

* Add comments, use Weight struct where possible

* fmt, fix testrs

* dont fallback to bitcoin::MAX, fail instead

* make mempool space optional

* fmt

* refactor: use estimate_fee(...) in max_giveable(...)

* refactor max_giveable(...)

* refactor max_giveeable to return fee as well, remove safety margin for fee

* fix compile

* fmtr

* fix(integration test): Use pre-calculated cancel / punish fees for assert_alice_punished

* fix(integration test): Use real fees for asserts

* sync wallet before transaction_fee call

* split send_to_address into sweep_balance_to_address_dynamic_fee

---------

Co-authored-by: Byron Hambly <byron@hambly.dev>
This commit is contained in:
Mohan 2025-05-27 15:41:24 +02:00 committed by GitHub
parent 854b14939e
commit 091ba57547
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 995 additions and 358 deletions

View file

@ -28,6 +28,7 @@ impl TxLock {
impl EstimateFeeRate + Send + Sync + 'static,
>,
amount: Amount,
spending_fee: Amount,
A: PublicKey,
B: PublicKey,
change: bitcoin::Address,
@ -38,7 +39,7 @@ impl TxLock {
.expect("can derive address from descriptor");
let psbt = wallet
.send_to_address(address, amount, Some(change))
.send_to_address(address, amount, spending_fee, Some(change))
.await?;
Ok(Self {
@ -177,8 +178,8 @@ impl TxLock {
}
}
pub fn weight() -> usize {
TX_LOCK_WEIGHT
pub fn weight() -> ::bitcoin::Weight {
::bitcoin::Weight::from_wu(TX_LOCK_WEIGHT as u64)
}
}
@ -201,15 +202,28 @@ impl Watchable for TxLock {
#[cfg(test)]
mod tests {
use super::*;
use crate::bitcoin::TestWalletBuilder;
use crate::bitcoin::wallet::TestWalletBuilder;
use crate::bitcoin::Amount;
use ::bitcoin::psbt::Psbt as PartiallySignedTransaction;
// Basic setup function for tests
async fn setup() -> (
PublicKey,
PublicKey,
Wallet<bdk_wallet::rusqlite::Connection, crate::bitcoin::wallet::StaticFeeRate>,
) {
let (A, B) = alice_and_bob();
let wallet = TestWalletBuilder::new(100_000).build().await;
(A, B, wallet)
}
#[tokio::test]
async fn given_bob_sends_good_psbt_when_reconstructing_then_succeeeds() {
let (A, B) = alice_and_bob();
let wallet = TestWalletBuilder::new(50_000).build().await;
let (A, B, wallet) = setup().await;
let agreed_amount = Amount::from_sat(10000);
let spending_fee = Amount::from_sat(1000);
let psbt = bob_make_psbt(A, B, &wallet, agreed_amount).await;
let psbt = bob_make_psbt(A, B, &wallet, agreed_amount, spending_fee).await;
let result = TxLock::from_psbt(psbt, A, B, agreed_amount);
result.expect("PSBT to be valid");
@ -217,31 +231,28 @@ mod tests {
#[tokio::test]
async fn bob_can_fund_without_a_change_output() {
let (A, B) = alice_and_bob();
let fees = 300;
let agreed_amount = Amount::from_sat(10000);
let amount = agreed_amount.to_sat() + fees;
let wallet = TestWalletBuilder::new(amount).build().await;
let (A, B, _) = setup().await;
let amount = 10_000;
let agreed_amount = Amount::from_sat(amount);
let spending_fee = Amount::from_sat(300);
let wallet = TestWalletBuilder::new(amount + 300).build().await;
let psbt = bob_make_psbt(A, B, &wallet, agreed_amount).await;
let psbt = bob_make_psbt(A, B, &wallet, agreed_amount, spending_fee).await;
assert_eq!(
psbt.unsigned_tx.output.len(),
1,
"psbt should only have a single output"
"Expected no change output"
);
let result = TxLock::from_psbt(psbt, A, B, agreed_amount);
result.expect("PSBT to be valid");
}
#[tokio::test]
async fn given_bob_is_sending_less_than_agreed_when_reconstructing_txlock_then_fails() {
let (A, B) = alice_and_bob();
let wallet = TestWalletBuilder::new(50_000).build().await;
let (A, B, wallet) = setup().await;
let agreed_amount = Amount::from_sat(10000);
let spending_fee = Amount::from_sat(1000);
let bad_amount = Amount::from_sat(5000);
let psbt = bob_make_psbt(A, B, &wallet, bad_amount).await;
let psbt = bob_make_psbt(A, B, &wallet, bad_amount, spending_fee).await;
let result = TxLock::from_psbt(psbt, A, B, agreed_amount);
result.expect_err("PSBT to be invalid");
@ -249,12 +260,12 @@ mod tests {
#[tokio::test]
async fn given_bob_is_sending_to_a_bad_output_reconstructing_txlock_then_fails() {
let (A, B) = alice_and_bob();
let wallet = TestWalletBuilder::new(50_000).build().await;
let (A, B, wallet) = setup().await;
let agreed_amount = Amount::from_sat(10000);
let spending_fee = Amount::from_sat(1000);
let E = eve();
let psbt = bob_make_psbt(E, B, &wallet, agreed_amount).await;
let psbt = bob_make_psbt(E, B, &wallet, agreed_amount, spending_fee).await;
let result = TxLock::from_psbt(psbt, A, B, agreed_amount);
result.expect_err("PSBT to be invalid");
@ -271,9 +282,7 @@ mod tests {
}
}
/// Helper function that represents Bob's action of constructing the PSBT.
///
/// Extracting this allows us to keep the tests concise.
// Helper function for testing PSBT creation by Bob
async fn bob_make_psbt(
A: PublicKey,
B: PublicKey,
@ -282,9 +291,10 @@ mod tests {
impl EstimateFeeRate + Send + Sync + 'static,
>,
amount: Amount,
spending_fee: Amount,
) -> PartiallySignedTransaction {
let change = wallet.new_address().await.unwrap();
TxLock::new(wallet, amount, A, B, change)
TxLock::new(wallet, amount, spending_fee, A, B, change)
.await
.unwrap()
.into()