Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion e2e-tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
25 changes: 22 additions & 3 deletions e2e-tests/tests/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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]
Expand Down
40 changes: 29 additions & 11 deletions ldk-server-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
#[arg(long, help = "Amount to push to the remote side, e.g. 50sat or 50000msat")]
push_to_counterparty: Option<Amount>,
#[arg(long, help = "Whether the channel should be public")]
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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>(
Expand Down Expand Up @@ -878,6 +888,14 @@ async fn main() {
}
}

fn parse_node_pubkey_and_inline_address(node_pubkey: String) -> (String, Option<String>) {
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<u32>, forwarding_fee_base_msat: Option<u32>,
cltv_expiry_delta: Option<u32>,
Expand Down
9 changes: 5 additions & 4 deletions ldk-server-protos/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 4 additions & 3 deletions ldk-server-protos/src/proto/api.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
20 changes: 18 additions & 2 deletions ldk-server/src/api/open_channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,31 @@ 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(
context: Context, request: OpenChannelRequest,
) -> Result<OpenChannelResponse, LdkServerError> {
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(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have you thought about looking up the network graph for the address of the peer ? Maybe that's too far, but pretty sure other node implementations do look at the network graph in case the address is not specified.

Copy link
Collaborator Author

@benthecarman benthecarman Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my testing, lnd doesn't do that. Not sure about CLN or eclair.

Could make sense but imo that should probably be a ldk-node feature where we iterate over all of the announced addresses trying each. We could select the first here but not really a complete feature

)
})?,
};

let channel_config = request
.channel_config
Expand Down
Loading