Skip to content

Commit 0c3d2e0

Browse files
committed
Add splicing RPCs and CLI commands
1 parent a88e44d commit 0c3d2e0

File tree

8 files changed

+242
-4
lines changed

8 files changed

+242
-4
lines changed

ldk-server-cli/src/main.rs

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use ldk_server_client::ldk_server_protos::api::{
88
Bolt11ReceiveRequest, Bolt11SendRequest, Bolt12ReceiveRequest, Bolt12SendRequest,
99
CloseChannelRequest, ForceCloseChannelRequest, GetBalancesRequest, GetNodeInfoRequest,
1010
ListChannelsRequest, ListPaymentsRequest, OnchainReceiveRequest, OnchainSendRequest,
11-
OpenChannelRequest,
11+
OpenChannelRequest, SpliceInRequest, SpliceOutRequest,
1212
};
1313
use ldk_server_client::ldk_server_protos::types::{
1414
bolt11_invoice_description, Bolt11InvoiceDescription, PageToken, Payment,
@@ -102,6 +102,24 @@ enum Commands {
102102
#[arg(long)]
103103
announce_channel: bool,
104104
},
105+
SpliceIn {
106+
#[arg(short, long)]
107+
user_channel_id: String,
108+
#[arg(short, long)]
109+
counterparty_node_id: String,
110+
#[arg(long)]
111+
splice_amount_sats: u64,
112+
},
113+
SpliceOut {
114+
#[arg(short, long)]
115+
user_channel_id: String,
116+
#[arg(short, long)]
117+
counterparty_node_id: String,
118+
#[arg(short, long)]
119+
address: Option<String>,
120+
#[arg(long)]
121+
splice_amount_sats: u64,
122+
},
105123
ListChannels,
106124
ListPayments {
107125
#[arg(short, long)]
@@ -227,6 +245,34 @@ async fn main() {
227245
.await,
228246
);
229247
},
248+
Commands::SpliceIn { user_channel_id, counterparty_node_id, splice_amount_sats } => {
249+
handle_response_result(
250+
client
251+
.splice_in_channel(SpliceInRequest {
252+
user_channel_id,
253+
counterparty_node_id,
254+
splice_amount_sats,
255+
})
256+
.await,
257+
);
258+
},
259+
Commands::SpliceOut {
260+
user_channel_id,
261+
counterparty_node_id,
262+
address,
263+
splice_amount_sats,
264+
} => {
265+
handle_response_result(
266+
client
267+
.splice_out_channel(SpliceOutRequest {
268+
user_channel_id,
269+
counterparty_node_id,
270+
address,
271+
splice_amount_sats,
272+
})
273+
.await,
274+
);
275+
},
230276
Commands::ListChannels => {
231277
handle_response_result(client.list_channels(ListChannelsRequest {}).await);
232278
},

ldk-server-client/src/client.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@ use ldk_server_protos::api::{
1111
GetBalancesRequest, GetBalancesResponse, GetNodeInfoRequest, GetNodeInfoResponse,
1212
ListChannelsRequest, ListChannelsResponse, ListPaymentsRequest, ListPaymentsResponse,
1313
OnchainReceiveRequest, OnchainReceiveResponse, OnchainSendRequest, OnchainSendResponse,
14-
OpenChannelRequest, OpenChannelResponse,
14+
OpenChannelRequest, OpenChannelResponse, SpliceInRequest, SpliceInResponse, SpliceOutRequest,
15+
SpliceOutResponse,
1516
};
1617
use ldk_server_protos::endpoints::{
1718
BOLT11_RECEIVE_PATH, BOLT11_SEND_PATH, BOLT12_RECEIVE_PATH, BOLT12_SEND_PATH,
1819
CLOSE_CHANNEL_PATH, FORCE_CLOSE_CHANNEL_PATH, GET_BALANCES_PATH, GET_NODE_INFO_PATH,
1920
LIST_CHANNELS_PATH, LIST_PAYMENTS_PATH, ONCHAIN_RECEIVE_PATH, ONCHAIN_SEND_PATH,
20-
OPEN_CHANNEL_PATH,
21+
OPEN_CHANNEL_PATH, SPLICE_IN_CHANNEL_PATH, SPLICE_OUT_CHANNEL_PATH,
2122
};
2223
use ldk_server_protos::error::{ErrorCode, ErrorResponse};
2324
use reqwest::header::CONTENT_TYPE;
@@ -119,6 +120,24 @@ impl LdkServerClient {
119120
self.post_request(&request, &url).await
120121
}
121122

123+
/// Splices funds into the channel specified by given request.
124+
/// For API contract/usage, refer to docs for [`SpliceInRequest`] and [`SpliceInResponse`].
125+
pub async fn splice_in_channel(
126+
&self, request: SpliceInRequest,
127+
) -> Result<SpliceInResponse, LdkServerError> {
128+
let url = format!("http://{}/{SPLICE_IN_CHANNEL_PATH}", self.base_url);
129+
self.post_request(&request, &url).await
130+
}
131+
132+
/// Splices funds out of the channel specified by given request.
133+
/// For API contract/usage, refer to docs for [`SpliceOutRequest`] and [`SpliceOutResponse`].
134+
pub async fn splice_out_channel(
135+
&self, request: SpliceOutRequest,
136+
) -> Result<SpliceOutResponse, LdkServerError> {
137+
let url = format!("http://{}/{SPLICE_OUT_CHANNEL_PATH}", self.base_url);
138+
self.post_request(&request, &url).await
139+
}
140+
122141
/// Closes the channel specified by given request.
123142
/// For API contract/usage, refer to docs for [`CloseChannelRequest`] and [`CloseChannelResponse`].
124143
pub async fn close_channel(

ldk-server-protos/src/api.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,55 @@ pub struct OpenChannelResponse {
249249
#[prost(string, tag = "1")]
250250
pub user_channel_id: ::prost::alloc::string::String,
251251
}
252+
/// Increases the channel balance by the given amount.
253+
/// See more: <https://docs.rs/ldk-node/latest/ldk_node/struct.Node.html#method.splice_in_channel>
254+
#[allow(clippy::derive_partial_eq_without_eq)]
255+
#[derive(Clone, PartialEq, ::prost::Message)]
256+
pub struct SpliceInRequest {
257+
/// The local `user_channel_id` of the channel.
258+
#[prost(string, tag = "1")]
259+
pub user_channel_id: ::prost::alloc::string::String,
260+
/// The hex-encoded public key of the channel's counterparty node.
261+
#[prost(string, tag = "2")]
262+
pub counterparty_node_id: ::prost::alloc::string::String,
263+
/// The amount of sats to splice into the channel.
264+
#[prost(uint64, tag = "3")]
265+
pub splice_amount_sats: u64,
266+
}
267+
/// The response `content` for the `SpliceIn` API, when HttpStatusCode is OK (200).
268+
/// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`.
269+
#[allow(clippy::derive_partial_eq_without_eq)]
270+
#[derive(Clone, PartialEq, ::prost::Message)]
271+
pub struct SpliceInResponse {}
272+
/// Decreases the channel balance by the given amount.
273+
/// See more: <https://docs.rs/ldk-node/latest/ldk_node/struct.Node.html#method.splice_out_channel>
274+
#[allow(clippy::derive_partial_eq_without_eq)]
275+
#[derive(Clone, PartialEq, ::prost::Message)]
276+
pub struct SpliceOutRequest {
277+
/// The local `user_channel_id` of this channel.
278+
#[prost(string, tag = "1")]
279+
pub user_channel_id: ::prost::alloc::string::String,
280+
/// The hex-encoded public key of the channel's counterparty node.
281+
#[prost(string, tag = "2")]
282+
pub counterparty_node_id: ::prost::alloc::string::String,
283+
/// A Bitcoin on-chain address to send the spliced-out funds.
284+
///
285+
/// If not set, an address from the node's on-chain wallet will be used.
286+
#[prost(string, optional, tag = "3")]
287+
pub address: ::core::option::Option<::prost::alloc::string::String>,
288+
/// The amount of sats to splice out of the channel.
289+
#[prost(uint64, tag = "4")]
290+
pub splice_amount_sats: u64,
291+
}
292+
/// The response `content` for the `SpliceOut` API, when HttpStatusCode is OK (200).
293+
/// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`.
294+
#[allow(clippy::derive_partial_eq_without_eq)]
295+
#[derive(Clone, PartialEq, ::prost::Message)]
296+
pub struct SpliceOutResponse {
297+
/// The Bitcoin on-chain address where the funds will be sent.
298+
#[prost(string, tag = "1")]
299+
pub address: ::prost::alloc::string::String,
300+
}
252301
/// Update the config for a previously opened channel.
253302
/// See more: <https://docs.rs/ldk-node/latest/ldk_node/struct.Node.html#method.update_channel_config>
254303
#[allow(clippy::derive_partial_eq_without_eq)]

ldk-server-protos/src/endpoints.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ pub const BOLT11_SEND_PATH: &str = "Bolt11Send";
77
pub const BOLT12_RECEIVE_PATH: &str = "Bolt12Receive";
88
pub const BOLT12_SEND_PATH: &str = "Bolt12Send";
99
pub const OPEN_CHANNEL_PATH: &str = "OpenChannel";
10+
pub const SPLICE_IN_CHANNEL_PATH: &str = "SpliceIn";
11+
pub const SPLICE_OUT_CHANNEL_PATH: &str = "SpliceOut";
1012
pub const CLOSE_CHANNEL_PATH: &str = "CloseChannel";
1113
pub const FORCE_CLOSE_CHANNEL_PATH: &str = "ForceCloseChannel";
1214
pub const LIST_CHANNELS_PATH: &str = "ListChannels";

ldk-server-protos/src/proto/api.proto

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,51 @@ message OpenChannelResponse {
241241
string user_channel_id = 1;
242242
}
243243

244+
// Increases the channel balance by the given amount.
245+
// See more: https://docs.rs/ldk-node/latest/ldk_node/struct.Node.html#method.splice_in_channel
246+
message SpliceInRequest {
247+
248+
// The local `user_channel_id` of the channel.
249+
string user_channel_id = 1;
250+
251+
// The hex-encoded public key of the channel's counterparty node.
252+
string counterparty_node_id = 2;
253+
254+
// The amount of sats to splice into the channel.
255+
uint64 splice_amount_sats = 3;
256+
}
257+
258+
// The response `content` for the `SpliceIn` API, when HttpStatusCode is OK (200).
259+
// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`.
260+
message SpliceInResponse {}
261+
262+
// Decreases the channel balance by the given amount.
263+
// See more: https://docs.rs/ldk-node/latest/ldk_node/struct.Node.html#method.splice_out_channel
264+
message SpliceOutRequest {
265+
266+
// The local `user_channel_id` of this channel.
267+
string user_channel_id = 1;
268+
269+
// The hex-encoded public key of the channel's counterparty node.
270+
string counterparty_node_id = 2;
271+
272+
// A Bitcoin on-chain address to send the spliced-out funds.
273+
//
274+
// If not set, an address from the node's on-chain wallet will be used.
275+
optional string address = 3;
276+
277+
// The amount of sats to splice out of the channel.
278+
uint64 splice_amount_sats = 4;
279+
}
280+
281+
// The response `content` for the `SpliceOut` API, when HttpStatusCode is OK (200).
282+
// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`.
283+
message SpliceOutResponse {
284+
285+
// The Bitcoin on-chain address where the funds will be sent.
286+
string address = 1;
287+
}
288+
244289
// Update the config for a previously opened channel.
245290
// See more: https://docs.rs/ldk-node/latest/ldk_node/struct.Node.html#method.update_channel_config
246291
message UpdateChannelConfigRequest {

ldk-server/src/api/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ pub(crate) mod list_payments;
1313
pub(crate) mod onchain_receive;
1414
pub(crate) mod onchain_send;
1515
pub(crate) mod open_channel;
16+
pub(crate) mod splice_channel;
1617
pub(crate) mod update_channel_config;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
use crate::api::error::LdkServerError;
2+
use crate::api::error::LdkServerErrorCode::InvalidRequestError;
3+
use crate::service::Context;
4+
use ldk_node::bitcoin::secp256k1::PublicKey;
5+
use ldk_node::bitcoin::Address;
6+
use ldk_node::UserChannelId;
7+
use ldk_server_protos::api::{
8+
SpliceInRequest, SpliceInResponse, SpliceOutRequest, SpliceOutResponse,
9+
};
10+
use std::str::FromStr;
11+
12+
pub(crate) const SPLICE_IN_PATH: &str = "SpliceIn";
13+
14+
pub(crate) fn handle_splice_in_request(
15+
context: Context, request: SpliceInRequest,
16+
) -> Result<SpliceInResponse, LdkServerError> {
17+
let user_channel_id = parse_user_channel_id(&request.user_channel_id)?;
18+
let counterparty_node_id = parse_counterparty_node_id(&request.counterparty_node_id)?;
19+
20+
context.node.splice_in(&user_channel_id, counterparty_node_id, request.splice_amount_sats)?;
21+
22+
Ok(SpliceInResponse {})
23+
}
24+
25+
pub(crate) const SPLICE_OUT_PATH: &str = "SpliceOut";
26+
27+
pub(crate) fn handle_splice_out_request(
28+
context: Context, request: SpliceOutRequest,
29+
) -> Result<SpliceOutResponse, LdkServerError> {
30+
let user_channel_id = parse_user_channel_id(&request.user_channel_id)?;
31+
let counterparty_node_id = parse_counterparty_node_id(&request.counterparty_node_id)?;
32+
33+
let address = request
34+
.address
35+
.map(|address| {
36+
Address::from_str(&address)
37+
.and_then(|address| address.require_network(context.node.config().network))
38+
.map_err(|_| ldk_node::NodeError::InvalidAddress)
39+
})
40+
.unwrap_or_else(|| context.node.onchain_payment().new_address())
41+
.map_err(|_| {
42+
LdkServerError::new(
43+
InvalidRequestError,
44+
"Address is not valid for LdkServer's configured network.".to_string(),
45+
)
46+
})?;
47+
48+
context.node.splice_out(
49+
&user_channel_id,
50+
counterparty_node_id,
51+
&address,
52+
request.splice_amount_sats,
53+
)?;
54+
55+
Ok(SpliceOutResponse { address: address.to_string() })
56+
}
57+
58+
fn parse_user_channel_id(id: &str) -> Result<UserChannelId, LdkServerError> {
59+
let parsed = id.parse::<u128>().map_err(|_| {
60+
LdkServerError::new(InvalidRequestError, "Invalid UserChannelId.".to_string())
61+
})?;
62+
Ok(UserChannelId(parsed))
63+
}
64+
65+
fn parse_counterparty_node_id(id: &str) -> Result<PublicKey, LdkServerError> {
66+
PublicKey::from_str(id).map_err(|e| {
67+
LdkServerError::new(
68+
InvalidRequestError,
69+
format!("Invalid counterparty node ID, error: {}", e),
70+
)
71+
})
72+
}

ldk-server/src/service.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ use ldk_server_protos::endpoints::{
99
BOLT11_RECEIVE_PATH, BOLT11_SEND_PATH, BOLT12_RECEIVE_PATH, BOLT12_SEND_PATH,
1010
CLOSE_CHANNEL_PATH, FORCE_CLOSE_CHANNEL_PATH, GET_BALANCES_PATH, GET_NODE_INFO_PATH,
1111
GET_PAYMENT_DETAILS_PATH, LIST_CHANNELS_PATH, LIST_FORWARDED_PAYMENTS_PATH, LIST_PAYMENTS_PATH,
12-
ONCHAIN_RECEIVE_PATH, ONCHAIN_SEND_PATH, OPEN_CHANNEL_PATH, UPDATE_CHANNEL_CONFIG_PATH,
12+
ONCHAIN_RECEIVE_PATH, ONCHAIN_SEND_PATH, OPEN_CHANNEL_PATH, SPLICE_IN_CHANNEL_PATH,
13+
SPLICE_OUT_CHANNEL_PATH, UPDATE_CHANNEL_CONFIG_PATH,
1314
};
1415

1516
use prost::Message;
@@ -30,6 +31,7 @@ use crate::api::list_payments::handle_list_payments_request;
3031
use crate::api::onchain_receive::handle_onchain_receive_request;
3132
use crate::api::onchain_send::handle_onchain_send_request;
3233
use crate::api::open_channel::handle_open_channel;
34+
use crate::api::splice_channel::{handle_splice_in_request, handle_splice_out_request};
3335
use crate::api::update_channel_config::handle_update_channel_config_request;
3436
use crate::io::persist::paginated_kv_store::PaginatedKVStore;
3537
use crate::util::proto_adapter::to_error_response;
@@ -91,6 +93,8 @@ impl Service<Request<Incoming>> for NodeService {
9193
},
9294
BOLT12_SEND_PATH => Box::pin(handle_request(context, req, handle_bolt12_send_request)),
9395
OPEN_CHANNEL_PATH => Box::pin(handle_request(context, req, handle_open_channel)),
96+
SPLICE_IN_CHANNEL_PATH => Box::pin(handle_request(context, req, handle_splice_in_request)),
97+
SPLICE_OUT_CHANNEL_PATH => Box::pin(handle_request(context, req, handle_splice_out_request)),
9498
CLOSE_CHANNEL_PATH => {
9599
Box::pin(handle_request(context, req, handle_close_channel_request))
96100
},

0 commit comments

Comments
 (0)