From 2850e0951f3f2e742c6c85e002574d1041fec29c Mon Sep 17 00:00:00 2001 From: benthecarman Date: Tue, 17 Mar 2026 16:49:40 -0500 Subject: [PATCH] Make address optional in `open-channel` Previously we always required providing the node's address when opening a channel. This was annoying because we may not even need it because we're already connected to the peer. Now we look up the node's address from our peers list if the address is not provided. Also did the same handling as connect peer where you can provide the node and address as pk@addr instead of 2 separate args for easier use in the cli. --- e2e-tests/src/lib.rs | 2 +- e2e-tests/tests/e2e.rs | 25 +++++++++++++++-- ldk-server-cli/src/main.rs | 40 +++++++++++++++++++-------- ldk-server-protos/src/api.rs | 9 +++--- ldk-server-protos/src/proto/api.proto | 7 +++-- ldk-server/src/api/open_channel.rs | 20 ++++++++++++-- 6 files changed, 79 insertions(+), 24 deletions(-) diff --git a/e2e-tests/src/lib.rs b/e2e-tests/src/lib.rs index 8b34fd22..f58c8cdc 100644 --- a/e2e-tests/src/lib.rs +++ b/e2e-tests/src/lib.rs @@ -410,7 +410,7 @@ pub async fn setup_funded_channel( .client() .open_channel(OpenChannelRequest { node_pubkey: server_b.node_id().to_string(), - address: format!("127.0.0.1:{}", server_b.p2p_port), + address: Some(format!("127.0.0.1:{}", server_b.p2p_port)), channel_amount_sats, push_to_counterparty_msat: None, channel_config: None, diff --git a/e2e-tests/tests/e2e.rs b/e2e-tests/tests/e2e.rs index 7744a0dd..b2f432ce 100644 --- a/e2e-tests/tests/e2e.rs +++ b/e2e-tests/tests/e2e.rs @@ -12,7 +12,8 @@ use std::time::Duration; use e2e_tests::{ find_available_port, mine_and_sync, run_cli, run_cli_raw, setup_funded_channel, - wait_for_onchain_balance, LdkServerHandle, RabbitMqEventConsumer, TestBitcoind, + wait_for_onchain_balance, wait_for_usable_channel, LdkServerHandle, RabbitMqEventConsumer, + TestBitcoind, }; use ldk_node::lightning::ln::msgs::SocketAddress; use ldk_server_client::ldk_server_protos::api::{ @@ -226,9 +227,27 @@ async fn test_cli_open_channel() { let addr = format!("127.0.0.1:{}", server_b.p2p_port); let output = run_cli( &server_a, - &["open-channel", server_b.node_id(), &addr, "100000sat", "--announce-channel"], + &[ + "open-channel", + server_b.node_id(), + "100000sat", + "--address", + &addr, + "--announce-channel", + ], ); - assert!(!output["user_channel_id"].as_str().unwrap().is_empty()); + let first_user_channel_id = output["user_channel_id"].as_str().unwrap().to_string(); + assert!(!first_user_channel_id.is_empty()); + + // Confirm first channel is usable before opening a second one. + mine_and_sync(&bitcoind, &[&server_a, &server_b], 6).await; + wait_for_usable_channel(server_a.client(), &bitcoind, Duration::from_secs(60)).await; + + // Open a second channel without specifying address (resolved by backend from connected peers). + let output = run_cli(&server_a, &["open-channel", server_b.node_id(), "50000sat"]); + let second_user_channel_id = output["user_channel_id"].as_str().unwrap().to_string(); + assert!(!second_user_channel_id.is_empty()); + assert_ne!(first_user_channel_id, second_user_channel_id); } #[tokio::test] diff --git a/ldk-server-cli/src/main.rs b/ldk-server-cli/src/main.rs index 8cfa0878..af3956bc 100644 --- a/ldk-server-cli/src/main.rs +++ b/ldk-server-cli/src/main.rs @@ -249,16 +249,18 @@ enum Commands { }, #[command(about = "Create a new outbound channel to the given remote node")] OpenChannel { - #[arg(help = "The hex-encoded public key of the node to open a channel with")] + #[arg(help = "The node to open a channel with, as hex pubkey or pubkey@address format")] node_pubkey: String, - #[arg( - help = "Address to connect to remote peer (IPv4:port, IPv6:port, OnionV3:port, or hostname:port)" - )] - address: String, #[arg( help = "The amount to commit to the channel, e.g. 100sat or 100000msat, must be a whole sat amount, cannot send msats on-chain." )] channel_amount: Amount, + #[arg( + short, + long, + help = "Address to connect to remote peer (IPv4:port, IPv6:port, OnionV3:port, or hostname:port). Optional if included in pubkey via @ separator or if peer is already connected." + )] + address: Option, #[arg(long, help = "Amount to push to the remote side, e.g. 50sat or 50000msat")] push_to_counterparty: Option, #[arg(long, help = "Whether the channel should be public")] @@ -678,14 +680,21 @@ async fn main() { }, Commands::OpenChannel { node_pubkey, - address, channel_amount, + address, push_to_counterparty, announce_channel, forwarding_fee_proportional_millionths, forwarding_fee_base_msat, cltv_expiry_delta, } => { + let (node_pubkey, inline_address) = parse_node_pubkey_and_inline_address(node_pubkey); + if inline_address.is_some() && address.is_some() { + handle_error_msg( + "Address was provided twice. Use either pubkey@address or a separate address argument.", + ); + } + let address = address.or(inline_address); let channel_amount_sats = channel_amount.to_sat().unwrap_or_else(|e| handle_error_msg(&e)); let push_to_counterparty_msat = push_to_counterparty.map(|a| a.to_msat()); @@ -804,12 +813,13 @@ async fn main() { ); }, Commands::ConnectPeer { node_pubkey, address, persist } => { - let (node_pubkey, address) = if let Some(address) = address { - (node_pubkey, address) - } else if let Some((pubkey, addr)) = node_pubkey.split_once('@') { - (pubkey.to_string(), addr.to_string()) + let (node_pubkey, inline_address) = parse_node_pubkey_and_inline_address(node_pubkey); + let address = if let Some(address) = address.or(inline_address) { + address } else { - eprintln!("Error: address is required. Provide it as pubkey@address or as a separate argument."); + eprintln!( + "Error: address is required. Provide it as pubkey@address or as a separate argument." + ); std::process::exit(1); }; handle_response_result::<_, ConnectPeerResponse>( @@ -878,6 +888,14 @@ async fn main() { } } +fn parse_node_pubkey_and_inline_address(node_pubkey: String) -> (String, Option) { + if let Some((pubkey, address)) = node_pubkey.split_once('@') { + (pubkey.to_string(), Some(address.to_string())) + } else { + (node_pubkey, None) + } +} + fn build_open_channel_config( forwarding_fee_proportional_millionths: Option, forwarding_fee_base_msat: Option, cltv_expiry_delta: Option, diff --git a/ldk-server-protos/src/api.rs b/ldk-server-protos/src/api.rs index 4dbdb667..3d3962c3 100644 --- a/ldk-server-protos/src/api.rs +++ b/ldk-server-protos/src/api.rs @@ -317,10 +317,11 @@ pub struct OpenChannelRequest { /// The hex-encoded public key of the node to open a channel with. #[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, + /// An optional 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. + /// If unset, the server will try to resolve it from currently connected peers. + #[prost(string, optional, tag = "2")] + pub address: ::core::option::Option<::prost::alloc::string::String>, /// The amount of satoshis the caller is willing to commit to the channel. #[prost(uint64, tag = "3")] pub channel_amount_sats: u64, diff --git a/ldk-server-protos/src/proto/api.proto b/ldk-server-protos/src/proto/api.proto index a841bc0c..603fb485 100644 --- a/ldk-server-protos/src/proto/api.proto +++ b/ldk-server-protos/src/proto/api.proto @@ -263,9 +263,10 @@ message OpenChannelRequest { // The hex-encoded public key of the node to open a channel with. string node_pubkey = 1; - // 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 - string address = 2; + // An optional 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. + // If unset, the server will try to resolve it from currently connected peers. + optional string address = 2; // The amount of satoshis the caller is willing to commit to the channel. uint64 channel_amount_sats = 3; diff --git a/ldk-server/src/api/open_channel.rs b/ldk-server/src/api/open_channel.rs index 6c470b71..468f8373 100644 --- a/ldk-server/src/api/open_channel.rs +++ b/ldk-server/src/api/open_channel.rs @@ -16,6 +16,7 @@ use ldk_server_protos::api::{OpenChannelRequest, OpenChannelResponse}; use crate::api::build_channel_config_from_proto; use crate::api::error::LdkServerError; +use crate::api::error::LdkServerErrorCode::InvalidRequestError; use crate::service::Context; pub(crate) fn handle_open_channel( @@ -23,8 +24,23 @@ pub(crate) fn handle_open_channel( ) -> 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 address = match request.address { + Some(address) => { + SocketAddress::from_str(&address).map_err(|_| ldk_node::NodeError::InvalidSocketAddress)? + }, + None => context + .node + .list_peers() + .into_iter() + .find(|peer| peer.node_id == node_id) + .map(|peer| peer.address) + .ok_or_else(|| { + LdkServerError::new( + InvalidRequestError, + "Address is required unless the peer is currently connected. Provide an address or connect-peer first.".to_string(), + ) + })?, + }; let channel_config = request .channel_config