diff --git a/e2e-tests/src/lib.rs b/e2e-tests/src/lib.rs index 8b34fd2..f58c8cd 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 7744a0d..b2f432c 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 8cfa087..af3956b 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 4dbdb66..3d3962c 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 a841bc0..603fb48 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 6c470b7..468f837 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