Skip to content

Commit ed38998

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 4bd360c commit ed38998

File tree

3 files changed

+72
-7
lines changed

3 files changed

+72
-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: 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;
@@ -1328,6 +1328,71 @@ impl Node {
13281328
}
13291329
}
13301330

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

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)