From d67c11116f94182ac9db6bcbd698922eb5b018de 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. Co-Authored-By: Claude Opus 5.5 --- e2e-tests/src/lib.rs | 2 +- e2e-tests/tests/e2e.rs | 34 +++++++++++++++++++++----- e2e-tests/tests/postgres.rs | 2 +- ldk-server-cli/src/main.rs | 37 +++++++++++++++++++++-------- ldk-server-grpc/src/api.rs | 9 +++---- ldk-server-grpc/src/proto/api.proto | 7 +++--- ldk-server-mcp/src/tools/schema.rs | 4 ++-- ldk-server/src/api/open_channel.rs | 19 +++++++++++++-- 8 files changed, 85 insertions(+), 29 deletions(-) diff --git a/e2e-tests/src/lib.rs b/e2e-tests/src/lib.rs index e6c48970..bdafa872 100644 --- a/e2e-tests/src/lib.rs +++ b/e2e-tests/src/lib.rs @@ -985,7 +985,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)), amount: Some(open_channel_request::Amount::ChannelAmountSats(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 157f2e8c..dea408f8 100644 --- a/e2e-tests/tests/e2e.rs +++ b/e2e-tests/tests/e2e.rs @@ -675,7 +675,9 @@ async fn test_cli_list_peers() { // === CLI tests: Group 4 — Two-node with channel === -async fn open_channel_via_cli(channel_amount: &str) { +async fn open_channel_via_cli( + channel_amount: &str, +) -> (TestBitcoind, LdkServerHandle, LdkServerHandle, String) { let bitcoind = TestBitcoind::new(); let server_a = LdkServerHandle::start(&bitcoind).await; let server_b = LdkServerHandle::start(&bitcoind).await; @@ -693,14 +695,34 @@ async fn open_channel_via_cli(channel_amount: &str) { let addr = format!("127.0.0.1:{}", server_b.p2p_port); let output = run_cli( &server_a, - &["open-channel", server_b.node_id(), &addr, channel_amount, "--announce-channel"], + &[ + "open-channel", + server_b.node_id(), + channel_amount, + "--address", + &addr, + "--announce-channel", + ], ); - assert!(!output["user_channel_id"].as_str().unwrap().is_empty()); + let user_channel_id = output["user_channel_id"].as_str().unwrap().to_string(); + assert!(!user_channel_id.is_empty()); + (bitcoind, server_a, server_b, user_channel_id) } #[tokio::test] async fn test_cli_open_channel() { - open_channel_via_cli("100000sat").await; + let (bitcoind, server_a, server_b, first_user_channel_id) = + open_channel_via_cli("100000sat").await; + + // 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] @@ -729,7 +751,7 @@ async fn test_subscribe_events_channel_state_lifecycle_pending_ready_closed() { .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)), amount: Some(open_channel_request::Amount::ChannelAmountSats(100_000)), push_to_counterparty_msat: None, channel_config: None, @@ -901,7 +923,7 @@ async fn test_subscribe_events_channel_state_lifecycle_pending_ready_force_close .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)), amount: Some(open_channel_request::Amount::ChannelAmountSats(100_000)), push_to_counterparty_msat: None, channel_config: None, diff --git a/e2e-tests/tests/postgres.rs b/e2e-tests/tests/postgres.rs index 6626530b..936b67a8 100644 --- a/e2e-tests/tests/postgres.rs +++ b/e2e-tests/tests/postgres.rs @@ -316,7 +316,7 @@ async fn test_postgres_persistence_and_sqlite_interoperability() { .client() .open_channel(OpenChannelRequest { node_pubkey: sqlite_c.node_id().to_string(), - address: format!("127.0.0.1:{}", sqlite_c.p2p_port), + address: Some(format!("127.0.0.1:{}", sqlite_c.p2p_port)), amount: Some(open_channel_request::Amount::ChannelAmountSats(1_000_000)), push_to_counterparty_msat: Some(50_000_000), channel_config: None, diff --git a/ldk-server-cli/src/main.rs b/ldk-server-cli/src/main.rs index 0c912721..bd85eb96 100644 --- a/ldk-server-cli/src/main.rs +++ b/ldk-server-cli/src/main.rs @@ -567,16 +567,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, or 'all' to use all available on-chain funds. Exact amounts must be a whole sat amount." )] channel_amount: AmountOrAll, + #[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")] @@ -1344,8 +1346,8 @@ async fn main() { }, Commands::OpenChannel { node_pubkey, - address, channel_amount, + address, push_to_counterparty, announce_channel, disable_counterparty_reserve, @@ -1353,6 +1355,14 @@ async fn main() { 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." + .to_string(), + ); + } + let address = address.or(inline_address); let amount = match channel_amount.to_sat().unwrap_or_else(|e| handle_error_msg(e)) { Some(amount_sats) => open_channel_request::Amount::ChannelAmountSats(amount_sats), None => open_channel_request::Amount::AllFunds(AllFunds {}), @@ -1548,10 +1558,9 @@ 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."); std::process::exit(1); @@ -1655,6 +1664,14 @@ fn load_client_config(explicit_path: Option) -> Result, } } +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-grpc/src/api.rs b/ldk-server-grpc/src/api.rs index 50d987a3..b0ac9802 100644 --- a/ldk-server-grpc/src/api.rs +++ b/ldk-server-grpc/src/api.rs @@ -768,10 +768,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 to push to the remote side as part of the initial commitment state. #[prost(uint64, optional, tag = "4")] pub push_to_counterparty_msat: ::core::option::Option, diff --git a/ldk-server-grpc/src/proto/api.proto b/ldk-server-grpc/src/proto/api.proto index 7d8fbc5f..b0b8c00f 100644 --- a/ldk-server-grpc/src/proto/api.proto +++ b/ldk-server-grpc/src/proto/api.proto @@ -580,9 +580,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; // Required. The funds to commit to the channel. oneof amount { diff --git a/ldk-server-mcp/src/tools/schema.rs b/ldk-server-mcp/src/tools/schema.rs index ee90a530..52d3019f 100644 --- a/ldk-server-mcp/src/tools/schema.rs +++ b/ldk-server-mcp/src/tools/schema.rs @@ -611,7 +611,7 @@ pub fn open_channel_schema() -> Value { }, "address": { "type": "string", - "description": "Address of the remote peer (IPv4:port, IPv6:port, OnionV3:port, or hostname:port)" + "description": "Address of the remote peer (IPv4:port, IPv6:port, OnionV3:port, or hostname:port). If omitted, the address of the currently connected peer is used" }, "channel_amount_sats": { "oneOf": [ @@ -634,7 +634,7 @@ pub fn open_channel_schema() -> Value { }, "channel_config": channel_config_schema() }, - "required": ["node_pubkey", "address", "channel_amount_sats"] + "required": ["node_pubkey", "channel_amount_sats"] }) } diff --git a/ldk-server/src/api/open_channel.rs b/ldk-server/src/api/open_channel.rs index 951a5680..89df6ec7 100644 --- a/ldk-server/src/api/open_channel.rs +++ b/ldk-server/src/api/open_channel.rs @@ -25,8 +25,23 @@ pub(crate) async 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( + LdkServerErrorCode::InvalidRequestError, + "Address is required unless the peer is currently connected. Provide an address or connect-peer first.", + ) + })?, + }; let amount = require_amount(request.amount)?;