Skip to content

Commit 35f1632

Browse files
committed
Add Node::splice_out method
Instead of closing and re-opening a channel when on-chain funds are needed, splicing allows removing funds (splice-out) while keeping the channel operational. This commit implements splice-out sending funds to a user-provided on-chain address.
1 parent 6a8c29d commit 35f1632

File tree

3 files changed

+73
-7
lines changed

3 files changed

+73
-7
lines changed

bindings/ldk_node.udl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ interface Node {
143143
[Throws=NodeError]
144144
void splice_in([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id, u64 splice_amount_sats);
145145
[Throws=NodeError]
146+
void splice_out([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id, [ByRef]Address address, u64 splice_amount_sats);
147+
[Throws=NodeError]
146148
void close_channel([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id);
147149
[Throws=NodeError]
148150
void force_close_channel([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id, string? reason);

src/lib.rs

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
109109

110110
pub use balance::{BalanceDetails, LightningBalance, PendingSweepBalance};
111111
use bitcoin::secp256k1::PublicKey;
112-
use bitcoin::Amount;
112+
use bitcoin::{Address, Amount};
113113
#[cfg(feature = "uniffi")]
114114
pub use builder::ArcedNodeBuilder as Builder;
115115
pub use builder::BuildError;
@@ -1339,6 +1339,72 @@ impl Node {
13391339
}
13401340
}
13411341

1342+
/// Remove funds from an existing channel, sending them to an on-chain address.
1343+
///
1344+
/// This provides for decreasing a channel's outbound liquidity without re-balancing or closing
1345+
/// it. Once negotiation with the counterparty is complete, the channel remains operational
1346+
/// while waiting for a new funding transaction to confirm.
1347+
///
1348+
/// # Experimental API
1349+
///
1350+
/// This API is experimental. Currently, a splice-out will be marked as an inbound payment if
1351+
/// paid to an address associated with the on-chain wallet, but this classification may change
1352+
/// in the future.
1353+
pub fn splice_out(
1354+
&self, user_channel_id: &UserChannelId, counterparty_node_id: PublicKey, address: &Address,
1355+
splice_amount_sats: u64,
1356+
) -> Result<(), Error> {
1357+
let open_channels =
1358+
self.channel_manager.list_channels_with_counterparty(&counterparty_node_id);
1359+
if let Some(channel_details) =
1360+
open_channels.iter().find(|c| c.user_channel_id == user_channel_id.0)
1361+
{
1362+
if splice_amount_sats > channel_details.outbound_capacity_msat {
1363+
return Err(Error::ChannelSplicingFailed);
1364+
}
1365+
1366+
self.wallet.parse_and_validate_address(address)?;
1367+
1368+
let contribution = SpliceContribution::SpliceOut {
1369+
outputs: vec![bitcoin::TxOut {
1370+
value: Amount::from_sat(splice_amount_sats),
1371+
script_pubkey: address.script_pubkey(),
1372+
}],
1373+
};
1374+
1375+
let fee_rate = self.fee_estimator.estimate_fee_rate(ConfirmationTarget::ChannelFunding);
1376+
let funding_feerate_per_kw: u32 = match fee_rate.to_sat_per_kwu().try_into() {
1377+
Ok(fee_rate) => fee_rate,
1378+
Err(_) => {
1379+
debug_assert!(false, "FeeRate should always fit within u32");
1380+
log_error!(self.logger, "FeeRate should always fit within u32");
1381+
fee_estimator::get_fallback_rate_for_target(ConfirmationTarget::ChannelFunding)
1382+
},
1383+
};
1384+
1385+
self.channel_manager
1386+
.splice_channel(
1387+
&channel_details.channel_id,
1388+
&counterparty_node_id,
1389+
contribution,
1390+
funding_feerate_per_kw,
1391+
None,
1392+
)
1393+
.map_err(|e| {
1394+
log_error!(self.logger, "Failed to splice channel: {:?}", e);
1395+
Error::ChannelSplicingFailed
1396+
})
1397+
} else {
1398+
log_error!(
1399+
self.logger,
1400+
"Channel not found for user_channel_id {} and counterparty {}",
1401+
user_channel_id,
1402+
counterparty_node_id
1403+
);
1404+
Err(Error::ChannelSplicingFailed)
1405+
}
1406+
}
1407+
13421408
/// Manually sync the LDK and BDK wallets with the current chain state and update the fee rate
13431409
/// cache.
13441410
///

src/wallet/mod.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use bitcoin::secp256k1::ecdh::SharedSecret;
2626
use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature};
2727
use bitcoin::secp256k1::{All, PublicKey, Scalar, Secp256k1, SecretKey};
2828
use bitcoin::{
29-
Address, Amount, FeeRate, Network, ScriptBuf, Transaction, TxOut, Txid, WPubkeyHash, Weight,
29+
Address, Amount, FeeRate, ScriptBuf, Transaction, TxOut, Txid, WPubkeyHash, Weight,
3030
WitnessProgram, WitnessVersion,
3131
};
3232
use lightning::chain::chaininterface::BroadcasterInterface;
@@ -348,12 +348,10 @@ impl Wallet {
348348
self.get_balances(total_anchor_channels_reserve_sats).map(|(_, s)| s)
349349
}
350350

351-
fn parse_and_validate_address(
352-
&self, network: Network, address: &Address,
353-
) -> Result<Address, Error> {
351+
pub(crate) fn parse_and_validate_address(&self, address: &Address) -> Result<Address, Error> {
354352
Address::<NetworkUnchecked>::from_str(address.to_string().as_str())
355353
.map_err(|_| Error::InvalidAddress)?
356-
.require_network(network)
354+
.require_network(self.config.network)
357355
.map_err(|_| Error::InvalidAddress)
358356
}
359357

@@ -362,7 +360,7 @@ impl Wallet {
362360
&self, address: &bitcoin::Address, send_amount: OnchainSendAmount,
363361
fee_rate: Option<FeeRate>,
364362
) -> Result<Txid, Error> {
365-
self.parse_and_validate_address(self.config.network, &address)?;
363+
self.parse_and_validate_address(&address)?;
366364

367365
// Use the set fee_rate or default to fee estimation.
368366
let confirmation_target = ConfirmationTarget::OnchainPayment;

0 commit comments

Comments
 (0)