Skip to content

Commit e52af7f

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 7d26667 commit e52af7f

8 files changed

Lines changed: 412 additions & 96 deletions

File tree

bindings/ldk_node.udl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,8 @@ interface OnchainPayment {
269269
Txid send_all_to_address([ByRef]Address address, boolean retain_reserve, FeeRate? fee_rate);
270270
[Throws=NodeError]
271271
void rebroadcast_transaction(PaymentId payment_id);
272+
[Throws=NodeError]
273+
Txid bump_fee_rbf(PaymentId payment_id);
272274
};
273275

274276
interface FeeRate {

src/ffi/types.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ pub use bip39::Mnemonic;
2020
use bitcoin::hashes::sha256::Hash as Sha256;
2121
use bitcoin::hashes::Hash;
2222
use bitcoin::secp256k1::PublicKey;
23-
pub use bitcoin::{Address, BlockHash, FeeRate, Network, OutPoint, ScriptBuf, Txid};
23+
pub use bitcoin::{Address, BlockHash, FeeRate, Network, OutPoint, ScriptBuf, Transaction, Txid};
2424
pub use lightning::chain::channelmonitor::BalanceSource;
2525
pub use lightning::events::{ClosureReason, PaymentFailureReason};
2626
use lightning::ln::channelmanager::PaymentId;

src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,8 @@ pub use builder::NodeBuilder as Builder;
124124
use chain::ChainSource;
125125
use config::{
126126
default_user_config, may_announce_channel, AsyncPaymentsRole, ChannelConfig, Config,
127-
NODE_ANN_BCAST_INTERVAL, PEER_RECONNECTION_INTERVAL, RGS_SYNC_INTERVAL, UNCONFIRMED_TX_BROADCAST_INTERVAL,
127+
NODE_ANN_BCAST_INTERVAL, PEER_RECONNECTION_INTERVAL, RGS_SYNC_INTERVAL,
128+
UNCONFIRMED_TX_BROADCAST_INTERVAL,
128129
};
129130
use connection::ConnectionManager;
130131
pub use error::Error as NodeError;

src/payment/onchain.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,8 @@ use crate::logger::{log_info, LdkLogger, Logger};
1717
use crate::types::{ChannelManager, Wallet};
1818
use crate::wallet::OnchainSendAmount;
1919

20-
use bitcoin::{Address, Txid};
2120
use lightning::ln::channelmanager::PaymentId;
2221

23-
use std::sync::{Arc, RwLock};
24-
2522
#[cfg(not(feature = "uniffi"))]
2623
type FeeRate = bitcoin::FeeRate;
2724
#[cfg(feature = "uniffi")]
@@ -136,4 +133,18 @@ impl OnchainPayment {
136133
pub fn rebroadcast_transaction(&self, payment_id: PaymentId) -> Result<(), Error> {
137134
self.wallet.rebroadcast_transaction(payment_id)
138135
}
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+
}
139150
}

src/payment/pending_payment_store.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,12 @@ impl StorableObject for PendingPaymentDetails {
6262
}
6363

6464
if let Some(new_conflicting_txids) = &update.conflicting_txids {
65-
if &self.conflicting_txids != new_conflicting_txids {
66-
self.conflicting_txids = new_conflicting_txids.clone();
67-
updated = true;
65+
// Don't overwrite existing conflicts with an empty list
66+
if !new_conflicting_txids.is_empty() {
67+
if &self.conflicting_txids != new_conflicting_txids {
68+
self.conflicting_txids = new_conflicting_txids.clone();
69+
updated = true;
70+
}
6871
}
6972
}
7073

src/payment/store.rs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
use std::time::{Duration, SystemTime, UNIX_EPOCH};
99

10-
use bitcoin::{BlockHash, Txid};
10+
use bitcoin::{BlockHash, Transaction, Txid};
1111
use lightning::ln::channelmanager::PaymentId;
1212
use lightning::ln::msgs::DecodeError;
1313
use lightning::offers::offer::OfferId;
@@ -19,10 +19,6 @@ use lightning::{
1919
use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret};
2020
use lightning_types::string::UntrustedString;
2121

22-
use bitcoin::{BlockHash, Transaction, Txid};
23-
24-
use std::time::{Duration, SystemTime, UNIX_EPOCH};
25-
2622
use crate::data_store::{StorableObject, StorableObjectId, StorableObjectUpdate};
2723
use crate::hex_utils;
2824

@@ -322,6 +318,15 @@ impl StorableObject for PaymentDetails {
322318
}
323319
}
324320

321+
if let Some(tx_id) = update.txid {
322+
match self.kind {
323+
PaymentKind::Onchain { ref mut txid, .. } => {
324+
update_if_necessary!(*txid, tx_id);
325+
},
326+
_ => {},
327+
}
328+
}
329+
325330
if updated {
326331
self.latest_update_timestamp = SystemTime::now()
327332
.duration_since(UNIX_EPOCH)
@@ -583,6 +588,7 @@ pub(crate) struct PaymentDetailsUpdate {
583588
pub raw_tx: Option<Option<Transaction>>,
584589
pub last_broadcast_time: Option<Option<u64>>,
585590
pub broadcast_attempts: Option<Option<u32>>,
591+
pub txid: Option<Txid>,
586592
}
587593

588594
impl PaymentDetailsUpdate {
@@ -601,6 +607,7 @@ impl PaymentDetailsUpdate {
601607
raw_tx: None,
602608
last_broadcast_time: None,
603609
broadcast_attempts: None,
610+
txid: None,
604611
}
605612
}
606613
}
@@ -616,16 +623,23 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate {
616623
_ => (None, None, None),
617624
};
618625

619-
let (confirmation_status, raw_tx, last_broadcast_time, broadcast_attempts) =
626+
let (confirmation_status, raw_tx, last_broadcast_time, broadcast_attempts, txid) =
620627
match &value.kind {
621628
PaymentKind::Onchain {
622629
status,
623630
raw_tx,
624631
last_broadcast_time,
625632
broadcast_attempts,
633+
txid,
626634
..
627-
} => (Some(*status), raw_tx.clone(), *last_broadcast_time, *broadcast_attempts),
628-
_ => (None, None, None, None),
635+
} => (
636+
Some(*status),
637+
raw_tx.clone(),
638+
*last_broadcast_time,
639+
*broadcast_attempts,
640+
Some(*txid),
641+
),
642+
_ => (None, None, None, None, None),
629643
};
630644

631645
let counterparty_skimmed_fee_msat = match value.kind {
@@ -649,6 +663,7 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate {
649663
raw_tx: Some(raw_tx),
650664
last_broadcast_time: Some(last_broadcast_time),
651665
broadcast_attempts: Some(broadcast_attempts),
666+
txid,
652667
}
653668
}
654669
}

0 commit comments

Comments
 (0)