Skip to content

Commit 9b33ccd

Browse files
authored
Merge pull request #141 from tnull/2026-03-expose-network-graph
Expose network graph in RPC
2 parents 137e571 + 4d90c74 commit 9b33ccd

15 files changed

Lines changed: 671 additions & 11 deletions

File tree

e2e-tests/tests/e2e.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,70 @@ async fn test_cli_splice_out() {
457457
assert!(address.starts_with("bcrt1"), "Expected regtest address, got: {}", address);
458458
}
459459

460+
#[tokio::test]
461+
async fn test_cli_graph_list_channels_empty() {
462+
let bitcoind = TestBitcoind::new();
463+
let server = LdkServerHandle::start(&bitcoind).await;
464+
465+
let output = run_cli(&server, &["graph-list-channels"]);
466+
assert!(output["short_channel_ids"].as_array().unwrap().is_empty());
467+
}
468+
469+
#[tokio::test]
470+
async fn test_cli_graph_list_nodes_empty() {
471+
let bitcoind = TestBitcoind::new();
472+
let server = LdkServerHandle::start(&bitcoind).await;
473+
474+
let output = run_cli(&server, &["graph-list-nodes"]);
475+
assert!(output["node_ids"].as_array().unwrap().is_empty());
476+
}
477+
478+
#[tokio::test]
479+
async fn test_cli_graph_with_channel() {
480+
let bitcoind = TestBitcoind::new();
481+
let server_a = LdkServerHandle::start(&bitcoind).await;
482+
let server_b = LdkServerHandle::start(&bitcoind).await;
483+
setup_funded_channel(&bitcoind, &server_a, &server_b, 100_000).await;
484+
485+
// Wait for the channel announcement to appear in the network graph.
486+
let scid = {
487+
let start = std::time::Instant::now();
488+
loop {
489+
let output = run_cli(&server_a, &["graph-list-channels"]);
490+
let scids = output["short_channel_ids"].as_array().unwrap();
491+
if !scids.is_empty() {
492+
break scids[0].as_u64().unwrap().to_string();
493+
}
494+
if start.elapsed() > Duration::from_secs(30) {
495+
panic!("Timed out waiting for channel to appear in network graph");
496+
}
497+
tokio::time::sleep(Duration::from_secs(1)).await;
498+
}
499+
};
500+
501+
// Test GraphGetChannel: should return channel info with both our nodes.
502+
let output = run_cli(&server_a, &["graph-get-channel", &scid]);
503+
let channel = &output["channel"];
504+
let node_one = channel["node_one"].as_str().unwrap();
505+
let node_two = channel["node_two"].as_str().unwrap();
506+
let nodes = [server_a.node_id(), server_b.node_id()];
507+
assert!(nodes.contains(&node_one), "node_one {} not one of our nodes", node_one);
508+
assert!(nodes.contains(&node_two), "node_two {} not one of our nodes", node_two);
509+
510+
// Test GraphListNodes: should contain both node IDs.
511+
let output = run_cli(&server_a, &["graph-list-nodes"]);
512+
let node_ids: Vec<&str> =
513+
output["node_ids"].as_array().unwrap().iter().map(|n| n.as_str().unwrap()).collect();
514+
assert!(node_ids.contains(&server_a.node_id()), "Expected server_a in graph nodes");
515+
assert!(node_ids.contains(&server_b.node_id()), "Expected server_b in graph nodes");
516+
517+
// Test GraphGetNode: should return node info with at least one channel.
518+
let output = run_cli(&server_a, &["graph-get-node", server_b.node_id()]);
519+
let node = &output["node"];
520+
let channels = node["channels"].as_array().unwrap();
521+
assert!(!channels.is_empty(), "Expected node to have at least one channel");
522+
}
523+
460524
#[tokio::test]
461525
async fn test_cli_completions() {
462526
let bitcoind = TestBitcoind::new();

ldk-server-cli/src/main.rs

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,13 @@ use ldk_server_client::ldk_server_protos::api::{
2727
DisconnectPeerRequest, DisconnectPeerResponse, ExportPathfindingScoresRequest,
2828
ForceCloseChannelRequest, ForceCloseChannelResponse, GetBalancesRequest, GetBalancesResponse,
2929
GetNodeInfoRequest, GetNodeInfoResponse, GetPaymentDetailsRequest, GetPaymentDetailsResponse,
30-
ListChannelsRequest, ListChannelsResponse, ListForwardedPaymentsRequest, ListPaymentsRequest,
31-
OnchainReceiveRequest, OnchainReceiveResponse, OnchainSendRequest, OnchainSendResponse,
32-
OpenChannelRequest, OpenChannelResponse, SignMessageRequest, SignMessageResponse,
33-
SpliceInRequest, SpliceInResponse, SpliceOutRequest, SpliceOutResponse, SpontaneousSendRequest,
30+
GraphGetChannelRequest, GraphGetChannelResponse, GraphGetNodeRequest, GraphGetNodeResponse,
31+
GraphListChannelsRequest, GraphListChannelsResponse, GraphListNodesRequest,
32+
GraphListNodesResponse, ListChannelsRequest, ListChannelsResponse,
33+
ListForwardedPaymentsRequest, ListPaymentsRequest, OnchainReceiveRequest,
34+
OnchainReceiveResponse, OnchainSendRequest, OnchainSendResponse, OpenChannelRequest,
35+
OpenChannelResponse, SignMessageRequest, SignMessageResponse, SpliceInRequest,
36+
SpliceInResponse, SpliceOutRequest, SpliceOutResponse, SpontaneousSendRequest,
3437
SpontaneousSendResponse, UpdateChannelConfigRequest, UpdateChannelConfigResponse,
3538
VerifySignatureRequest, VerifySignatureResponse,
3639
};
@@ -394,6 +397,20 @@ enum Commands {
394397
},
395398
#[command(about = "Export the pathfinding scores used by the router")]
396399
ExportPathfindingScores,
400+
#[command(about = "List all known short channel IDs in the network graph")]
401+
GraphListChannels,
402+
#[command(about = "Get channel information from the network graph by short channel ID")]
403+
GraphGetChannel {
404+
#[arg(help = "The short channel ID to look up")]
405+
short_channel_id: u64,
406+
},
407+
#[command(about = "List all known node IDs in the network graph")]
408+
GraphListNodes,
409+
#[command(about = "Get node information from the network graph by node ID")]
410+
GraphGetNode {
411+
#[arg(help = "The hex-encoded node ID to look up")]
412+
node_id: String,
413+
},
397414
#[command(about = "Generate shell completions for the CLI")]
398415
Completions {
399416
#[arg(
@@ -807,6 +824,26 @@ async fn main() {
807824
),
808825
);
809826
},
827+
Commands::GraphListChannels => {
828+
handle_response_result::<_, GraphListChannelsResponse>(
829+
client.graph_list_channels(GraphListChannelsRequest {}).await,
830+
);
831+
},
832+
Commands::GraphGetChannel { short_channel_id } => {
833+
handle_response_result::<_, GraphGetChannelResponse>(
834+
client.graph_get_channel(GraphGetChannelRequest { short_channel_id }).await,
835+
);
836+
},
837+
Commands::GraphListNodes => {
838+
handle_response_result::<_, GraphListNodesResponse>(
839+
client.graph_list_nodes(GraphListNodesRequest {}).await,
840+
);
841+
},
842+
Commands::GraphGetNode { node_id } => {
843+
handle_response_result::<_, GraphGetNodeResponse>(
844+
client.graph_get_node(GraphGetNodeRequest { node_id }).await,
845+
);
846+
},
810847
Commands::Completions { .. } => unreachable!("Handled above"),
811848
}
812849
}

ldk-server-client/src/client.rs

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,21 @@ use ldk_server_protos::api::{
1818
DisconnectPeerRequest, DisconnectPeerResponse, ExportPathfindingScoresRequest,
1919
ExportPathfindingScoresResponse, ForceCloseChannelRequest, ForceCloseChannelResponse,
2020
GetBalancesRequest, GetBalancesResponse, GetNodeInfoRequest, GetNodeInfoResponse,
21-
GetPaymentDetailsRequest, GetPaymentDetailsResponse, ListChannelsRequest, ListChannelsResponse,
22-
ListForwardedPaymentsRequest, ListForwardedPaymentsResponse, ListPaymentsRequest,
23-
ListPaymentsResponse, OnchainReceiveRequest, OnchainReceiveResponse, OnchainSendRequest,
24-
OnchainSendResponse, OpenChannelRequest, OpenChannelResponse, SignMessageRequest,
25-
SignMessageResponse, SpliceInRequest, SpliceInResponse, SpliceOutRequest, SpliceOutResponse,
26-
SpontaneousSendRequest, SpontaneousSendResponse, UpdateChannelConfigRequest,
21+
GetPaymentDetailsRequest, GetPaymentDetailsResponse, GraphGetChannelRequest,
22+
GraphGetChannelResponse, GraphGetNodeRequest, GraphGetNodeResponse, GraphListChannelsRequest,
23+
GraphListChannelsResponse, GraphListNodesRequest, GraphListNodesResponse, ListChannelsRequest,
24+
ListChannelsResponse, ListForwardedPaymentsRequest, ListForwardedPaymentsResponse,
25+
ListPaymentsRequest, ListPaymentsResponse, OnchainReceiveRequest, OnchainReceiveResponse,
26+
OnchainSendRequest, OnchainSendResponse, OpenChannelRequest, OpenChannelResponse,
27+
SignMessageRequest, SignMessageResponse, SpliceInRequest, SpliceInResponse, SpliceOutRequest,
28+
SpliceOutResponse, SpontaneousSendRequest, SpontaneousSendResponse, UpdateChannelConfigRequest,
2729
UpdateChannelConfigResponse, VerifySignatureRequest, VerifySignatureResponse,
2830
};
2931
use ldk_server_protos::endpoints::{
3032
BOLT11_RECEIVE_PATH, BOLT11_SEND_PATH, BOLT12_RECEIVE_PATH, BOLT12_SEND_PATH,
3133
CLOSE_CHANNEL_PATH, CONNECT_PEER_PATH, DISCONNECT_PEER_PATH, EXPORT_PATHFINDING_SCORES_PATH,
3234
FORCE_CLOSE_CHANNEL_PATH, GET_BALANCES_PATH, GET_NODE_INFO_PATH, GET_PAYMENT_DETAILS_PATH,
35+
GRAPH_GET_CHANNEL_PATH, GRAPH_GET_NODE_PATH, GRAPH_LIST_CHANNELS_PATH, GRAPH_LIST_NODES_PATH,
3336
LIST_CHANNELS_PATH, LIST_FORWARDED_PAYMENTS_PATH, LIST_PAYMENTS_PATH, ONCHAIN_RECEIVE_PATH,
3437
ONCHAIN_SEND_PATH, OPEN_CHANNEL_PATH, SIGN_MESSAGE_PATH, SPLICE_IN_PATH, SPLICE_OUT_PATH,
3538
SPONTANEOUS_SEND_PATH, UPDATE_CHANNEL_CONFIG_PATH, VERIFY_SIGNATURE_PATH,
@@ -310,6 +313,42 @@ impl LdkServerClient {
310313
self.post_request(&request, &url).await
311314
}
312315

316+
/// Returns a list of all known short channel IDs in the network graph.
317+
/// For API contract/usage, refer to docs for [`GraphListChannelsRequest`] and [`GraphListChannelsResponse`].
318+
pub async fn graph_list_channels(
319+
&self, request: GraphListChannelsRequest,
320+
) -> Result<GraphListChannelsResponse, LdkServerError> {
321+
let url = format!("https://{}/{GRAPH_LIST_CHANNELS_PATH}", self.base_url);
322+
self.post_request(&request, &url).await
323+
}
324+
325+
/// Returns information on a channel with the given short channel ID from the network graph.
326+
/// For API contract/usage, refer to docs for [`GraphGetChannelRequest`] and [`GraphGetChannelResponse`].
327+
pub async fn graph_get_channel(
328+
&self, request: GraphGetChannelRequest,
329+
) -> Result<GraphGetChannelResponse, LdkServerError> {
330+
let url = format!("https://{}/{GRAPH_GET_CHANNEL_PATH}", self.base_url);
331+
self.post_request(&request, &url).await
332+
}
333+
334+
/// Returns a list of all known node IDs in the network graph.
335+
/// For API contract/usage, refer to docs for [`GraphListNodesRequest`] and [`GraphListNodesResponse`].
336+
pub async fn graph_list_nodes(
337+
&self, request: GraphListNodesRequest,
338+
) -> Result<GraphListNodesResponse, LdkServerError> {
339+
let url = format!("https://{}/{GRAPH_LIST_NODES_PATH}", self.base_url);
340+
self.post_request(&request, &url).await
341+
}
342+
343+
/// Returns information on a node with the given ID from the network graph.
344+
/// For API contract/usage, refer to docs for [`GraphGetNodeRequest`] and [`GraphGetNodeResponse`].
345+
pub async fn graph_get_node(
346+
&self, request: GraphGetNodeRequest,
347+
) -> Result<GraphGetNodeResponse, LdkServerError> {
348+
let url = format!("https://{}/{GRAPH_GET_NODE_PATH}", self.base_url);
349+
self.post_request(&request, &url).await
350+
}
351+
313352
async fn post_request<Rq: Message, Rs: Message + Default>(
314353
&self, request: &Rq, url: &str,
315354
) -> Result<Rs, LdkServerError> {

ldk-server-protos/src/api.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -761,3 +761,83 @@ pub struct DisconnectPeerRequest {
761761
#[allow(clippy::derive_partial_eq_without_eq)]
762762
#[derive(Clone, PartialEq, ::prost::Message)]
763763
pub struct DisconnectPeerResponse {}
764+
/// Returns a list of all known short channel IDs in the network graph.
765+
/// See more: <https://docs.rs/ldk-node/latest/ldk_node/graph/struct.NetworkGraph.html#method.list_channels>
766+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
767+
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
768+
#[allow(clippy::derive_partial_eq_without_eq)]
769+
#[derive(Clone, PartialEq, ::prost::Message)]
770+
pub struct GraphListChannelsRequest {}
771+
/// The response `content` for the `GraphListChannels` API, when HttpStatusCode is OK (200).
772+
/// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`.
773+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
774+
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
775+
#[allow(clippy::derive_partial_eq_without_eq)]
776+
#[derive(Clone, PartialEq, ::prost::Message)]
777+
pub struct GraphListChannelsResponse {
778+
/// List of short channel IDs known to the network graph.
779+
#[prost(uint64, repeated, tag = "1")]
780+
pub short_channel_ids: ::prost::alloc::vec::Vec<u64>,
781+
}
782+
/// Returns information on a channel with the given short channel ID from the network graph.
783+
/// See more: <https://docs.rs/ldk-node/latest/ldk_node/graph/struct.NetworkGraph.html#method.channel>
784+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
785+
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
786+
#[allow(clippy::derive_partial_eq_without_eq)]
787+
#[derive(Clone, PartialEq, ::prost::Message)]
788+
pub struct GraphGetChannelRequest {
789+
/// The short channel ID to look up.
790+
#[prost(uint64, tag = "1")]
791+
pub short_channel_id: u64,
792+
}
793+
/// The response `content` for the `GraphGetChannel` API, when HttpStatusCode is OK (200).
794+
/// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`.
795+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
796+
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
797+
#[allow(clippy::derive_partial_eq_without_eq)]
798+
#[derive(Clone, PartialEq, ::prost::Message)]
799+
pub struct GraphGetChannelResponse {
800+
/// The channel information.
801+
#[prost(message, optional, tag = "1")]
802+
pub channel: ::core::option::Option<super::types::GraphChannel>,
803+
}
804+
/// Returns a list of all known node IDs in the network graph.
805+
/// See more: <https://docs.rs/ldk-node/latest/ldk_node/graph/struct.NetworkGraph.html#method.list_nodes>
806+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
807+
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
808+
#[allow(clippy::derive_partial_eq_without_eq)]
809+
#[derive(Clone, PartialEq, ::prost::Message)]
810+
pub struct GraphListNodesRequest {}
811+
/// The response `content` for the `GraphListNodes` API, when HttpStatusCode is OK (200).
812+
/// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`.
813+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
814+
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
815+
#[allow(clippy::derive_partial_eq_without_eq)]
816+
#[derive(Clone, PartialEq, ::prost::Message)]
817+
pub struct GraphListNodesResponse {
818+
/// List of hex-encoded node IDs known to the network graph.
819+
#[prost(string, repeated, tag = "1")]
820+
pub node_ids: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
821+
}
822+
/// Returns information on a node with the given ID from the network graph.
823+
/// See more: <https://docs.rs/ldk-node/latest/ldk_node/graph/struct.NetworkGraph.html#method.node>
824+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
825+
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
826+
#[allow(clippy::derive_partial_eq_without_eq)]
827+
#[derive(Clone, PartialEq, ::prost::Message)]
828+
pub struct GraphGetNodeRequest {
829+
/// The hex-encoded node ID to look up.
830+
#[prost(string, tag = "1")]
831+
pub node_id: ::prost::alloc::string::String,
832+
}
833+
/// The response `content` for the `GraphGetNode` API, when HttpStatusCode is OK (200).
834+
/// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`.
835+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
836+
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
837+
#[allow(clippy::derive_partial_eq_without_eq)]
838+
#[derive(Clone, PartialEq, ::prost::Message)]
839+
pub struct GraphGetNodeResponse {
840+
/// The node information.
841+
#[prost(message, optional, tag = "1")]
842+
pub node: ::core::option::Option<super::types::GraphNode>,
843+
}

ldk-server-protos/src/endpoints.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,7 @@ pub const SPONTANEOUS_SEND_PATH: &str = "SpontaneousSend";
3131
pub const SIGN_MESSAGE_PATH: &str = "SignMessage";
3232
pub const VERIFY_SIGNATURE_PATH: &str = "VerifySignature";
3333
pub const EXPORT_PATHFINDING_SCORES_PATH: &str = "ExportPathfindingScores";
34+
pub const GRAPH_LIST_CHANNELS_PATH: &str = "GraphListChannels";
35+
pub const GRAPH_GET_CHANNEL_PATH: &str = "GraphGetChannel";
36+
pub const GRAPH_LIST_NODES_PATH: &str = "GraphListNodes";
37+
pub const GRAPH_GET_NODE_PATH: &str = "GraphGetNode";

ldk-server-protos/src/proto/api.proto

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,3 +596,53 @@ message DisconnectPeerRequest {
596596
// The response `content` for the `DisconnectPeer` API, when HttpStatusCode is OK (200).
597597
// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`.
598598
message DisconnectPeerResponse {}
599+
600+
// Returns a list of all known short channel IDs in the network graph.
601+
// See more: https://docs.rs/ldk-node/latest/ldk_node/graph/struct.NetworkGraph.html#method.list_channels
602+
message GraphListChannelsRequest {}
603+
604+
// The response `content` for the `GraphListChannels` API, when HttpStatusCode is OK (200).
605+
// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`.
606+
message GraphListChannelsResponse {
607+
// List of short channel IDs known to the network graph.
608+
repeated uint64 short_channel_ids = 1;
609+
}
610+
611+
// Returns information on a channel with the given short channel ID from the network graph.
612+
// See more: https://docs.rs/ldk-node/latest/ldk_node/graph/struct.NetworkGraph.html#method.channel
613+
message GraphGetChannelRequest {
614+
// The short channel ID to look up.
615+
uint64 short_channel_id = 1;
616+
}
617+
618+
// The response `content` for the `GraphGetChannel` API, when HttpStatusCode is OK (200).
619+
// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`.
620+
message GraphGetChannelResponse {
621+
// The channel information.
622+
types.GraphChannel channel = 1;
623+
}
624+
625+
// Returns a list of all known node IDs in the network graph.
626+
// See more: https://docs.rs/ldk-node/latest/ldk_node/graph/struct.NetworkGraph.html#method.list_nodes
627+
message GraphListNodesRequest {}
628+
629+
// The response `content` for the `GraphListNodes` API, when HttpStatusCode is OK (200).
630+
// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`.
631+
message GraphListNodesResponse {
632+
// List of hex-encoded node IDs known to the network graph.
633+
repeated string node_ids = 1;
634+
}
635+
636+
// Returns information on a node with the given ID from the network graph.
637+
// See more: https://docs.rs/ldk-node/latest/ldk_node/graph/struct.NetworkGraph.html#method.node
638+
message GraphGetNodeRequest {
639+
// The hex-encoded node ID to look up.
640+
string node_id = 1;
641+
}
642+
643+
// The response `content` for the `GraphGetNode` API, when HttpStatusCode is OK (200).
644+
// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`.
645+
message GraphGetNodeResponse {
646+
// The node information.
647+
types.GraphNode node = 1;
648+
}

0 commit comments

Comments
 (0)