Skip to content
Merged
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
64 changes: 64 additions & 0 deletions e2e-tests/tests/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,70 @@ async fn test_cli_splice_out() {
assert!(address.starts_with("bcrt1"), "Expected regtest address, got: {}", address);
}

#[tokio::test]
async fn test_cli_graph_list_channels_empty() {
let bitcoind = TestBitcoind::new();
let server = LdkServerHandle::start(&bitcoind).await;

let output = run_cli(&server, &["graph-list-channels"]);
assert!(output["short_channel_ids"].as_array().unwrap().is_empty());
}

#[tokio::test]
async fn test_cli_graph_list_nodes_empty() {
let bitcoind = TestBitcoind::new();
let server = LdkServerHandle::start(&bitcoind).await;

let output = run_cli(&server, &["graph-list-nodes"]);
assert!(output["node_ids"].as_array().unwrap().is_empty());
}

#[tokio::test]
async fn test_cli_graph_with_channel() {
let bitcoind = TestBitcoind::new();
let server_a = LdkServerHandle::start(&bitcoind).await;
let server_b = LdkServerHandle::start(&bitcoind).await;
setup_funded_channel(&bitcoind, &server_a, &server_b, 100_000).await;

// Wait for the channel announcement to appear in the network graph.
let scid = {
let start = std::time::Instant::now();
loop {
let output = run_cli(&server_a, &["graph-list-channels"]);
let scids = output["short_channel_ids"].as_array().unwrap();
if !scids.is_empty() {
break scids[0].as_u64().unwrap().to_string();
}
if start.elapsed() > Duration::from_secs(30) {
panic!("Timed out waiting for channel to appear in network graph");
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
};

// Test GraphGetChannel: should return channel info with both our nodes.
let output = run_cli(&server_a, &["graph-get-channel", &scid]);
let channel = &output["channel"];
let node_one = channel["node_one"].as_str().unwrap();
let node_two = channel["node_two"].as_str().unwrap();
let nodes = [server_a.node_id(), server_b.node_id()];
assert!(nodes.contains(&node_one), "node_one {} not one of our nodes", node_one);
assert!(nodes.contains(&node_two), "node_two {} not one of our nodes", node_two);

// Test GraphListNodes: should contain both node IDs.
let output = run_cli(&server_a, &["graph-list-nodes"]);
let node_ids: Vec<&str> =
output["node_ids"].as_array().unwrap().iter().map(|n| n.as_str().unwrap()).collect();
assert!(node_ids.contains(&server_a.node_id()), "Expected server_a in graph nodes");
assert!(node_ids.contains(&server_b.node_id()), "Expected server_b in graph nodes");

// Test GraphGetNode: should return node info with at least one channel.
let output = run_cli(&server_a, &["graph-get-node", server_b.node_id()]);
let node = &output["node"];
let channels = node["channels"].as_array().unwrap();
assert!(!channels.is_empty(), "Expected node to have at least one channel");
}

#[tokio::test]
async fn test_cli_completions() {
let bitcoind = TestBitcoind::new();
Expand Down
45 changes: 41 additions & 4 deletions ldk-server-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,13 @@ use ldk_server_client::ldk_server_protos::api::{
DisconnectPeerRequest, DisconnectPeerResponse, ExportPathfindingScoresRequest,
ForceCloseChannelRequest, ForceCloseChannelResponse, GetBalancesRequest, GetBalancesResponse,
GetNodeInfoRequest, GetNodeInfoResponse, GetPaymentDetailsRequest, GetPaymentDetailsResponse,
ListChannelsRequest, ListChannelsResponse, ListForwardedPaymentsRequest, ListPaymentsRequest,
OnchainReceiveRequest, OnchainReceiveResponse, OnchainSendRequest, OnchainSendResponse,
OpenChannelRequest, OpenChannelResponse, SignMessageRequest, SignMessageResponse,
SpliceInRequest, SpliceInResponse, SpliceOutRequest, SpliceOutResponse, SpontaneousSendRequest,
GraphGetChannelRequest, GraphGetChannelResponse, GraphGetNodeRequest, GraphGetNodeResponse,
GraphListChannelsRequest, GraphListChannelsResponse, GraphListNodesRequest,
GraphListNodesResponse, ListChannelsRequest, ListChannelsResponse,
ListForwardedPaymentsRequest, ListPaymentsRequest, OnchainReceiveRequest,
OnchainReceiveResponse, OnchainSendRequest, OnchainSendResponse, OpenChannelRequest,
OpenChannelResponse, SignMessageRequest, SignMessageResponse, SpliceInRequest,
SpliceInResponse, SpliceOutRequest, SpliceOutResponse, SpontaneousSendRequest,
SpontaneousSendResponse, UpdateChannelConfigRequest, UpdateChannelConfigResponse,
VerifySignatureRequest, VerifySignatureResponse,
};
Expand Down Expand Up @@ -394,6 +397,20 @@ enum Commands {
},
#[command(about = "Export the pathfinding scores used by the router")]
ExportPathfindingScores,
#[command(about = "List all known short channel IDs in the network graph")]
GraphListChannels,
#[command(about = "Get channel information from the network graph by short channel ID")]
GraphGetChannel {
#[arg(help = "The short channel ID to look up")]
short_channel_id: u64,
},
#[command(about = "List all known node IDs in the network graph")]
GraphListNodes,
#[command(about = "Get node information from the network graph by node ID")]
GraphGetNode {
#[arg(help = "The hex-encoded node ID to look up")]
node_id: String,
},
#[command(about = "Generate shell completions for the CLI")]
Completions {
#[arg(
Expand Down Expand Up @@ -807,6 +824,26 @@ async fn main() {
),
);
},
Commands::GraphListChannels => {
handle_response_result::<_, GraphListChannelsResponse>(
client.graph_list_channels(GraphListChannelsRequest {}).await,
);
},
Commands::GraphGetChannel { short_channel_id } => {
handle_response_result::<_, GraphGetChannelResponse>(
client.graph_get_channel(GraphGetChannelRequest { short_channel_id }).await,
);
},
Commands::GraphListNodes => {
handle_response_result::<_, GraphListNodesResponse>(
client.graph_list_nodes(GraphListNodesRequest {}).await,
);
},
Commands::GraphGetNode { node_id } => {
handle_response_result::<_, GraphGetNodeResponse>(
client.graph_get_node(GraphGetNodeRequest { node_id }).await,
);
},
Commands::Completions { .. } => unreachable!("Handled above"),
}
}
Expand Down
51 changes: 45 additions & 6 deletions ldk-server-client/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,21 @@ use ldk_server_protos::api::{
DisconnectPeerRequest, DisconnectPeerResponse, ExportPathfindingScoresRequest,
ExportPathfindingScoresResponse, ForceCloseChannelRequest, ForceCloseChannelResponse,
GetBalancesRequest, GetBalancesResponse, GetNodeInfoRequest, GetNodeInfoResponse,
GetPaymentDetailsRequest, GetPaymentDetailsResponse, ListChannelsRequest, ListChannelsResponse,
ListForwardedPaymentsRequest, ListForwardedPaymentsResponse, ListPaymentsRequest,
ListPaymentsResponse, OnchainReceiveRequest, OnchainReceiveResponse, OnchainSendRequest,
OnchainSendResponse, OpenChannelRequest, OpenChannelResponse, SignMessageRequest,
SignMessageResponse, SpliceInRequest, SpliceInResponse, SpliceOutRequest, SpliceOutResponse,
SpontaneousSendRequest, SpontaneousSendResponse, UpdateChannelConfigRequest,
GetPaymentDetailsRequest, GetPaymentDetailsResponse, GraphGetChannelRequest,
GraphGetChannelResponse, GraphGetNodeRequest, GraphGetNodeResponse, GraphListChannelsRequest,
GraphListChannelsResponse, GraphListNodesRequest, GraphListNodesResponse, ListChannelsRequest,
ListChannelsResponse, ListForwardedPaymentsRequest, ListForwardedPaymentsResponse,
ListPaymentsRequest, ListPaymentsResponse, OnchainReceiveRequest, OnchainReceiveResponse,
OnchainSendRequest, OnchainSendResponse, OpenChannelRequest, OpenChannelResponse,
SignMessageRequest, SignMessageResponse, SpliceInRequest, SpliceInResponse, SpliceOutRequest,
SpliceOutResponse, SpontaneousSendRequest, SpontaneousSendResponse, UpdateChannelConfigRequest,
UpdateChannelConfigResponse, VerifySignatureRequest, VerifySignatureResponse,
};
use ldk_server_protos::endpoints::{
BOLT11_RECEIVE_PATH, BOLT11_SEND_PATH, BOLT12_RECEIVE_PATH, BOLT12_SEND_PATH,
CLOSE_CHANNEL_PATH, CONNECT_PEER_PATH, DISCONNECT_PEER_PATH, EXPORT_PATHFINDING_SCORES_PATH,
FORCE_CLOSE_CHANNEL_PATH, GET_BALANCES_PATH, GET_NODE_INFO_PATH, GET_PAYMENT_DETAILS_PATH,
GRAPH_GET_CHANNEL_PATH, GRAPH_GET_NODE_PATH, GRAPH_LIST_CHANNELS_PATH, GRAPH_LIST_NODES_PATH,
LIST_CHANNELS_PATH, LIST_FORWARDED_PAYMENTS_PATH, LIST_PAYMENTS_PATH, ONCHAIN_RECEIVE_PATH,
ONCHAIN_SEND_PATH, OPEN_CHANNEL_PATH, SIGN_MESSAGE_PATH, SPLICE_IN_PATH, SPLICE_OUT_PATH,
SPONTANEOUS_SEND_PATH, UPDATE_CHANNEL_CONFIG_PATH, VERIFY_SIGNATURE_PATH,
Expand Down Expand Up @@ -310,6 +313,42 @@ impl LdkServerClient {
self.post_request(&request, &url).await
}

/// Returns a list of all known short channel IDs in the network graph.
/// For API contract/usage, refer to docs for [`GraphListChannelsRequest`] and [`GraphListChannelsResponse`].
pub async fn graph_list_channels(
&self, request: GraphListChannelsRequest,
) -> Result<GraphListChannelsResponse, LdkServerError> {
let url = format!("https://{}/{GRAPH_LIST_CHANNELS_PATH}", self.base_url);
self.post_request(&request, &url).await
}

/// Returns information on a channel with the given short channel ID from the network graph.
/// For API contract/usage, refer to docs for [`GraphGetChannelRequest`] and [`GraphGetChannelResponse`].
pub async fn graph_get_channel(
&self, request: GraphGetChannelRequest,
) -> Result<GraphGetChannelResponse, LdkServerError> {
let url = format!("https://{}/{GRAPH_GET_CHANNEL_PATH}", self.base_url);
self.post_request(&request, &url).await
}

/// Returns a list of all known node IDs in the network graph.
/// For API contract/usage, refer to docs for [`GraphListNodesRequest`] and [`GraphListNodesResponse`].
pub async fn graph_list_nodes(
&self, request: GraphListNodesRequest,
) -> Result<GraphListNodesResponse, LdkServerError> {
let url = format!("https://{}/{GRAPH_LIST_NODES_PATH}", self.base_url);
self.post_request(&request, &url).await
}

/// Returns information on a node with the given ID from the network graph.
/// For API contract/usage, refer to docs for [`GraphGetNodeRequest`] and [`GraphGetNodeResponse`].
pub async fn graph_get_node(
&self, request: GraphGetNodeRequest,
) -> Result<GraphGetNodeResponse, LdkServerError> {
let url = format!("https://{}/{GRAPH_GET_NODE_PATH}", self.base_url);
self.post_request(&request, &url).await
}

async fn post_request<Rq: Message, Rs: Message + Default>(
&self, request: &Rq, url: &str,
) -> Result<Rs, LdkServerError> {
Expand Down
80 changes: 80 additions & 0 deletions ldk-server-protos/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -761,3 +761,83 @@ pub struct DisconnectPeerRequest {
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct DisconnectPeerResponse {}
/// Returns a list of all known short channel IDs in the network graph.
/// See more: <https://docs.rs/ldk-node/latest/ldk_node/graph/struct.NetworkGraph.html#method.list_channels>
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct GraphListChannelsRequest {}
/// The response `content` for the `GraphListChannels` API, when HttpStatusCode is OK (200).
/// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct GraphListChannelsResponse {
/// List of short channel IDs known to the network graph.
#[prost(uint64, repeated, tag = "1")]
pub short_channel_ids: ::prost::alloc::vec::Vec<u64>,
}
/// Returns information on a channel with the given short channel ID from the network graph.
/// See more: <https://docs.rs/ldk-node/latest/ldk_node/graph/struct.NetworkGraph.html#method.channel>
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct GraphGetChannelRequest {
/// The short channel ID to look up.
#[prost(uint64, tag = "1")]
pub short_channel_id: u64,
}
/// The response `content` for the `GraphGetChannel` API, when HttpStatusCode is OK (200).
/// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct GraphGetChannelResponse {
/// The channel information.
#[prost(message, optional, tag = "1")]
pub channel: ::core::option::Option<super::types::GraphChannel>,
}
/// Returns a list of all known node IDs in the network graph.
/// See more: <https://docs.rs/ldk-node/latest/ldk_node/graph/struct.NetworkGraph.html#method.list_nodes>
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct GraphListNodesRequest {}
/// The response `content` for the `GraphListNodes` API, when HttpStatusCode is OK (200).
/// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct GraphListNodesResponse {
/// List of hex-encoded node IDs known to the network graph.
#[prost(string, repeated, tag = "1")]
pub node_ids: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
}
/// Returns information on a node with the given ID from the network graph.
/// See more: <https://docs.rs/ldk-node/latest/ldk_node/graph/struct.NetworkGraph.html#method.node>
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct GraphGetNodeRequest {
/// The hex-encoded node ID to look up.
#[prost(string, tag = "1")]
pub node_id: ::prost::alloc::string::String,
}
/// The response `content` for the `GraphGetNode` API, when HttpStatusCode is OK (200).
/// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct GraphGetNodeResponse {
/// The node information.
#[prost(message, optional, tag = "1")]
pub node: ::core::option::Option<super::types::GraphNode>,
}
4 changes: 4 additions & 0 deletions ldk-server-protos/src/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,7 @@ pub const SPONTANEOUS_SEND_PATH: &str = "SpontaneousSend";
pub const SIGN_MESSAGE_PATH: &str = "SignMessage";
pub const VERIFY_SIGNATURE_PATH: &str = "VerifySignature";
pub const EXPORT_PATHFINDING_SCORES_PATH: &str = "ExportPathfindingScores";
pub const GRAPH_LIST_CHANNELS_PATH: &str = "GraphListChannels";
pub const GRAPH_GET_CHANNEL_PATH: &str = "GraphGetChannel";
pub const GRAPH_LIST_NODES_PATH: &str = "GraphListNodes";
pub const GRAPH_GET_NODE_PATH: &str = "GraphGetNode";
50 changes: 50 additions & 0 deletions ldk-server-protos/src/proto/api.proto
Original file line number Diff line number Diff line change
Expand Up @@ -596,3 +596,53 @@ message DisconnectPeerRequest {
// The response `content` for the `DisconnectPeer` API, when HttpStatusCode is OK (200).
// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`.
message DisconnectPeerResponse {}

// Returns a list of all known short channel IDs in the network graph.
// See more: https://docs.rs/ldk-node/latest/ldk_node/graph/struct.NetworkGraph.html#method.list_channels
message GraphListChannelsRequest {}

// The response `content` for the `GraphListChannels` API, when HttpStatusCode is OK (200).
// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`.
message GraphListChannelsResponse {
// List of short channel IDs known to the network graph.
repeated uint64 short_channel_ids = 1;
}

// Returns information on a channel with the given short channel ID from the network graph.
// See more: https://docs.rs/ldk-node/latest/ldk_node/graph/struct.NetworkGraph.html#method.channel
message GraphGetChannelRequest {
// The short channel ID to look up.
uint64 short_channel_id = 1;
}

// The response `content` for the `GraphGetChannel` API, when HttpStatusCode is OK (200).
// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`.
message GraphGetChannelResponse {
// The channel information.
types.GraphChannel channel = 1;
}

// Returns a list of all known node IDs in the network graph.
// See more: https://docs.rs/ldk-node/latest/ldk_node/graph/struct.NetworkGraph.html#method.list_nodes
message GraphListNodesRequest {}

// The response `content` for the `GraphListNodes` API, when HttpStatusCode is OK (200).
// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`.
message GraphListNodesResponse {
// List of hex-encoded node IDs known to the network graph.
repeated string node_ids = 1;
}

// Returns information on a node with the given ID from the network graph.
// See more: https://docs.rs/ldk-node/latest/ldk_node/graph/struct.NetworkGraph.html#method.node
message GraphGetNodeRequest {
// The hex-encoded node ID to look up.
string node_id = 1;
}

// The response `content` for the `GraphGetNode` API, when HttpStatusCode is OK (200).
// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`.
message GraphGetNodeResponse {
// The node information.
types.GraphNode node = 1;
}
Loading