Skip to content

Commit d14d2ce

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 af45025 commit d14d2ce

3 files changed

Lines changed: 72 additions & 7 deletions

File tree

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: 66 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;
@@ -1331,6 +1331,71 @@ impl Node {
13311331
}
13321332
}
13331333

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

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;
@@ -335,12 +335,10 @@ impl Wallet {
335335
self.get_balances(total_anchor_channels_reserve_sats).map(|(_, s)| s)
336336
}
337337

338-
fn parse_and_validate_address(
339-
&self, network: Network, address: &Address,
340-
) -> Result<Address, Error> {
338+
pub(crate) fn parse_and_validate_address(&self, address: &Address) -> Result<Address, Error> {
341339
Address::<NetworkUnchecked>::from_str(address.to_string().as_str())
342340
.map_err(|_| Error::InvalidAddress)?
343-
.require_network(network)
341+
.require_network(self.config.network)
344342
.map_err(|_| Error::InvalidAddress)
345343
}
346344

@@ -349,7 +347,7 @@ impl Wallet {
349347
&self, address: &bitcoin::Address, send_amount: OnchainSendAmount,
350348
fee_rate: Option<FeeRate>,
351349
) -> Result<Txid, Error> {
352-
self.parse_and_validate_address(self.config.network, &address)?;
350+
self.parse_and_validate_address(&address)?;
353351

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

0 commit comments

Comments
 (0)