Skip to content

Commit c4afd11

Browse files
committed
Allow to override fee rates for onchain payments
We allow to override our fee estimator in the `send_to_address` and `send_all_to_address` API methods. To this end, we implement a bindings-compatible wrapper around `bitcoin::FeeRate`.
1 parent 03c4ab0 commit c4afd11

File tree

6 files changed

+171
-14
lines changed

6 files changed

+171
-14
lines changed

bindings/ldk_node.udl

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,26 @@ interface OnchainPayment {
161161
[Throws=NodeError]
162162
Address new_address();
163163
[Throws=NodeError]
164-
Txid send_to_address([ByRef]Address address, u64 amount_sats);
164+
Txid send_to_address([ByRef]Address address, u64 amount_sats, FeeRate? fee_rate);
165165
[Throws=NodeError]
166-
Txid send_all_to_address([ByRef]Address address, boolean retain_reserve);
166+
Txid send_all_to_address([ByRef]Address address, boolean retain_reserve, FeeRate? fee_rate);
167+
};
168+
169+
interface FeeRate {
170+
[Name=from_sat_per_kwu]
171+
constructor(u64 sat_kwu);
172+
[Name=from_sat_per_vb, Throws=FeeRateError]
173+
constructor(u64 sat_vb);
174+
[Name=from_sat_per_vb_unchecked]
175+
constructor(u64 sat_vb);
176+
u64 as_sat_per_kwu();
177+
u64 as_sat_per_vb_floor();
178+
u64 as_sat_per_vb_ceil();
179+
};
180+
181+
[Error]
182+
enum FeeRateError {
183+
"ConversionError",
167184
};
168185

169186
interface UnifiedQrPayment {

src/payment/onchain.rs

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ use bitcoin::{Address, Txid};
1717

1818
use std::sync::{Arc, RwLock};
1919

20+
#[cfg(not(feature = "uniffi"))]
21+
type FeeRate = bitcoin::FeeRate;
22+
#[cfg(feature = "uniffi")]
23+
type FeeRate = crate::uniffi_types::FeeRate;
24+
2025
/// A payment handler allowing to send and receive on-chain payments.
2126
///
2227
/// Should be retrieved by calling [`Node::onchain_payment`].
@@ -50,9 +55,13 @@ impl OnchainPayment {
5055
/// This will respect any on-chain reserve we need to keep, i.e., won't allow to cut into
5156
/// [`BalanceDetails::total_anchor_channels_reserve_sats`].
5257
///
58+
/// If `fee_rate` is set it will used on the resulting transaction. Otherwise a reasonable
59+
/// we'll retrieve an estimate from the configured chain source.
60+
///
5361
/// [`BalanceDetails::total_anchor_channels_reserve_sats`]: crate::BalanceDetails::total_anchor_channels_reserve_sats
62+
#[cfg(not(feature = "uniffi"))]
5463
pub fn send_to_address(
55-
&self, address: &bitcoin::Address, amount_sats: u64,
64+
&self, address: &bitcoin::Address, amount_sats: u64, fee_rate: Option<FeeRate>,
5665
) -> Result<Txid, Error> {
5766
let rt_lock = self.runtime.read().unwrap();
5867
if rt_lock.is_none() {
@@ -63,7 +72,32 @@ impl OnchainPayment {
6372
crate::total_anchor_channels_reserve_sats(&self.channel_manager, &self.config);
6473
let send_amount =
6574
OnchainSendAmount::ExactRetainingReserve { amount_sats, cur_anchor_reserve_sats };
66-
self.wallet.send_to_address(address, send_amount)
75+
self.wallet.send_to_address(address, send_amount, fee_rate)
76+
}
77+
78+
/// Send an on-chain payment to the given address.
79+
///
80+
/// This will respect any on-chain reserve we need to keep, i.e., won't allow to cut into
81+
/// [`BalanceDetails::total_anchor_channels_reserve_sats`].
82+
///
83+
/// If `fee_rate` is set it will used on the resulting transaction. Otherwise a reasonable
84+
/// we'll retrieve an estimate from the configured chain source.
85+
///
86+
/// [`BalanceDetails::total_anchor_channels_reserve_sats`]: crate::BalanceDetails::total_anchor_channels_reserve_sats
87+
#[cfg(feature = "uniffi")]
88+
pub fn send_to_address(
89+
&self, address: &bitcoin::Address, amount_sats: u64, fee_rate: Option<Arc<FeeRate>>,
90+
) -> Result<Txid, Error> {
91+
let rt_lock = self.runtime.read().unwrap();
92+
if rt_lock.is_none() {
93+
return Err(Error::NotRunning);
94+
}
95+
96+
let cur_anchor_reserve_sats =
97+
crate::total_anchor_channels_reserve_sats(&self.channel_manager, &self.config);
98+
let send_amount =
99+
OnchainSendAmount::ExactRetainingReserve { amount_sats, cur_anchor_reserve_sats };
100+
self.wallet.send_to_address(address, send_amount, fee_rate.map(|f| f.0))
67101
}
68102

69103
/// Send an on-chain payment to the given address, draining the available funds.
@@ -77,9 +111,48 @@ impl OnchainPayment {
77111
/// will try to send all spendable onchain funds, i.e.,
78112
/// [`BalanceDetails::spendable_onchain_balance_sats`].
79113
///
114+
/// If `fee_rate` is set it will be used on the resulting transaction. Otherwise a reasonable
115+
/// we'll retrieve an estimate from the configured chain source.
116+
///
117+
/// [`BalanceDetails::spendable_onchain_balance_sats`]: crate::balance::BalanceDetails::spendable_onchain_balance_sats
118+
#[cfg(not(feature = "uniffi"))]
119+
pub fn send_all_to_address(
120+
&self, address: &bitcoin::Address, retain_reserves: bool, fee_rate: Option<FeeRate>,
121+
) -> Result<Txid, Error> {
122+
let rt_lock = self.runtime.read().unwrap();
123+
if rt_lock.is_none() {
124+
return Err(Error::NotRunning);
125+
}
126+
127+
let send_amount = if retain_reserves {
128+
let cur_anchor_reserve_sats =
129+
crate::total_anchor_channels_reserve_sats(&self.channel_manager, &self.config);
130+
OnchainSendAmount::AllRetainingReserve { cur_anchor_reserve_sats }
131+
} else {
132+
OnchainSendAmount::AllDrainingReserve
133+
};
134+
135+
self.wallet.send_to_address(address, send_amount, fee_rate)
136+
}
137+
138+
/// Send an on-chain payment to the given address, draining the available funds.
139+
///
140+
/// This is useful if you have closed all channels and want to migrate funds to another
141+
/// on-chain wallet.
142+
///
143+
/// Please note that if `retain_reserves` is set to `false` this will **not** retain any on-chain reserves, which might be potentially
144+
/// dangerous if you have open Anchor channels for which you can't trust the counterparty to
145+
/// spend the Anchor output after channel closure. If `retain_reserves` is set to `true`, this
146+
/// will try to send all spendable onchain funds, i.e.,
147+
/// [`BalanceDetails::spendable_onchain_balance_sats`].
148+
///
149+
/// If `fee_rate` is set it will be used on the resulting transaction. Otherwise a reasonable
150+
/// we'll retrieve an estimate from the configured chain source.
151+
///
80152
/// [`BalanceDetails::spendable_onchain_balance_sats`]: crate::balance::BalanceDetails::spendable_onchain_balance_sats
153+
#[cfg(feature = "uniffi")]
81154
pub fn send_all_to_address(
82-
&self, address: &bitcoin::Address, retain_reserves: bool,
155+
&self, address: &bitcoin::Address, retain_reserves: bool, fee_rate: Option<Arc<FeeRate>>,
83156
) -> Result<Txid, Error> {
84157
let rt_lock = self.runtime.read().unwrap();
85158
if rt_lock.is_none() {
@@ -94,6 +167,6 @@ impl OnchainPayment {
94167
OnchainSendAmount::AllDrainingReserve
95168
};
96169

97-
self.wallet.send_to_address(address, send_amount)
170+
self.wallet.send_to_address(address, send_amount, fee_rate.map(|f| f.0))
98171
}
99172
}

src/payment/unified_qr.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,11 @@ impl UnifiedQrPayment {
156156
},
157157
};
158158

159-
let txid =
160-
self.onchain_payment.send_to_address(&uri_network_checked.address, amount.to_sat())?;
159+
let txid = self.onchain_payment.send_to_address(
160+
&uri_network_checked.address,
161+
amount.to_sat(),
162+
None,
163+
)?;
161164

162165
Ok(QrPaymentResult::Onchain { txid })
163166
}

src/uniffi_types.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ use lightning::util::ser::Writeable;
5151
use lightning_invoice::SignedRawBolt11Invoice;
5252

5353
use std::convert::TryInto;
54+
use std::fmt;
5455
use std::str::FromStr;
5556

5657
impl UniffiCustomTypeConverter for PublicKey {
@@ -345,3 +346,61 @@ impl UniffiCustomTypeConverter for NodeAlias {
345346
obj.to_string()
346347
}
347348
}
349+
350+
/// Represents fee rate.
351+
///
352+
/// This is a simple wrapper around [`bitcoin::FeeRate`], only used for UniFFI bindings.
353+
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
354+
pub struct FeeRate(pub(crate) bitcoin::FeeRate);
355+
356+
impl FeeRate {
357+
/// Constructs `FeeRate` from satoshis per 1000 weight units.
358+
pub const fn from_sat_per_kwu(sat_kwu: u64) -> Self {
359+
Self(bitcoin::FeeRate::from_sat_per_kwu(sat_kwu))
360+
}
361+
362+
/// Constructs `FeeRate` from satoshis per virtual bytes.
363+
///
364+
/// # Errors
365+
///
366+
/// Returns `None` on arithmetic overflow.
367+
pub fn from_sat_per_vb(sat_vb: u64) -> Result<Self, FeeRateError> {
368+
Ok(Self(bitcoin::FeeRate::from_sat_per_vb(sat_vb).ok_or(FeeRateError::ConversionError)?))
369+
}
370+
371+
/// Constructs `FeeRate` from satoshis per virtual bytes without overflow check.
372+
pub const fn from_sat_per_vb_unchecked(sat_vb: u64) -> Self {
373+
Self(bitcoin::FeeRate::from_sat_per_vb_unchecked(sat_vb))
374+
}
375+
376+
/// Returns raw fee rate as satoshis per 1000 weight units.
377+
pub const fn as_sat_per_kwu(&self) -> u64 {
378+
self.0.to_sat_per_kwu()
379+
}
380+
381+
/// Converts to sat/vB rounding down.
382+
pub const fn as_sat_per_vb_floor(&self) -> u64 {
383+
self.0.to_sat_per_vb_floor()
384+
}
385+
386+
/// Converts to sat/vB rounding up.
387+
pub const fn as_sat_per_vb_ceil(self) -> u64 {
388+
self.0.to_sat_per_vb_ceil()
389+
}
390+
}
391+
392+
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
393+
/// A fee rate error that possibly needs to be handled by the user.
394+
pub enum FeeRateError {
395+
ConversionError,
396+
}
397+
398+
impl std::error::Error for FeeRateError {}
399+
400+
impl fmt::Display for FeeRateError {
401+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
402+
match self {
403+
Self::ConversionError => write!(f, "Fee-rate conversion failed."),
404+
}
405+
}
406+
}

src/wallet/mod.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ use bitcoin::secp256k1::ecdh::SharedSecret;
3939
use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature};
4040
use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey, Signing};
4141
use bitcoin::{
42-
Amount, ScriptBuf, Transaction, TxOut, Txid, WPubkeyHash, WitnessProgram, WitnessVersion,
42+
Amount, FeeRate, ScriptBuf, Transaction, TxOut, Txid, WPubkeyHash, WitnessProgram,
43+
WitnessVersion,
4344
};
4445

4546
use std::ops::Deref;
@@ -239,9 +240,12 @@ where
239240

240241
pub(crate) fn send_to_address(
241242
&self, address: &bitcoin::Address, send_amount: OnchainSendAmount,
243+
fee_rate: Option<FeeRate>,
242244
) -> Result<Txid, Error> {
245+
// Use the set fee_rate or default to fee estimation.
243246
let confirmation_target = ConfirmationTarget::OnchainPayment;
244-
let fee_rate = self.fee_estimator.estimate_fee_rate(confirmation_target);
247+
let fee_rate =
248+
fee_rate.unwrap_or(self.fee_estimator.estimate_fee_rate(confirmation_target));
245249

246250
let tx = {
247251
let mut locked_wallet = self.inner.lock().unwrap();

tests/integration_tests_rust.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -315,11 +315,12 @@ fn onchain_spend_receive() {
315315

316316
assert_eq!(
317317
Err(NodeError::InsufficientFunds),
318-
node_a.onchain_payment().send_to_address(&addr_b, expected_node_a_balance + 1)
318+
node_a.onchain_payment().send_to_address(&addr_b, expected_node_a_balance + 1, None)
319319
);
320320

321321
let amount_to_send_sats = 1000;
322-
let txid = node_b.onchain_payment().send_to_address(&addr_a, amount_to_send_sats).unwrap();
322+
let txid =
323+
node_b.onchain_payment().send_to_address(&addr_a, amount_to_send_sats, None).unwrap();
323324
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6);
324325
wait_for_tx(&electrsd.client, txid);
325326

@@ -334,7 +335,7 @@ fn onchain_spend_receive() {
334335
assert!(node_b.list_balances().spendable_onchain_balance_sats < expected_node_b_balance_upper);
335336

336337
let addr_b = node_b.onchain_payment().new_address().unwrap();
337-
let txid = node_a.onchain_payment().send_all_to_address(&addr_b, true).unwrap();
338+
let txid = node_a.onchain_payment().send_all_to_address(&addr_b, true, None).unwrap();
338339
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6);
339340
wait_for_tx(&electrsd.client, txid);
340341

@@ -350,7 +351,7 @@ fn onchain_spend_receive() {
350351
assert!(node_b.list_balances().spendable_onchain_balance_sats < expected_node_b_balance_upper);
351352

352353
let addr_b = node_b.onchain_payment().new_address().unwrap();
353-
let txid = node_a.onchain_payment().send_all_to_address(&addr_b, false).unwrap();
354+
let txid = node_a.onchain_payment().send_all_to_address(&addr_b, false, None).unwrap();
354355
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6);
355356
wait_for_tx(&electrsd.client, txid);
356357

0 commit comments

Comments
 (0)