From ff1c2712e99c64ed9de439748e9b2a25146041fc Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Thu, 22 Jan 2026 09:08:43 +0100 Subject: [PATCH] Add ConnectPeer API endpoint Adds a new endpoint to connect to a peer on the Lightning Network without opening a channel. This is useful for establishing connections before channel operations or for maintaining peer connectivity. The endpoint accepts node_pubkey, address, and an optional persist flag that defaults to true for automatic reconnection on restart. Also adds client library and CLI support for the new endpoint. Co-Authored-By: Claude Opus 4.5 --- ldk-server-cli/src/main.rs | 35 ++++++++++++++++++++++++------ ldk-server-client/src/client.rs | 33 ++++++++++++++++++---------- ldk-server-protos/src/api.rs | 26 ++++++++++++++++++++++ ldk-server-protos/src/endpoints.rs | 1 + ldk-server/src/api/connect_peer.rs | 30 +++++++++++++++++++++++++ ldk-server/src/api/mod.rs | 1 + ldk-server/src/service.rs | 12 ++++++---- 7 files changed, 115 insertions(+), 23 deletions(-) create mode 100644 ldk-server/src/api/connect_peer.rs diff --git a/ldk-server-cli/src/main.rs b/ldk-server-cli/src/main.rs index 488ca10..1b0d19e 100644 --- a/ldk-server-cli/src/main.rs +++ b/ldk-server-cli/src/main.rs @@ -23,13 +23,13 @@ use ldk_server_client::error::LdkServerErrorCode::{ use ldk_server_client::ldk_server_protos::api::{ Bolt11ReceiveRequest, Bolt11ReceiveResponse, Bolt11SendRequest, Bolt11SendResponse, Bolt12ReceiveRequest, Bolt12ReceiveResponse, Bolt12SendRequest, Bolt12SendResponse, - CloseChannelRequest, CloseChannelResponse, ForceCloseChannelRequest, ForceCloseChannelResponse, - GetBalancesRequest, GetBalancesResponse, GetNodeInfoRequest, GetNodeInfoResponse, - GetPaymentDetailsRequest, GetPaymentDetailsResponse, ListChannelsRequest, ListChannelsResponse, - ListForwardedPaymentsRequest, ListPaymentsRequest, OnchainReceiveRequest, - OnchainReceiveResponse, OnchainSendRequest, OnchainSendResponse, OpenChannelRequest, - OpenChannelResponse, SpliceInRequest, SpliceInResponse, SpliceOutRequest, SpliceOutResponse, - UpdateChannelConfigRequest, UpdateChannelConfigResponse, + CloseChannelRequest, CloseChannelResponse, ConnectPeerRequest, ConnectPeerResponse, + ForceCloseChannelRequest, ForceCloseChannelResponse, GetBalancesRequest, GetBalancesResponse, + GetNodeInfoRequest, GetNodeInfoResponse, GetPaymentDetailsRequest, GetPaymentDetailsResponse, + ListChannelsRequest, ListChannelsResponse, ListForwardedPaymentsRequest, ListPaymentsRequest, + OnchainReceiveRequest, OnchainReceiveResponse, OnchainSendRequest, OnchainSendResponse, + OpenChannelRequest, OpenChannelResponse, SpliceInRequest, SpliceInResponse, SpliceOutRequest, + SpliceOutResponse, UpdateChannelConfigRequest, UpdateChannelConfigResponse, }; use ldk_server_client::ldk_server_protos::types::{ bolt11_invoice_description, Bolt11InvoiceDescription, ChannelConfig, PageToken, @@ -336,6 +336,22 @@ enum Commands { )] cltv_expiry_delta: Option, }, + #[command(about = "Connect to a peer on the Lightning Network without opening a channel")] + ConnectPeer { + #[arg(short, long, help = "The hex-encoded public key of the node to connect to")] + node_pubkey: String, + #[arg( + short, + long, + help = "Address to connect to remote peer (IPv4:port, IPv6:port, OnionV3:port, or hostname:port)" + )] + address: String, + #[arg( + long, + help = "Whether to persist the connection for automatic reconnection on restart (default: true)" + )] + persist: Option, + }, #[command(about = "Generate shell completions for the CLI")] Completions { #[arg( @@ -673,6 +689,11 @@ async fn main() { .await, ); }, + Commands::ConnectPeer { node_pubkey, address, persist } => { + handle_response_result::<_, ConnectPeerResponse>( + client.connect_peer(ConnectPeerRequest { node_pubkey, address, persist }).await, + ); + }, Commands::Completions { .. } => unreachable!("Handled above"), } } diff --git a/ldk-server-client/src/client.rs b/ldk-server-client/src/client.rs index ab3d2c2..0d137db 100644 --- a/ldk-server-client/src/client.rs +++ b/ldk-server-client/src/client.rs @@ -14,21 +14,21 @@ use bitcoin_hashes::{sha256, Hash, HashEngine}; use ldk_server_protos::api::{ Bolt11ReceiveRequest, Bolt11ReceiveResponse, Bolt11SendRequest, Bolt11SendResponse, Bolt12ReceiveRequest, Bolt12ReceiveResponse, Bolt12SendRequest, Bolt12SendResponse, - CloseChannelRequest, CloseChannelResponse, ForceCloseChannelRequest, ForceCloseChannelResponse, - GetBalancesRequest, GetBalancesResponse, GetNodeInfoRequest, GetNodeInfoResponse, - GetPaymentDetailsRequest, GetPaymentDetailsResponse, ListChannelsRequest, ListChannelsResponse, - ListForwardedPaymentsRequest, ListForwardedPaymentsResponse, ListPaymentsRequest, - ListPaymentsResponse, OnchainReceiveRequest, OnchainReceiveResponse, OnchainSendRequest, - OnchainSendResponse, OpenChannelRequest, OpenChannelResponse, SpliceInRequest, - SpliceInResponse, SpliceOutRequest, SpliceOutResponse, UpdateChannelConfigRequest, - UpdateChannelConfigResponse, + CloseChannelRequest, CloseChannelResponse, ConnectPeerRequest, ConnectPeerResponse, + ForceCloseChannelRequest, ForceCloseChannelResponse, GetBalancesRequest, GetBalancesResponse, + GetNodeInfoRequest, GetNodeInfoResponse, GetPaymentDetailsRequest, GetPaymentDetailsResponse, + ListChannelsRequest, ListChannelsResponse, ListForwardedPaymentsRequest, + ListForwardedPaymentsResponse, ListPaymentsRequest, ListPaymentsResponse, + OnchainReceiveRequest, OnchainReceiveResponse, OnchainSendRequest, OnchainSendResponse, + OpenChannelRequest, OpenChannelResponse, SpliceInRequest, SpliceInResponse, SpliceOutRequest, + SpliceOutResponse, UpdateChannelConfigRequest, UpdateChannelConfigResponse, }; 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, SPLICE_IN_PATH, SPLICE_OUT_PATH, - UPDATE_CHANNEL_CONFIG_PATH, + CLOSE_CHANNEL_PATH, CONNECT_PEER_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, SPLICE_IN_PATH, + SPLICE_OUT_PATH, UPDATE_CHANNEL_CONFIG_PATH, }; use ldk_server_protos::error::{ErrorCode, ErrorResponse}; use prost::Message; @@ -252,6 +252,15 @@ impl LdkServerClient { self.post_request(&request, &url).await } + /// Connect to a peer on the Lightning Network. + /// For API contract/usage, refer to docs for [`ConnectPeerRequest`] and [`ConnectPeerResponse`]. + pub async fn connect_peer( + &self, request: ConnectPeerRequest, + ) -> Result { + let url = format!("https://{}/{CONNECT_PEER_PATH}", self.base_url); + self.post_request(&request, &url).await + } + async fn post_request( &self, request: &Rq, url: &str, ) -> Result { diff --git a/ldk-server-protos/src/api.rs b/ldk-server-protos/src/api.rs index 439a403..35a5bc2 100644 --- a/ldk-server-protos/src/api.rs +++ b/ldk-server-protos/src/api.rs @@ -600,3 +600,29 @@ pub struct GetBalancesResponse { pub pending_balances_from_channel_closures: ::prost::alloc::vec::Vec, } +/// Connect to a peer on the Lightning Network. +/// See more: +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ConnectPeerRequest { + /// The hex-encoded public key of the node to connect to. + #[prost(string, tag = "1")] + pub node_pubkey: ::prost::alloc::string::String, + /// An address which can be used to connect to a remote peer. + /// It can be of type IPv4:port, IPv6:port, OnionV3:port or hostname:port + #[prost(string, tag = "2")] + pub address: ::prost::alloc::string::String, + /// Whether to persist the peer connection, i.e., whether the peer will be re-connected on + /// restart. Defaults to true. + #[prost(bool, optional, tag = "3")] + pub persist: ::core::option::Option, +} +/// The response `content` for the `ConnectPeer` API, when HttpStatusCode is OK (200). +/// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`. +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ConnectPeerResponse {} diff --git a/ldk-server-protos/src/endpoints.rs b/ldk-server-protos/src/endpoints.rs index c7ed1c2..606da0d 100644 --- a/ldk-server-protos/src/endpoints.rs +++ b/ldk-server-protos/src/endpoints.rs @@ -25,3 +25,4 @@ pub const LIST_PAYMENTS_PATH: &str = "ListPayments"; pub const LIST_FORWARDED_PAYMENTS_PATH: &str = "ListForwardedPayments"; pub const UPDATE_CHANNEL_CONFIG_PATH: &str = "UpdateChannelConfig"; pub const GET_PAYMENT_DETAILS_PATH: &str = "GetPaymentDetails"; +pub const CONNECT_PEER_PATH: &str = "ConnectPeer"; diff --git a/ldk-server/src/api/connect_peer.rs b/ldk-server/src/api/connect_peer.rs new file mode 100644 index 0000000..3ca85ce --- /dev/null +++ b/ldk-server/src/api/connect_peer.rs @@ -0,0 +1,30 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +use crate::api::error::LdkServerError; +use crate::service::Context; +use ldk_node::bitcoin::secp256k1::PublicKey; +use ldk_node::lightning::ln::msgs::SocketAddress; +use ldk_server_protos::api::{ConnectPeerRequest, ConnectPeerResponse}; +use std::str::FromStr; + +pub(crate) fn handle_connect_peer( + context: Context, request: ConnectPeerRequest, +) -> Result { + let node_id = PublicKey::from_str(&request.node_pubkey) + .map_err(|_| ldk_node::NodeError::InvalidPublicKey)?; + let address = SocketAddress::from_str(&request.address) + .map_err(|_| ldk_node::NodeError::InvalidSocketAddress)?; + + let persist = request.persist.unwrap_or(true); + + context.node.connect(node_id, address, persist)?; + + Ok(ConnectPeerResponse {}) +} diff --git a/ldk-server/src/api/mod.rs b/ldk-server/src/api/mod.rs index 0063d9d..1152f8b 100644 --- a/ldk-server/src/api/mod.rs +++ b/ldk-server/src/api/mod.rs @@ -18,6 +18,7 @@ pub(crate) mod bolt11_send; pub(crate) mod bolt12_receive; pub(crate) mod bolt12_send; pub(crate) mod close_channel; +pub(crate) mod connect_peer; pub(crate) mod error; pub(crate) mod get_balances; pub(crate) mod get_node_info; diff --git a/ldk-server/src/service.rs b/ldk-server/src/service.rs index 056b256..0f05094 100644 --- a/ldk-server/src/service.rs +++ b/ldk-server/src/service.rs @@ -20,10 +20,10 @@ use ldk_node::bitcoin::hashes::{sha256, Hash, HashEngine}; use ldk_node::Node; 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, SPLICE_IN_PATH, SPLICE_OUT_PATH, - UPDATE_CHANNEL_CONFIG_PATH, + CLOSE_CHANNEL_PATH, CONNECT_PEER_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, SPLICE_IN_PATH, + SPLICE_OUT_PATH, UPDATE_CHANNEL_CONFIG_PATH, }; use prost::Message; @@ -32,6 +32,7 @@ use crate::api::bolt11_send::handle_bolt11_send_request; use crate::api::bolt12_receive::handle_bolt12_receive_request; use crate::api::bolt12_send::handle_bolt12_send_request; use crate::api::close_channel::{handle_close_channel_request, handle_force_close_channel_request}; +use crate::api::connect_peer::handle_connect_peer; use crate::api::error::LdkServerError; use crate::api::error::LdkServerErrorCode::{AuthError, InvalidRequestError}; use crate::api::get_balances::handle_get_balances_request; @@ -292,6 +293,9 @@ impl Service> for NodeService { api_key, handle_list_forwarded_payments_request, )), + CONNECT_PEER_PATH => { + Box::pin(handle_request(context, req, auth_params, api_key, handle_connect_peer)) + }, path => { let error = format!("Unknown request: {}", path).into_bytes(); Box::pin(async {