Skip to content

Commit 8b7cf78

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 - Implements fee rate estimation with safety limits - Maintains payment history consistency across wallet updates - Includes integration tests for various RBF scenarios
1 parent be0a040 commit 8b7cf78

File tree

5 files changed

+641
-38
lines changed

5 files changed

+641
-38
lines changed

bindings/ldk_node.udl

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ dictionary Config {
1313
u64 probing_liquidity_limit_multiplier;
1414
AnchorChannelsConfig? anchor_channels_config;
1515
SendingParameters? sending_parameters;
16+
boolean? auto_rebroadcast_unconfirmed_tx
1617
};
1718

1819
dictionary AnchorChannelsConfig {
@@ -104,6 +105,8 @@ interface Builder {
104105
Node build_with_vss_store_and_fixed_headers(string vss_url, string store_id, record<string, string> fixed_headers);
105106
[Throws=BuildError]
106107
Node build_with_vss_store_and_header_provider(string vss_url, string store_id, VssHeaderProvider header_provider);
108+
[Throws=BuildError]
109+
void set_auto_rebroadcast_unconfirmed(boolean enable);
107110
};
108111

109112
interface Node {
@@ -231,6 +234,10 @@ interface OnchainPayment {
231234
Txid send_to_address([ByRef]Address address, u64 amount_sats, FeeRate? fee_rate);
232235
[Throws=NodeError]
233236
Txid send_all_to_address([ByRef]Address address, boolean retain_reserve, FeeRate? fee_rate);
237+
[Throws=NodeError]
238+
void rebroadcast_transaction(PaymentId payment_id);
239+
[Throws=NodeError]
240+
Txid bump_fee_rbf(PaymentId payment_id);
234241
};
235242

236243
interface FeeRate {
@@ -409,7 +416,7 @@ interface ClosureReason {
409416

410417
[Enum]
411418
interface PaymentKind {
412-
Onchain(Txid txid, ConfirmationStatus status);
419+
Onchain(Txid txid, ConfirmationStatus status, sequence<u8>? raw_tx, u64? last_broadcast_time, u32? broadcast_attempts);
413420
Bolt11(PaymentHash hash, PaymentPreimage? preimage, PaymentSecret? secret);
414421
Bolt11Jit(PaymentHash hash, PaymentPreimage? preimage, PaymentSecret? secret, u64? counterparty_skimmed_fee_msat, LSPFeeLimits lsp_fee_limits);
415422
Bolt12Offer(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret, OfferId offer_id, UntrustedString? payer_note, u64? quantity);

src/payment/onchain.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use crate::types::{ChannelManager, Wallet};
1414
use crate::wallet::OnchainSendAmount;
1515

1616
use bitcoin::{Address, Txid};
17+
use lightning::ln::channelmanager::PaymentId;
1718

1819
use std::sync::{Arc, RwLock};
1920

@@ -120,4 +121,30 @@ 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+
/// Manually trigger a rebroadcast of a specific transaction according to the default policy.
126+
///
127+
/// This is useful if you suspect a transaction may not have propagated properly through the
128+
/// network and you want to attempt to rebroadcast it immediately rather than waiting for the
129+
/// automatic background job to handle it.
130+
///
131+
/// updating the attempt count and last broadcast time for the transaction in the payment store.
132+
pub fn rebroadcast_transaction(&self, payment_id: PaymentId) -> Result<(), Error> {
133+
self.wallet.rebroadcast_transaction(payment_id)?;
134+
Ok(())
135+
}
136+
137+
/// Attempt to bump the fee of an unconfirmed transaction using Replace-by-Fee (RBF).
138+
///
139+
/// This creates a new transaction that replaces the original one, increasing the fee by the
140+
/// specified increment to improve its chances of confirmation. The original transaction must
141+
/// be signaling RBF replaceability for this to succeed.
142+
///
143+
/// The new transaction will have the same outputs as the original but with a
144+
/// higher fee, resulting in faster confirmation potential.
145+
///
146+
/// Returns the Txid of the new replacement transaction if successful.
147+
pub fn bump_fee_rbf(&self, payment_id: PaymentId) -> Result<Txid, Error> {
148+
self.wallet.bump_fee_rbf(payment_id)
149+
}
123150
}

src/payment/store.rs

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,24 @@ impl StorableObject for PaymentDetails {
293293
}
294294
}
295295

296+
if let Some(attempts) = update.broadcast_attempts {
297+
match self.kind {
298+
PaymentKind::Onchain { ref mut broadcast_attempts, .. } => {
299+
update_if_necessary!(*broadcast_attempts, attempts);
300+
},
301+
_ => {},
302+
}
303+
}
304+
305+
if let Some(broadcast_time) = update.last_broadcast_time {
306+
match self.kind {
307+
PaymentKind::Onchain { ref mut last_broadcast_time, .. } => {
308+
update_if_necessary!(*last_broadcast_time, broadcast_time);
309+
},
310+
_ => {},
311+
}
312+
}
313+
296314
if updated {
297315
self.latest_update_timestamp = SystemTime::now()
298316
.duration_since(UNIX_EPOCH)
@@ -353,6 +371,12 @@ pub enum PaymentKind {
353371
txid: Txid,
354372
/// The confirmation status of this payment.
355373
status: ConfirmationStatus,
374+
/// The raw transaction for rebroadcasting
375+
raw_tx: Option<Vec<u8>>,
376+
/// Last broadcast attempt timestamp (UNIX seconds)
377+
last_broadcast_time: Option<u64>,
378+
/// Number of broadcast attempts
379+
broadcast_attempts: Option<u32>,
356380
},
357381
/// A [BOLT 11] payment.
358382
///
@@ -451,6 +475,9 @@ impl_writeable_tlv_based_enum!(PaymentKind,
451475
(0, Onchain) => {
452476
(0, txid, required),
453477
(2, status, required),
478+
(4, raw_tx, option),
479+
(10, last_broadcast_time, option),
480+
(12, broadcast_attempts, option),
454481
},
455482
(2, Bolt11) => {
456483
(0, hash, required),
@@ -542,6 +569,8 @@ pub(crate) struct PaymentDetailsUpdate {
542569
pub direction: Option<PaymentDirection>,
543570
pub status: Option<PaymentStatus>,
544571
pub confirmation_status: Option<ConfirmationStatus>,
572+
pub last_broadcast_time: Option<Option<u64>>,
573+
pub broadcast_attempts: Option<Option<u32>>,
545574
}
546575

547576
impl PaymentDetailsUpdate {
@@ -557,6 +586,8 @@ impl PaymentDetailsUpdate {
557586
direction: None,
558587
status: None,
559588
confirmation_status: None,
589+
last_broadcast_time: None,
590+
broadcast_attempts: None,
560591
}
561592
}
562593
}
@@ -572,9 +603,11 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate {
572603
_ => (None, None, None),
573604
};
574605

575-
let confirmation_status = match value.kind {
576-
PaymentKind::Onchain { status, .. } => Some(status),
577-
_ => None,
606+
let (confirmation_status, last_broadcast_time, broadcast_attempts) = match value.kind {
607+
PaymentKind::Onchain { status, last_broadcast_time, broadcast_attempts, .. } => {
608+
(Some(status), last_broadcast_time, broadcast_attempts)
609+
},
610+
_ => (None, None, None),
578611
};
579612

580613
let counterparty_skimmed_fee_msat = match value.kind {
@@ -595,6 +628,8 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate {
595628
direction: Some(value.direction),
596629
status: Some(value.status),
597630
confirmation_status,
631+
last_broadcast_time: Some(last_broadcast_time),
632+
broadcast_attempts: Some(broadcast_attempts),
598633
}
599634
}
600635
}

0 commit comments

Comments
 (0)