diff --git a/ldk-server-cli/src/main.rs b/ldk-server-cli/src/main.rs index a45ec6e..caa43e7 100644 --- a/ldk-server-cli/src/main.rs +++ b/ldk-server-cli/src/main.rs @@ -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, @@ -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, + #[arg(long)] + splice_amount_sats: u64, + }, ListChannels, ListPayments { #[arg(short, long)] @@ -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(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(SpliceOutRequest { + user_channel_id, + counterparty_node_id, + address, + splice_amount_sats, + }) + .await, + ); + }, Commands::ListChannels => { handle_response_result(client.list_channels(ListChannelsRequest {}).await); }, diff --git a/ldk-server-client/src/client.rs b/ldk-server-client/src/client.rs index 0b081a2..237264b 100644 --- a/ldk-server-client/src/client.rs +++ b/ldk-server-client/src/client.rs @@ -11,13 +11,14 @@ 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::endpoints::{ BOLT11_RECEIVE_PATH, BOLT11_SEND_PATH, BOLT12_RECEIVE_PATH, BOLT12_SEND_PATH, CLOSE_CHANNEL_PATH, FORCE_CLOSE_CHANNEL_PATH, GET_BALANCES_PATH, GET_NODE_INFO_PATH, LIST_CHANNELS_PATH, LIST_PAYMENTS_PATH, ONCHAIN_RECEIVE_PATH, ONCHAIN_SEND_PATH, - OPEN_CHANNEL_PATH, + OPEN_CHANNEL_PATH, SPLICE_IN_PATH, SPLICE_OUT_PATH, }; use ldk_server_protos::error::{ErrorCode, ErrorResponse}; use reqwest::header::CONTENT_TYPE; @@ -119,6 +120,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( + &self, request: SpliceInRequest, + ) -> Result { + let url = format!("http://{}/{SPLICE_IN_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( + &self, request: SpliceOutRequest, + ) -> Result { + let url = format!("http://{}/{SPLICE_OUT_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( diff --git a/ldk-server-protos/src/api.rs b/ldk-server-protos/src/api.rs index d89283b..ebd4ade 100644 --- a/ldk-server-protos/src/api.rs +++ b/ldk-server-protos/src/api.rs @@ -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: +#[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: +#[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: #[allow(clippy::derive_partial_eq_without_eq)] diff --git a/ldk-server-protos/src/endpoints.rs b/ldk-server-protos/src/endpoints.rs index ef0c465..d599018 100644 --- a/ldk-server-protos/src/endpoints.rs +++ b/ldk-server-protos/src/endpoints.rs @@ -7,6 +7,8 @@ pub const BOLT11_SEND_PATH: &str = "Bolt11Send"; pub const BOLT12_RECEIVE_PATH: &str = "Bolt12Receive"; pub const BOLT12_SEND_PATH: &str = "Bolt12Send"; pub const OPEN_CHANNEL_PATH: &str = "OpenChannel"; +pub const SPLICE_IN_PATH: &str = "SpliceIn"; +pub const SPLICE_OUT_PATH: &str = "SpliceOut"; pub const CLOSE_CHANNEL_PATH: &str = "CloseChannel"; pub const FORCE_CLOSE_CHANNEL_PATH: &str = "ForceCloseChannel"; pub const LIST_CHANNELS_PATH: &str = "ListChannels"; diff --git a/ldk-server-protos/src/proto/api.proto b/ldk-server-protos/src/proto/api.proto index 32be3c3..60c1ab4 100644 --- a/ldk-server-protos/src/proto/api.proto +++ b/ldk-server-protos/src/proto/api.proto @@ -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 +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 +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 { diff --git a/ldk-server/src/api/mod.rs b/ldk-server/src/api/mod.rs index c1b0fa2..f80a4f5 100644 --- a/ldk-server/src/api/mod.rs +++ b/ldk-server/src/api/mod.rs @@ -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; diff --git a/ldk-server/src/api/onchain_send.rs b/ldk-server/src/api/onchain_send.rs index d169cb1..b84c671 100644 --- a/ldk-server/src/api/onchain_send.rs +++ b/ldk-server/src/api/onchain_send.rs @@ -14,7 +14,7 @@ pub(crate) fn handle_onchain_send_request( .map_err(|_| { LdkServerError::new( InvalidRequestError, - "Address is not valid for LdkServer's configured network.".to_string(), + "Address is not valid for the configured network.".to_string(), ) })?; diff --git a/ldk-server/src/api/splice_channel.rs b/ldk-server/src/api/splice_channel.rs new file mode 100644 index 0000000..cab9210 --- /dev/null +++ b/ldk-server/src/api/splice_channel.rs @@ -0,0 +1,68 @@ +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) fn handle_splice_in_request( + context: Context, request: SpliceInRequest, +) -> Result { + 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) fn handle_splice_out_request( + context: Context, request: SpliceOutRequest, +) -> Result { + 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 the configured network.".to_string(), + ) + })?; + + context.node.splice_out( + &user_channel_id, + counterparty_node_id, + &address, + request.splice_amount_sats, + )?; + + Ok(SpliceOutResponse { address: address.to_string() }) +} + +fn parse_user_channel_id(id: &str) -> Result { + let parsed = id.parse::().map_err(|_| { + LdkServerError::new(InvalidRequestError, "Invalid UserChannelId.".to_string()) + })?; + Ok(UserChannelId(parsed)) +} + +fn parse_counterparty_node_id(id: &str) -> Result { + PublicKey::from_str(id).map_err(|e| { + LdkServerError::new( + InvalidRequestError, + format!("Invalid counterparty node ID, error: {}", e), + ) + }) +} diff --git a/ldk-server/src/service.rs b/ldk-server/src/service.rs index f71380b..b1144a7 100644 --- a/ldk-server/src/service.rs +++ b/ldk-server/src/service.rs @@ -9,7 +9,8 @@ use ldk_server_protos::endpoints::{ BOLT11_RECEIVE_PATH, BOLT11_SEND_PATH, BOLT12_RECEIVE_PATH, BOLT12_SEND_PATH, CLOSE_CHANNEL_PATH, FORCE_CLOSE_CHANNEL_PATH, GET_BALANCES_PATH, GET_NODE_INFO_PATH, GET_PAYMENT_DETAILS_PATH, LIST_CHANNELS_PATH, LIST_FORWARDED_PAYMENTS_PATH, LIST_PAYMENTS_PATH, - ONCHAIN_RECEIVE_PATH, ONCHAIN_SEND_PATH, OPEN_CHANNEL_PATH, UPDATE_CHANNEL_CONFIG_PATH, + ONCHAIN_RECEIVE_PATH, ONCHAIN_SEND_PATH, OPEN_CHANNEL_PATH, SPLICE_IN_PATH, SPLICE_OUT_PATH, + UPDATE_CHANNEL_CONFIG_PATH, }; use prost::Message; @@ -30,6 +31,7 @@ use crate::api::list_payments::handle_list_payments_request; use crate::api::onchain_receive::handle_onchain_receive_request; use crate::api::onchain_send::handle_onchain_send_request; use crate::api::open_channel::handle_open_channel; +use crate::api::splice_channel::{handle_splice_in_request, handle_splice_out_request}; use crate::api::update_channel_config::handle_update_channel_config_request; use crate::io::persist::paginated_kv_store::PaginatedKVStore; use crate::util::proto_adapter::to_error_response; @@ -91,6 +93,8 @@ impl Service> 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)) },