Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,567 changes: 921 additions & 646 deletions Cargo.lock

Large diffs are not rendered by default.

48 changes: 47 additions & 1 deletion ldk-server-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use ldk_server_client::ldk_server_protos::api::{
Bolt11ReceiveRequest, Bolt11SendRequest, Bolt12ReceiveRequest, Bolt12SendRequest,
CloseChannelRequest, ForceCloseChannelRequest, GetBalancesRequest, GetNodeInfoRequest,
ListChannelsRequest, ListPaymentsRequest, OnchainReceiveRequest, OnchainSendRequest,
OpenChannelRequest,
OpenChannelRequest, SpliceInRequest, SpliceOutRequest,
};
use ldk_server_client::ldk_server_protos::types::{
bolt11_invoice_description, Bolt11InvoiceDescription, PageToken, Payment,
Expand Down Expand Up @@ -102,6 +102,24 @@ enum Commands {
#[arg(long)]
announce_channel: bool,
},
SpliceIn {
#[arg(short, long)]
user_channel_id: String,
#[arg(short, long)]
counterparty_node_id: String,
#[arg(long)]
splice_amount_sats: u64,
},
SpliceOut {
#[arg(short, long)]
user_channel_id: String,
#[arg(short, long)]
counterparty_node_id: String,
#[arg(short, long)]
address: Option<String>,
#[arg(long)]
splice_amount_sats: u64,
},
ListChannels,
ListPayments {
#[arg(short, long)]
Expand Down Expand Up @@ -227,6 +245,34 @@ async fn main() {
.await,
);
},
Commands::SpliceIn { user_channel_id, counterparty_node_id, splice_amount_sats } => {
handle_response_result(
client
.splice_in_channel(SpliceInRequest {
user_channel_id,
counterparty_node_id,
splice_amount_sats,
})
.await,
);
},
Commands::SpliceOut {
user_channel_id,
counterparty_node_id,
address,
splice_amount_sats,
} => {
handle_response_result(
client
.splice_out_channel(SpliceOutRequest {
user_channel_id,
counterparty_node_id,
address,
splice_amount_sats,
})
.await,
);
},
Commands::ListChannels => {
handle_response_result(client.list_channels(ListChannelsRequest {}).await);
},
Expand Down
23 changes: 22 additions & 1 deletion ldk-server-client/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ use ldk_server_protos::api::{
GetBalancesRequest, GetBalancesResponse, GetNodeInfoRequest, GetNodeInfoResponse,
ListChannelsRequest, ListChannelsResponse, ListPaymentsRequest, ListPaymentsResponse,
OnchainReceiveRequest, OnchainReceiveResponse, OnchainSendRequest, OnchainSendResponse,
OpenChannelRequest, OpenChannelResponse,
OpenChannelRequest, OpenChannelResponse, SpliceInRequest, SpliceInResponse, SpliceOutRequest,
SpliceOutResponse,
};
use ldk_server_protos::error::{ErrorCode, ErrorResponse};
use reqwest::header::CONTENT_TYPE;
Expand All @@ -28,6 +29,8 @@ const BOLT11_SEND_PATH: &str = "Bolt11Send";
const BOLT12_RECEIVE_PATH: &str = "Bolt12Receive";
const BOLT12_SEND_PATH: &str = "Bolt12Send";
const OPEN_CHANNEL_PATH: &str = "OpenChannel";
const SPLICE_IN_CHANNEL_PATH: &str = "SpliceIn";
const SPLICE_OUT_CHANNEL_PATH: &str = "SpliceOut";
const CLOSE_CHANNEL_PATH: &str = "CloseChannel";
const FORCE_CLOSE_CHANNEL_PATH: &str = "ForceCloseChannel";
const LIST_CHANNELS_PATH: &str = "ListChannels";
Expand Down Expand Up @@ -127,6 +130,24 @@ impl LdkServerClient {
self.post_request(&request, &url).await
}

/// Splices funds into the channel specified by given request.
/// For API contract/usage, refer to docs for [`SpliceInRequest`] and [`SpliceInResponse`].
pub async fn splice_in_channel(
&self, request: SpliceInRequest,
) -> Result<SpliceInResponse, LdkServerError> {
let url = format!("http://{}/{SPLICE_IN_CHANNEL_PATH}", self.base_url);
self.post_request(&request, &url).await
}

/// Splices funds out of the channel specified by given request.
/// For API contract/usage, refer to docs for [`SpliceOutRequest`] and [`SpliceOutResponse`].
pub async fn splice_out_channel(
&self, request: SpliceOutRequest,
) -> Result<SpliceOutResponse, LdkServerError> {
let url = format!("http://{}/{SPLICE_OUT_CHANNEL_PATH}", self.base_url);
self.post_request(&request, &url).await
}

/// Closes the channel specified by given request.
/// For API contract/usage, refer to docs for [`CloseChannelRequest`] and [`CloseChannelResponse`].
pub async fn close_channel(
Expand Down
49 changes: 49 additions & 0 deletions ldk-server-protos/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,55 @@ pub struct OpenChannelResponse {
#[prost(string, tag = "1")]
pub user_channel_id: ::prost::alloc::string::String,
}
/// Increases the channel balance by the given amount.
/// See more: <https://docs.rs/ldk-node/latest/ldk_node/struct.Node.html#method.splice_in_channel>
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct SpliceInRequest {
/// The local `user_channel_id` of the channel.
#[prost(string, tag = "1")]
pub user_channel_id: ::prost::alloc::string::String,
/// The hex-encoded public key of the channel's counterparty node.
#[prost(string, tag = "2")]
pub counterparty_node_id: ::prost::alloc::string::String,
/// The amount of sats to splice into the channel.
#[prost(uint64, tag = "3")]
pub splice_amount_sats: u64,
}
/// The response `content` for the `SpliceIn` API, when HttpStatusCode is OK (200).
/// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`.
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct SpliceInResponse {}
/// Decreases the channel balance by the given amount.
/// See more: <https://docs.rs/ldk-node/latest/ldk_node/struct.Node.html#method.splice_out_channel>
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct SpliceOutRequest {
/// The local `user_channel_id` of this channel.
#[prost(string, tag = "1")]
pub user_channel_id: ::prost::alloc::string::String,
/// The hex-encoded public key of the channel's counterparty node.
#[prost(string, tag = "2")]
pub counterparty_node_id: ::prost::alloc::string::String,
/// A Bitcoin on-chain address to send the spliced-out funds.
///
/// If not set, an address from the node's on-chain wallet will be used.
#[prost(string, optional, tag = "3")]
pub address: ::core::option::Option<::prost::alloc::string::String>,
/// The amount of sats to splice out of the channel.
#[prost(uint64, tag = "4")]
pub splice_amount_sats: u64,
}
/// The response `content` for the `SpliceOut` API, when HttpStatusCode is OK (200).
/// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`.
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct SpliceOutResponse {
/// The Bitcoin on-chain address where the funds will be sent.
#[prost(string, tag = "1")]
pub address: ::prost::alloc::string::String,
}
/// Update the config for a previously opened channel.
/// See more: <https://docs.rs/ldk-node/latest/ldk_node/struct.Node.html#method.update_channel_config>
#[allow(clippy::derive_partial_eq_without_eq)]
Expand Down
45 changes: 45 additions & 0 deletions ldk-server-protos/src/proto/api.proto
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,51 @@ message OpenChannelResponse {
string user_channel_id = 1;
}

// Increases the channel balance by the given amount.
// See more: https://docs.rs/ldk-node/latest/ldk_node/struct.Node.html#method.splice_in_channel
message SpliceInRequest {

// The local `user_channel_id` of the channel.
string user_channel_id = 1;

// The hex-encoded public key of the channel's counterparty node.
string counterparty_node_id = 2;

// The amount of sats to splice into the channel.
uint64 splice_amount_sats = 3;
}

// The response `content` for the `SpliceIn` API, when HttpStatusCode is OK (200).
// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`.
message SpliceInResponse {}

// Decreases the channel balance by the given amount.
// See more: https://docs.rs/ldk-node/latest/ldk_node/struct.Node.html#method.splice_out_channel
message SpliceOutRequest {

// The local `user_channel_id` of this channel.
string user_channel_id = 1;

// The hex-encoded public key of the channel's counterparty node.
string counterparty_node_id = 2;

// A Bitcoin on-chain address to send the spliced-out funds.
//
// If not set, an address from the node's on-chain wallet will be used.
optional string address = 3;

// The amount of sats to splice out of the channel.
uint64 splice_amount_sats = 4;
}

// The response `content` for the `SpliceOut` API, when HttpStatusCode is OK (200).
// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`.
message SpliceOutResponse {

// The Bitcoin on-chain address where the funds will be sent.
string address = 1;
}

// Update the config for a previously opened channel.
// See more: https://docs.rs/ldk-node/latest/ldk_node/struct.Node.html#method.update_channel_config
message UpdateChannelConfigRequest {
Expand Down
6 changes: 6 additions & 0 deletions ldk-server-protos/src/proto/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ message Bolt11Jit {
// See [`LdkChannelConfig::accept_underpaying_htlcs`](https://docs.rs/lightning/latest/lightning/util/config/struct.ChannelConfig.html#structfield.accept_underpaying_htlcs)
// for more information.
LSPFeeLimits lsp_fee_limits = 4;

// The value, in thousands of a satoshi, that was deducted from this payment as an extra
// fee taken by our channel counterparty.
//
// Will only be `Some` once we received the payment.
optional uint64 counterparty_skimmed_fee_msat = 5;
}

// Represents a BOLT 12 ‘offer’ payment, i.e., a payment for an Offer.
Expand Down
6 changes: 6 additions & 0 deletions ldk-server-protos/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,12 @@ pub struct Bolt11Jit {
/// for more information.
#[prost(message, optional, tag = "4")]
pub lsp_fee_limits: ::core::option::Option<LspFeeLimits>,
/// The value, in thousands of a satoshi, that was deducted from this payment as an extra
/// fee taken by our channel counterparty.
///
/// Will only be `Some` once we received the payment.
#[prost(uint64, optional, tag = "5")]
pub counterparty_skimmed_fee_msat: ::core::option::Option<u64>,
}
/// Represents a BOLT 12 ‘offer’ payment, i.e., a payment for an Offer.
#[allow(clippy::derive_partial_eq_without_eq)]
Expand Down
2 changes: 1 addition & 1 deletion ldk-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2021"

[dependencies]
ldk-node = { git = "https://github.com/lightningdevkit/ldk-node.git", rev = "f0338d19256615088fabab2b6927d478ae3ec1a1" }
ldk-node = { git = "https://github.com/jkczyz/ldk-node.git", branch = "2025-10-splicing" }
serde = { version = "1.0.203", default-features = false, features = ["derive"] }
hyper = { version = "1", default-features = false, features = ["server", "http1"] }
http-body-util = { version = "0.1", default-features = false }
Expand Down
5 changes: 4 additions & 1 deletion ldk-server/src/api/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ impl From<NodeError> for LdkServerError {
| NodeError::InvalidNodeAlias
| NodeError::InvalidDateTime
| NodeError::InvalidFeeRate
| NodeError::UriParameterParsingFailed => {
| NodeError::UriParameterParsingFailed
| NodeError::InvalidBlindedPaths
| NodeError::AsyncPaymentServicesDisabled => {
(error.to_string(), LdkServerErrorCode::InvalidRequestError)
},

Expand All @@ -92,6 +94,7 @@ impl From<NodeError> for LdkServerError {
| NodeError::ProbeSendingFailed
| NodeError::ChannelCreationFailed
| NodeError::ChannelClosingFailed
| NodeError::ChannelSplicingFailed
| NodeError::ChannelConfigUpdateFailed
| NodeError::DuplicatePayment
| NodeError::InsufficientFunds
Expand Down
1 change: 1 addition & 0 deletions ldk-server/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ pub(crate) mod list_payments;
pub(crate) mod onchain_receive;
pub(crate) mod onchain_send;
pub(crate) mod open_channel;
pub(crate) mod splice_channel;
pub(crate) mod update_channel_config;
73 changes: 73 additions & 0 deletions ldk-server/src/api/splice_channel.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
use crate::api::error::LdkServerError;
use crate::api::error::LdkServerErrorCode::InvalidRequestError;
use crate::service::Context;
use ldk_node::bitcoin::secp256k1::PublicKey;
use ldk_node::bitcoin::Address;
use ldk_node::UserChannelId;
use ldk_server_protos::api::{
SpliceInRequest, SpliceInResponse, SpliceOutRequest, SpliceOutResponse,
};
use std::str::FromStr;

pub(crate) const SPLICE_IN_PATH: &str = "SpliceIn";

pub(crate) fn handle_splice_in_request(
context: Context, request: SpliceInRequest,
) -> Result<SpliceInResponse, LdkServerError> {
let user_channel_id = parse_user_channel_id(&request.user_channel_id)?;
let counterparty_node_id = parse_counterparty_node_id(&request.counterparty_node_id)?;

context.node.splice_in(&user_channel_id, counterparty_node_id, request.splice_amount_sats)?;

Ok(SpliceInResponse {})
}

pub(crate) const SPLICE_OUT_PATH: &str = "SpliceOut";

pub(crate) fn handle_splice_out_request(
context: Context, request: SpliceOutRequest,
) -> Result<SpliceOutResponse, LdkServerError> {
let user_channel_id = parse_user_channel_id(&request.user_channel_id)?;
let counterparty_node_id = parse_counterparty_node_id(&request.counterparty_node_id)?;

let address = request
.address
.map(|address| {
Address::from_str(&address)
.and_then(|address| address.require_network(context.node.config().network))
.map_err(|_| ldk_node::NodeError::InvalidAddress)
})
.unwrap_or_else(|| context.node.onchain_payment().new_address())
.map_err(|_| {
LdkServerError::new(
InvalidRequestError,
"Address is not valid for LdkServer's configured network.".to_string(),
)
})?;
let address_str = address.to_string();

context.node.splice_out(
&user_channel_id,
counterparty_node_id,
address,
request.splice_amount_sats,
)?;

Ok(SpliceOutResponse { address: address_str })
}

fn parse_user_channel_id(id: &str) -> Result<UserChannelId, LdkServerError> {
let parsed = id.parse::<u128>().map_err(|_| {
LdkServerError::new(InvalidRequestError, "Invalid UserChannelId.".to_string())
})?;
Ok(UserChannelId(parsed))
}

fn parse_counterparty_node_id(id: &str) -> Result<PublicKey, LdkServerError> {
PublicKey::from_str(id).map_err(|e| {
LdkServerError::new(
InvalidRequestError,
format!("Invalid counterparty node ID, error: {}", e),
)
})
}
4 changes: 3 additions & 1 deletion ldk-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ fn main() {
},
};

builder.set_runtime(runtime.handle().clone());

let node = match builder.build() {
Ok(node) => Arc::new(node),
Err(e) => {
Expand Down Expand Up @@ -131,7 +133,7 @@ fn main() {
};

println!("Starting up...");
match node.start_with_runtime(Arc::clone(&runtime)) {
match node.start() {
Ok(()) => {},
Err(e) => {
eprintln!("Failed to start up LDK Node: {}", e);
Expand Down
5 changes: 5 additions & 0 deletions ldk-server/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ use crate::api::list_payments::{handle_list_payments_request, LIST_PAYMENTS_PATH
use crate::api::onchain_receive::{handle_onchain_receive_request, ONCHAIN_RECEIVE_PATH};
use crate::api::onchain_send::{handle_onchain_send_request, ONCHAIN_SEND_PATH};
use crate::api::open_channel::{handle_open_channel, OPEN_CHANNEL_PATH};
use crate::api::splice_channel::{
handle_splice_in_request, handle_splice_out_request, SPLICE_IN_PATH, SPLICE_OUT_PATH,
};
use crate::api::update_channel_config::{
handle_update_channel_config_request, UPDATE_CHANNEL_CONFIG_PATH,
};
Expand Down Expand Up @@ -85,6 +88,8 @@ impl Service<Request<Incoming>> for NodeService {
},
BOLT12_SEND_PATH => Box::pin(handle_request(context, req, handle_bolt12_send_request)),
OPEN_CHANNEL_PATH => Box::pin(handle_request(context, req, handle_open_channel)),
SPLICE_IN_PATH => Box::pin(handle_request(context, req, handle_splice_in_request)),
SPLICE_OUT_PATH => Box::pin(handle_request(context, req, handle_splice_out_request)),
CLOSE_CHANNEL_PATH => {
Box::pin(handle_request(context, req, handle_close_channel_request))
},
Expand Down
Loading
Loading