Skip to content

Commit d059e4b

Browse files
committed
feat: Add RBF and CPFP transaction fee bumping support
Add Replace-By-Fee (RBF) and Child-Pays-For-Parent (CPFP) functionality to allow users to bump fees on stuck transactions. - Add `bump_fee_by_rbf` to replace transactions with higher fee versions - Add `accelerate_by_cpfp` to create child transactions that pay for parent - Add `calculate_cpfp_fee_rate` helper for automatic fee calculation - Add new error variants for transaction fee bumping operations - Expose methods through `OnchainPayment` API - Add UniFFI bindings for RBF/CPFP functionality RBF allows replacing an existing unconfirmed transaction with a new version that pays a higher fee. CPFP allows accelerating a transaction by spending one of its outputs with a high-fee child transaction.
1 parent 7977b04 commit d059e4b

File tree

4 files changed

+580
-3
lines changed

4 files changed

+580
-3
lines changed

bindings/ldk_node.udl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,10 @@ interface OnchainPayment {
222222
Txid send_to_address([ByRef]Address address, u64 amount_sats, FeeRate? fee_rate);
223223
[Throws=NodeError]
224224
Txid send_all_to_address([ByRef]Address address, boolean retain_reserve, FeeRate? fee_rate);
225+
[Throws=NodeError]
226+
Txid bump_fee_by_rbf([ByRef]Txid txid, FeeRate fee_rate);
227+
[Throws=NodeError]
228+
Txid accelerate_by_cpfp([ByRef]Txid txid, FeeRate? fee_rate, Address? destination_address);
225229
};
226230

227231
interface FeeRate {
@@ -302,6 +306,10 @@ enum NodeError {
302306
"InsufficientFunds",
303307
"LiquiditySourceUnavailable",
304308
"LiquidityFeeTooHigh",
309+
"CannotRbfFundingTransaction",
310+
"TransactionNotFound",
311+
"TransactionAlreadyConfirmed",
312+
"NoSpendableOutputs",
305313
};
306314

307315
dictionary NodeStatus {

src/error.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,14 @@ pub enum Error {
120120
LiquiditySourceUnavailable,
121121
/// The given operation failed due to the LSP's required opening fee being too high.
122122
LiquidityFeeTooHigh,
123+
/// Cannot RBF a channel funding transaction.
124+
CannotRbfFundingTransaction,
125+
/// The transaction was not found in the wallet.
126+
TransactionNotFound,
127+
/// The transaction is already confirmed and cannot be modified.
128+
TransactionAlreadyConfirmed,
129+
/// The transaction has no spendable outputs.
130+
NoSpendableOutputs
123131
}
124132

125133
impl fmt::Display for Error {
@@ -193,6 +201,10 @@ impl fmt::Display for Error {
193201
Self::LiquidityFeeTooHigh => {
194202
write!(f, "The given operation failed due to the LSP's required opening fee being too high.")
195203
},
204+
Self::CannotRbfFundingTransaction => write!(f, "Cannot RBF a channel funding transaction."),
205+
Self::TransactionNotFound => write!(f, "The transaction was not found in the wallet."),
206+
Self::TransactionAlreadyConfirmed => write!(f, "The transaction is already confirmed and cannot be modified."),
207+
Self::NoSpendableOutputs => write!(f, "The transaction has no spendable outputs.")
196208
}
197209
}
198210
}

src/payment/onchain.rs

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,4 +122,94 @@ impl OnchainPayment {
122122
let fee_rate_opt = maybe_map_fee_rate_opt!(fee_rate);
123123
self.wallet.send_to_address(address, send_amount, fee_rate_opt)
124124
}
125-
}
125+
126+
/// Bumps the fee of an existing transaction using Replace-By-Fee (RBF).
127+
///
128+
/// This allows a previously sent transaction to be replaced with a new version
129+
/// that pays a higher fee. The original transaction must have been created with
130+
/// RBF enabled (which is the default for transactions created by LDK).
131+
///
132+
/// **Note:** This cannot be used on funding transactions as doing so would invalidate the channel.
133+
///
134+
/// # Arguments
135+
///
136+
/// * `txid` - The transaction ID of the transaction to be replaced
137+
/// * `fee_rate` - The new fee rate to use (must be higher than the original fee rate)
138+
///
139+
/// # Returns
140+
///
141+
/// The transaction ID of the new transaction if successful.
142+
///
143+
/// # Errors
144+
///
145+
/// * [`Error::NotRunning`] - If the node is not running
146+
/// * [`Error::TransactionNotFound`] - If the transaction can't be found in the wallet
147+
/// * [`Error::TransactionAlreadyConfirmed`] - If the transaction is already confirmed
148+
/// * [`Error::CannotRbfFundingTransaction`] - If the transaction is a channel funding transaction
149+
/// * [`Error::InvalidFeeRate`] - If the new fee rate is not higher than the original
150+
/// * [`Error::OnchainTxCreationFailed`] - If the new transaction couldn't be created
151+
pub fn bump_fee_by_rbf(&self, txid: &Txid, fee_rate: FeeRate) -> Result<Txid, Error> {
152+
let rt_lock = self.runtime.read().unwrap();
153+
if rt_lock.is_none() {
154+
return Err(Error::NotRunning);
155+
}
156+
157+
// Pass through to the wallet implementation
158+
#[cfg(not(feature = "uniffi"))]
159+
let fee_rate_param = fee_rate;
160+
#[cfg(feature = "uniffi")]
161+
let fee_rate_param = *fee_rate;
162+
163+
self.wallet.bump_fee_by_rbf(txid, fee_rate_param, &self.channel_manager)
164+
}
165+
166+
/// Accelerates confirmation of a transaction using Child-Pays-For-Parent (CPFP).
167+
///
168+
/// This creates a new transaction (child) that spends an output from the
169+
/// transaction to be accelerated (parent), with a high enough fee to pay for both.
170+
///
171+
/// # Arguments
172+
///
173+
/// * `txid` - The transaction ID of the transaction to be accelerated
174+
/// * `fee_rate` - The fee rate to use for the child transaction (or None to calculate automatically)
175+
/// * `destination_address` - Optional address to send the funds to (if None, funds are sent to an internal address)
176+
///
177+
/// # Returns
178+
///
179+
/// The transaction ID of the child transaction if successful.
180+
///
181+
/// # Errors
182+
///
183+
/// * [`Error::NotRunning`] - If the node is not running
184+
/// * [`Error::TransactionNotFound`] - If the transaction can't be found
185+
/// * [`Error::TransactionAlreadyConfirmed`] - If the transaction is already confirmed
186+
/// * [`Error::NoSpendableOutputs`] - If the transaction has no spendable outputs
187+
/// * [`Error::OnchainTxCreationFailed`] - If the child transaction couldn't be created
188+
pub fn accelerate_by_cpfp(
189+
&self,
190+
txid: &Txid,
191+
fee_rate: Option<FeeRate>,
192+
destination_address: Option<Address>,
193+
) -> Result<Txid, Error> {
194+
let rt_lock = self.runtime.read().unwrap();
195+
if rt_lock.is_none() {
196+
return Err(Error::NotRunning);
197+
}
198+
199+
// Calculate fee rate if not provided
200+
#[cfg(not(feature = "uniffi"))]
201+
let fee_rate_param = match fee_rate {
202+
Some(rate) => rate,
203+
None => self.wallet.calculate_cpfp_fee_rate(txid, true)?,
204+
};
205+
206+
#[cfg(feature = "uniffi")]
207+
let fee_rate_param = match fee_rate {
208+
Some(rate) => *rate,
209+
None => self.wallet.calculate_cpfp_fee_rate(txid, true)?,
210+
};
211+
212+
// Pass through to the wallet implementation
213+
self.wallet.accelerate_by_cpfp(txid, fee_rate_param, destination_address)
214+
}
215+
}

0 commit comments

Comments
 (0)