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)?;