Skip to content

Commit 237bc16

Browse files
committed
Add ability to open channel with all on-chain funds
Uses the same fee estimation logic that `send_all_to_address` uses which can be hack but it works today. Tests generated with claude.
1 parent 80fb49b commit 237bc16

5 files changed

Lines changed: 348 additions & 25 deletions

File tree

bindings/ldk_node.udl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,10 @@ interface Node {
173173
[Throws=NodeError]
174174
UserChannelId open_announced_channel(PublicKey node_id, SocketAddress address, u64 channel_amount_sats, u64? push_to_counterparty_msat, ChannelConfig? channel_config);
175175
[Throws=NodeError]
176+
UserChannelId open_channel_with_all(PublicKey node_id, SocketAddress address, u64? push_to_counterparty_msat, ChannelConfig? channel_config);
177+
[Throws=NodeError]
178+
UserChannelId open_announced_channel_with_all(PublicKey node_id, SocketAddress address, u64? push_to_counterparty_msat, ChannelConfig? channel_config);
179+
[Throws=NodeError]
176180
void splice_in([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id, u64 splice_amount_sats);
177181
[Throws=NodeError]
178182
void splice_out([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id, [ByRef]Address address, u64 splice_amount_sats);

src/lib.rs

Lines changed: 121 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1091,7 +1091,7 @@ impl Node {
10911091
}
10921092

10931093
fn open_channel_inner(
1094-
&self, node_id: PublicKey, address: SocketAddress, channel_amount_sats: u64,
1094+
&self, node_id: PublicKey, address: SocketAddress, channel_amount_sats: Option<u64>,
10951095
push_to_counterparty_msat: Option<u64>, channel_config: Option<ChannelConfig>,
10961096
announce_for_forwarding: bool,
10971097
) -> Result<UserChannelId, Error> {
@@ -1111,8 +1111,38 @@ impl Node {
11111111
con_cm.connect_peer_if_necessary(con_node_id, con_addr).await
11121112
})?;
11131113

1114-
// Check funds availability after connection (includes anchor reserve calculation)
1115-
self.check_sufficient_funds_for_channel(channel_amount_sats, &node_id)?;
1114+
let channel_amount_sats = match channel_amount_sats {
1115+
Some(amount) => {
1116+
// Check funds availability after connection (includes anchor reserve
1117+
// calculation).
1118+
self.check_sufficient_funds_for_channel(amount, &peer_info.node_id)?;
1119+
amount
1120+
},
1121+
None => {
1122+
// Determine max funding amount from all available on-chain funds.
1123+
let cur_anchor_reserve_sats =
1124+
total_anchor_channels_reserve_sats(&self.channel_manager, &self.config);
1125+
let new_channel_reserve =
1126+
self.new_channel_anchor_reserve_sats(&peer_info.node_id)?;
1127+
let total_anchor_reserve_sats = cur_anchor_reserve_sats + new_channel_reserve;
1128+
1129+
let fee_rate =
1130+
self.fee_estimator.estimate_fee_rate(ConfirmationTarget::ChannelFunding);
1131+
1132+
let amount =
1133+
self.wallet.get_max_funding_amount(total_anchor_reserve_sats, fee_rate)?;
1134+
1135+
log_info!(
1136+
self.logger,
1137+
"Opening channel with all balance: {}sats (fee rate: {} sat/kw, anchor reserve: {}sats)",
1138+
amount,
1139+
fee_rate.to_sat_per_kwu(),
1140+
total_anchor_reserve_sats,
1141+
);
1142+
1143+
amount
1144+
},
1145+
};
11161146

11171147
let mut user_config = default_user_config(&self.config);
11181148
user_config.channel_handshake_config.announce_for_forwarding = announce_for_forwarding;
@@ -1153,6 +1183,25 @@ impl Node {
11531183
}
11541184
}
11551185

1186+
fn new_channel_anchor_reserve_sats(&self, peer_node_id: &PublicKey) -> Result<u64, Error> {
1187+
let init_features = self
1188+
.peer_manager
1189+
.peer_by_node_id(peer_node_id)
1190+
.ok_or(Error::ConnectionFailed)?
1191+
.init_features;
1192+
let sats = self.config.anchor_channels_config.as_ref().map_or(0, |c| {
1193+
if init_features.requires_anchors_zero_fee_htlc_tx()
1194+
&& !c.trusted_peers_no_reserve.contains(peer_node_id)
1195+
{
1196+
c.per_channel_reserve_sats
1197+
} else {
1198+
0
1199+
}
1200+
});
1201+
1202+
Ok(sats)
1203+
}
1204+
11561205
fn check_sufficient_funds_for_channel(
11571206
&self, amount_sats: u64, peer_node_id: &PublicKey,
11581207
) -> Result<(), Error> {
@@ -1171,21 +1220,8 @@ impl Node {
11711220
}
11721221

11731222
// Fail if we have less than the channel value + anchor reserve available (if applicable).
1174-
let init_features = self
1175-
.peer_manager
1176-
.peer_by_node_id(peer_node_id)
1177-
.ok_or(Error::ConnectionFailed)?
1178-
.init_features;
1179-
let required_funds_sats = amount_sats
1180-
+ self.config.anchor_channels_config.as_ref().map_or(0, |c| {
1181-
if init_features.requires_anchors_zero_fee_htlc_tx()
1182-
&& !c.trusted_peers_no_reserve.contains(peer_node_id)
1183-
{
1184-
c.per_channel_reserve_sats
1185-
} else {
1186-
0
1187-
}
1188-
});
1223+
let required_funds_sats =
1224+
amount_sats + self.new_channel_anchor_reserve_sats(peer_node_id)?;
11891225

11901226
if spendable_amount_sats < required_funds_sats {
11911227
log_error!(self.logger,
@@ -1222,7 +1258,7 @@ impl Node {
12221258
self.open_channel_inner(
12231259
node_id,
12241260
address,
1225-
channel_amount_sats,
1261+
Some(channel_amount_sats),
12261262
push_to_counterparty_msat,
12271263
channel_config,
12281264
false,
@@ -1262,7 +1298,72 @@ impl Node {
12621298
self.open_channel_inner(
12631299
node_id,
12641300
address,
1265-
channel_amount_sats,
1301+
Some(channel_amount_sats),
1302+
push_to_counterparty_msat,
1303+
channel_config,
1304+
true,
1305+
)
1306+
}
1307+
1308+
/// Connect to a node and open a new unannounced channel, using all available on-chain funds
1309+
/// minus fees and anchor reserves.
1310+
///
1311+
/// To open an announced channel, see [`Node::open_announced_channel_with_all`].
1312+
///
1313+
/// Disconnects and reconnects are handled automatically.
1314+
///
1315+
/// If `push_to_counterparty_msat` is set, the given value will be pushed (read: sent) to the
1316+
/// channel counterparty on channel open. This can be useful to start out with the balance not
1317+
/// entirely shifted to one side, therefore allowing to receive payments from the getgo.
1318+
///
1319+
/// Returns a [`UserChannelId`] allowing to locally keep track of the channel.
1320+
///
1321+
/// [`AnchorChannelsConfig::per_channel_reserve_sats`]: crate::config::AnchorChannelsConfig::per_channel_reserve_sats
1322+
pub fn open_channel_with_all(
1323+
&self, node_id: PublicKey, address: SocketAddress, push_to_counterparty_msat: Option<u64>,
1324+
channel_config: Option<ChannelConfig>,
1325+
) -> Result<UserChannelId, Error> {
1326+
self.open_channel_inner(
1327+
node_id,
1328+
address,
1329+
None,
1330+
push_to_counterparty_msat,
1331+
channel_config,
1332+
false,
1333+
)
1334+
}
1335+
1336+
/// Connect to a node and open a new announced channel, using all available on-chain funds
1337+
/// minus fees and anchor reserves.
1338+
///
1339+
/// This will return an error if the node has not been sufficiently configured to operate as a
1340+
/// forwarding node that can properly announce its existence to the public network graph, i.e.,
1341+
/// [`Config::listening_addresses`] and [`Config::node_alias`] are unset.
1342+
///
1343+
/// To open an unannounced channel, see [`Node::open_channel_with_all`].
1344+
///
1345+
/// Disconnects and reconnects are handled automatically.
1346+
///
1347+
/// If `push_to_counterparty_msat` is set, the given value will be pushed (read: sent) to the
1348+
/// channel counterparty on channel open. This can be useful to start out with the balance not
1349+
/// entirely shifted to one side, therefore allowing to receive payments from the getgo.
1350+
///
1351+
/// Returns a [`UserChannelId`] allowing to locally keep track of the channel.
1352+
///
1353+
/// [`AnchorChannelsConfig::per_channel_reserve_sats`]: crate::config::AnchorChannelsConfig::per_channel_reserve_sats
1354+
pub fn open_announced_channel_with_all(
1355+
&self, node_id: PublicKey, address: SocketAddress, push_to_counterparty_msat: Option<u64>,
1356+
channel_config: Option<ChannelConfig>,
1357+
) -> Result<UserChannelId, Error> {
1358+
if let Err(err) = may_announce_channel(&self.config) {
1359+
log_error!(self.logger, "Failed to open announced channel as the node hasn't been sufficiently configured to act as a forwarding node: {err}");
1360+
return Err(Error::ChannelCreationFailed);
1361+
}
1362+
1363+
self.open_channel_inner(
1364+
node_id,
1365+
address,
1366+
None,
12661367
push_to_counterparty_msat,
12671368
channel_config,
12681369
true,

src/wallet/mod.rs

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ pub(crate) enum OnchainSendAmount {
6565
pub(crate) mod persist;
6666
pub(crate) mod ser;
6767

68+
const DUST_LIMIT_SATS: u64 = 546;
69+
6870
pub(crate) struct Wallet {
6971
// A BDK on-chain wallet.
7072
inner: Mutex<PersistedWallet<KVStoreWalletPersister>>,
@@ -459,6 +461,87 @@ impl Wallet {
459461
self.get_balances(total_anchor_channels_reserve_sats).map(|(_, s)| s)
460462
}
461463

464+
/// Returns the maximum amount available for funding a channel, accounting for on-chain fees
465+
/// and anchor reserves.
466+
///
467+
/// Uses a two-pass approach: first builds a temporary transaction to estimate fees, then
468+
/// returns the spendable balance minus those fees.
469+
pub(crate) fn get_max_funding_amount(
470+
&self, cur_anchor_reserve_sats: u64, fee_rate: FeeRate,
471+
) -> Result<u64, Error> {
472+
let mut locked_wallet = self.inner.lock().unwrap();
473+
let balance = locked_wallet.balance();
474+
let spendable_amount_sats =
475+
self.get_balances_inner(balance, cur_anchor_reserve_sats).map(|(_, s)| s).unwrap_or(0);
476+
477+
if spendable_amount_sats == 0 {
478+
log_error!(
479+
self.logger,
480+
"Unable to determine max funding amount: no spendable funds available."
481+
);
482+
return Err(Error::InsufficientFunds);
483+
}
484+
485+
// Use a dummy P2WSH script (34 bytes) to match the size of a real funding output.
486+
let dummy_p2wsh_script = ScriptBuf::new().to_p2wsh();
487+
488+
let tmp_tx = if cur_anchor_reserve_sats > DUST_LIMIT_SATS {
489+
let change_address_info = locked_wallet.peek_address(KeychainKind::Internal, 0);
490+
let mut tmp_tx_builder = locked_wallet.build_tx();
491+
tmp_tx_builder
492+
.drain_wallet()
493+
.drain_to(dummy_p2wsh_script)
494+
.add_recipient(
495+
change_address_info.address.script_pubkey(),
496+
Amount::from_sat(cur_anchor_reserve_sats),
497+
)
498+
.fee_rate(fee_rate);
499+
match tmp_tx_builder.finish() {
500+
Ok(psbt) => psbt.unsigned_tx,
501+
Err(err) => {
502+
log_error!(
503+
self.logger,
504+
"Failed to create temporary transaction for fee estimation: {err}"
505+
);
506+
return Err(err.into());
507+
},
508+
}
509+
} else {
510+
let mut tmp_tx_builder = locked_wallet.build_tx();
511+
tmp_tx_builder.drain_wallet().drain_to(dummy_p2wsh_script).fee_rate(fee_rate);
512+
match tmp_tx_builder.finish() {
513+
Ok(psbt) => psbt.unsigned_tx,
514+
Err(err) => {
515+
log_error!(
516+
self.logger,
517+
"Failed to create temporary transaction for fee estimation: {err}"
518+
);
519+
return Err(err.into());
520+
},
521+
}
522+
};
523+
524+
let estimated_tx_fee = locked_wallet.calculate_fee(&tmp_tx).map_err(|e| {
525+
log_error!(self.logger, "Failed to calculate fee of temporary transaction: {e}");
526+
e
527+
})?;
528+
529+
// Cancel the temporary transaction to free up any used change addresses.
530+
locked_wallet.cancel_tx(&tmp_tx);
531+
532+
let max_amount = spendable_amount_sats.saturating_sub(estimated_tx_fee.to_sat());
533+
534+
if max_amount < DUST_LIMIT_SATS {
535+
log_error!(
536+
self.logger,
537+
"Unable to open channel: available funds would be consumed entirely by fees. Available: {spendable_amount_sats}sats, estimated fee: {estimated_tx_fee}sats.",
538+
);
539+
return Err(Error::InsufficientFunds);
540+
}
541+
542+
Ok(max_amount)
543+
}
544+
462545
pub(crate) fn parse_and_validate_address(&self, address: &Address) -> Result<Address, Error> {
463546
Address::<NetworkUnchecked>::from_str(address.to_string().as_str())
464547
.map_err(|_| Error::InvalidAddress)?
@@ -482,7 +565,6 @@ impl Wallet {
482565
let mut locked_wallet = self.inner.lock().unwrap();
483566

484567
// Prepare the tx_builder. We properly check the reserve requirements (again) further down.
485-
const DUST_LIMIT_SATS: u64 = 546;
486568
let tx_builder = match send_amount {
487569
OnchainSendAmount::ExactRetainingReserve { amount_sats, .. } => {
488570
let mut tx_builder = locked_wallet.build_tx();

tests/common/mod.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,38 @@ pub async fn open_channel_push_amt(
682682
funding_txo_a
683683
}
684684

685+
pub async fn open_channel_with_all(
686+
node_a: &TestNode, node_b: &TestNode, should_announce: bool, electrsd: &ElectrsD,
687+
) -> OutPoint {
688+
if should_announce {
689+
node_a
690+
.open_announced_channel_with_all(
691+
node_b.node_id(),
692+
node_b.listening_addresses().unwrap().first().unwrap().clone(),
693+
None,
694+
None,
695+
)
696+
.unwrap();
697+
} else {
698+
node_a
699+
.open_channel_with_all(
700+
node_b.node_id(),
701+
node_b.listening_addresses().unwrap().first().unwrap().clone(),
702+
None,
703+
None,
704+
)
705+
.unwrap();
706+
}
707+
assert!(node_a.list_peers().iter().find(|c| { c.node_id == node_b.node_id() }).is_some());
708+
709+
let funding_txo_a = expect_channel_pending_event!(node_a, node_b.node_id());
710+
let funding_txo_b = expect_channel_pending_event!(node_b, node_a.node_id());
711+
assert_eq!(funding_txo_a, funding_txo_b);
712+
wait_for_tx(&electrsd.client, funding_txo_a.txid).await;
713+
714+
funding_txo_a
715+
}
716+
685717
pub(crate) async fn do_channel_full_cycle<E: ElectrumApi>(
686718
node_a: TestNode, node_b: TestNode, bitcoind: &BitcoindClient, electrsd: &E, allow_0conf: bool,
687719
expect_anchor_channel: bool, force_close: bool,

0 commit comments

Comments
 (0)