Skip to content

Commit 2aa703c

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 0226ff3 commit 2aa703c

File tree

2 files changed

+58
-7
lines changed

2 files changed

+58
-7
lines changed

src/lib.rs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
110110
use crate::scoring::setup_background_pathfinding_scores_sync;
111111
pub use balance::{BalanceDetails, LightningBalance, PendingSweepBalance};
112112
use bitcoin::secp256k1::PublicKey;
113-
use bitcoin::Amount;
113+
use bitcoin::{Address, Amount};
114114
#[cfg(feature = "uniffi")]
115115
pub use builder::ArcedNodeBuilder as Builder;
116116
pub use builder::BuildError;
@@ -1328,6 +1328,59 @@ 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+
pub fn splice_out(
1337+
&self, user_channel_id: &UserChannelId, counterparty_node_id: PublicKey, address: Address,
1338+
splice_amount_sats: u64,
1339+
) -> Result<(), Error> {
1340+
let open_channels =
1341+
self.channel_manager.list_channels_with_counterparty(&counterparty_node_id);
1342+
if let Some(channel_details) =
1343+
open_channels.iter().find(|c| c.user_channel_id == user_channel_id.0)
1344+
{
1345+
if splice_amount_sats > channel_details.outbound_capacity_msat {
1346+
return Err(Error::ChannelSplicingFailed);
1347+
}
1348+
1349+
self.wallet.parse_and_validate_address(&address)?;
1350+
1351+
let contribution = SpliceContribution::SpliceOut {
1352+
outputs: vec![bitcoin::TxOut {
1353+
value: Amount::from_sat(splice_amount_sats),
1354+
script_pubkey: address.script_pubkey(),
1355+
}],
1356+
};
1357+
1358+
let fee_rate = self.wallet.estimate_channel_funding_fee_rate();
1359+
let funding_feerate_per_kw = fee_rate.to_sat_per_kwu().try_into().unwrap_or(u32::MAX);
1360+
1361+
self.channel_manager
1362+
.splice_channel(
1363+
&channel_details.channel_id,
1364+
&counterparty_node_id,
1365+
contribution,
1366+
funding_feerate_per_kw,
1367+
None,
1368+
)
1369+
.map_err(|e| {
1370+
log_error!(self.logger, "Failed to splice channel: {:?}", e);
1371+
Error::ChannelSplicingFailed
1372+
})
1373+
} else {
1374+
log_error!(
1375+
self.logger,
1376+
"Channel not found for user_channel_id: {:?} and counterparty: {}",
1377+
user_channel_id,
1378+
counterparty_node_id
1379+
);
1380+
Err(Error::ChannelSplicingFailed)
1381+
}
1382+
}
1383+
13311384
/// Manually sync the LDK and BDK wallets with the current chain state and update the fee rate
13321385
/// cache.
13331386
///

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)