Skip to content

Commit d9d061c

Browse files
committed
Implement RBF fee bumping for unconfirmed transactions
Add `Replace-by-Fee` functionality to allow users to increase fees on pending outbound transactions, improving confirmation likelihood during network congestion. - Uses BDK's `build_fee_bump` for transaction replacement - Validates transaction eligibility: must be outbound and unconfirmed - Maintains payment history consistency across wallet updates - Includes integration tests for various RBF scenarios
1 parent 38f9a87 commit d9d061c

File tree

6 files changed

+439
-28
lines changed

6 files changed

+439
-28
lines changed

bindings/ldk_node.udl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,8 @@ interface OnchainPayment {
277277
Txid send_to_address([ByRef]Address address, u64 amount_sats, FeeRate? fee_rate);
278278
[Throws=NodeError]
279279
Txid send_all_to_address([ByRef]Address address, boolean retain_reserve, FeeRate? fee_rate);
280+
[Throws=NodeError]
281+
Txid bump_fee_rbf(PaymentId payment_id, FeeRate? fee_rate);
280282
};
281283

282284
interface FeeRate {

src/payment/onchain.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use std::sync::{Arc, RwLock};
1111

1212
use bitcoin::{Address, Txid};
13+
use lightning::ln::channelmanager::PaymentId;
1314

1415
use crate::config::Config;
1516
use crate::error::Error;
@@ -120,4 +121,20 @@ impl OnchainPayment {
120121
let fee_rate_opt = maybe_map_fee_rate_opt!(fee_rate);
121122
self.wallet.send_to_address(address, send_amount, fee_rate_opt)
122123
}
124+
125+
/// Attempt to bump the fee of an unconfirmed transaction using Replace-by-Fee (RBF).
126+
///
127+
/// This creates a new transaction that replaces the original one, increasing the fee by the
128+
/// specified increment to improve its chances of confirmation.
129+
///
130+
/// The new transaction will have the same outputs as the original but with a
131+
/// higher fee, resulting in faster confirmation potential.
132+
///
133+
/// Returns the [`Txid`] of the new replacement transaction if successful.
134+
pub fn bump_fee_rbf(
135+
&self, payment_id: PaymentId, fee_rate: Option<FeeRate>,
136+
) -> Result<Txid, Error> {
137+
let fee_rate_opt = maybe_map_fee_rate_opt!(fee_rate);
138+
self.wallet.bump_fee_rbf(payment_id, fee_rate_opt)
139+
}
123140
}

src/payment/pending_payment_store.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,11 @@ impl StorableObjectUpdate<PendingPaymentDetails> for PendingPaymentDetailsUpdate
8484

8585
impl From<&PendingPaymentDetails> for PendingPaymentDetailsUpdate {
8686
fn from(value: &PendingPaymentDetails) -> Self {
87-
Self {
88-
id: value.id(),
89-
payment_update: Some(value.details.to_update()),
90-
conflicting_txids: Some(value.conflicting_txids.clone()),
91-
}
87+
let conflicting_txids = if value.conflicting_txids.is_empty() {
88+
None
89+
} else {
90+
Some(value.conflicting_txids.clone())
91+
};
92+
Self { id: value.id(), payment_update: Some(value.details.to_update()), conflicting_txids }
9293
}
9394
}

src/payment/store.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,15 @@ impl StorableObject for PaymentDetails {
291291
}
292292
}
293293

294+
if let Some(tx_id) = update.txid {
295+
match self.kind {
296+
PaymentKind::Onchain { ref mut txid, .. } => {
297+
update_if_necessary!(*txid, tx_id);
298+
},
299+
_ => {},
300+
}
301+
}
302+
294303
if updated {
295304
self.latest_update_timestamp = SystemTime::now()
296305
.duration_since(UNIX_EPOCH)
@@ -540,6 +549,7 @@ pub(crate) struct PaymentDetailsUpdate {
540549
pub direction: Option<PaymentDirection>,
541550
pub status: Option<PaymentStatus>,
542551
pub confirmation_status: Option<ConfirmationStatus>,
552+
pub txid: Option<Txid>,
543553
}
544554

545555
impl PaymentDetailsUpdate {
@@ -555,6 +565,7 @@ impl PaymentDetailsUpdate {
555565
direction: None,
556566
status: None,
557567
confirmation_status: None,
568+
txid: None,
558569
}
559570
}
560571
}
@@ -570,9 +581,9 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate {
570581
_ => (None, None, None),
571582
};
572583

573-
let confirmation_status = match value.kind {
574-
PaymentKind::Onchain { status, .. } => Some(status),
575-
_ => None,
584+
let (confirmation_status, txid) = match &value.kind {
585+
PaymentKind::Onchain { status, txid, .. } => (Some(*status), Some(*txid)),
586+
_ => (None, None),
576587
};
577588

578589
let counterparty_skimmed_fee_msat = match value.kind {
@@ -593,6 +604,7 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate {
593604
direction: Some(value.direction),
594605
status: Some(value.status),
595606
confirmation_status,
607+
txid,
596608
}
597609
}
598610
}

0 commit comments

Comments
 (0)