From 42c31412150a0c2dfce9b52edb6c26fd0c09558a Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 3 Mar 2026 13:08:52 +0100 Subject: [PATCH 1/6] Add network graph proto definitions and endpoint constants Add protobuf message types for network graph data: - `GraphRoutingFees`, `GraphChannelUpdate`, `GraphChannel` for channel info - `GraphNodeAnnouncement`, `GraphNode` for node info - Request/response pairs for 4 new endpoints: `GraphListChannels`, `GraphGetChannel`, `GraphListNodes`, `GraphGetNode` - Endpoint path constants for routing Co-Authored-By: HAL 9000 --- ldk-server-protos/src/api.rs | 80 ++++++++++++++++++++ ldk-server-protos/src/endpoints.rs | 4 + ldk-server-protos/src/proto/api.proto | 50 +++++++++++++ ldk-server-protos/src/proto/types.proto | 81 +++++++++++++++++++- ldk-server-protos/src/types.rs | 99 +++++++++++++++++++++++++ 5 files changed, 313 insertions(+), 1 deletion(-) diff --git a/ldk-server-protos/src/api.rs b/ldk-server-protos/src/api.rs index b8d23a75..cfe45508 100644 --- a/ldk-server-protos/src/api.rs +++ b/ldk-server-protos/src/api.rs @@ -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: +#[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, +} +/// Returns information on a channel with the given short channel ID from the network graph. +/// See more: +#[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, +} +/// Returns a list of all known node IDs in the network graph. +/// See more: +#[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: +#[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, +} diff --git a/ldk-server-protos/src/endpoints.rs b/ldk-server-protos/src/endpoints.rs index 6833b501..5766d527 100644 --- a/ldk-server-protos/src/endpoints.rs +++ b/ldk-server-protos/src/endpoints.rs @@ -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"; diff --git a/ldk-server-protos/src/proto/api.proto b/ldk-server-protos/src/proto/api.proto index 57c7b2fe..9e1c9fac 100644 --- a/ldk-server-protos/src/proto/api.proto +++ b/ldk-server-protos/src/proto/api.proto @@ -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; +} diff --git a/ldk-server-protos/src/proto/types.proto b/ldk-server-protos/src/proto/types.proto index 73061692..c49f31d8 100644 --- a/ldk-server-protos/src/proto/types.proto +++ b/ldk-server-protos/src/proto/types.proto @@ -699,8 +699,87 @@ message RouteParametersConfig { // Defaults to 10. uint32 max_path_count = 3; - // Selects the maximum share of a channel's total capacity which will be + // Selects the maximum share of a channel's total capacity which will be // sent over a channel, as a power of 1/2. // Default value: 2 uint32 max_channel_saturation_power_of_half = 4; } + +// Routing fees for a channel as part of the network graph. +message GraphRoutingFees { + // Flat routing fee in millisatoshis. + uint32 base_msat = 1; + + // Liquidity-based routing fee in millionths of a routed amount. + uint32 proportional_millionths = 2; +} + +// Details about one direction of a channel in the network graph, +// as received within a `ChannelUpdate`. +message GraphChannelUpdate { + // When the last update to the channel direction was issued. + // Value is opaque, as set in the announcement. + uint32 last_update = 1; + + // Whether the channel can be currently used for payments (in this one direction). + bool enabled = 2; + + // The difference in CLTV values that you must have when routing through this channel. + uint32 cltv_expiry_delta = 3; + + // The minimum value, which must be relayed to the next hop via the channel. + uint64 htlc_minimum_msat = 4; + + // The maximum value which may be relayed to the next hop via the channel. + uint64 htlc_maximum_msat = 5; + + // Fees charged when the channel is used for routing. + GraphRoutingFees fees = 6; +} + +// Details about a channel in the network graph (both directions). +// Received within a channel announcement. +message GraphChannel { + // Source node of the first direction of the channel (hex-encoded public key). + string node_one = 1; + + // Source node of the second direction of the channel (hex-encoded public key). + string node_two = 2; + + // The channel capacity as seen on-chain, if chain lookup is available. + optional uint64 capacity_sats = 3; + + // Details about the first direction of a channel. + GraphChannelUpdate one_to_two = 4; + + // Details about the second direction of a channel. + GraphChannelUpdate two_to_one = 5; +} + +// Information received in the latest node_announcement from this node. +message GraphNodeAnnouncement { + // When the last known update to the node state was issued. + // Value is opaque, as set in the announcement. + uint32 last_update = 1; + + // Moniker assigned to the node. + // May be invalid or malicious (eg control chars), should not be exposed to the user. + string alias = 2; + + // Color assigned to the node as a hex-encoded RGB string, e.g. "ff0000". + string rgb = 3; + + // List of addresses on which this node is reachable. + repeated string addresses = 4; +} + +// Details about a node in the network graph, known from the network announcement. +message GraphNode { + // All valid channels a node has announced. + repeated uint64 channels = 1; + + // More information about a node from node_announcement. + // Optional because we store a node entry after learning about it from + // a channel announcement, but before receiving a node announcement. + GraphNodeAnnouncement announcement_info = 2; +} diff --git a/ldk-server-protos/src/types.rs b/ldk-server-protos/src/types.rs index 07098166..2437f4fd 100644 --- a/ldk-server-protos/src/types.rs +++ b/ldk-server-protos/src/types.rs @@ -909,6 +909,105 @@ pub struct RouteParametersConfig { #[prost(uint32, tag = "4")] pub max_channel_saturation_power_of_half: u32, } +/// Routing fees for a channel as part of the network graph. +#[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 GraphRoutingFees { + /// Flat routing fee in millisatoshis. + #[prost(uint32, tag = "1")] + pub base_msat: u32, + /// Liquidity-based routing fee in millionths of a routed amount. + #[prost(uint32, tag = "2")] + pub proportional_millionths: u32, +} +/// Details about one direction of a channel in the network graph, +/// as received within a `ChannelUpdate`. +#[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 GraphChannelUpdate { + /// When the last update to the channel direction was issued. + /// Value is opaque, as set in the announcement. + #[prost(uint32, tag = "1")] + pub last_update: u32, + /// Whether the channel can be currently used for payments (in this one direction). + #[prost(bool, tag = "2")] + pub enabled: bool, + /// The difference in CLTV values that you must have when routing through this channel. + #[prost(uint32, tag = "3")] + pub cltv_expiry_delta: u32, + /// The minimum value, which must be relayed to the next hop via the channel. + #[prost(uint64, tag = "4")] + pub htlc_minimum_msat: u64, + /// The maximum value which may be relayed to the next hop via the channel. + #[prost(uint64, tag = "5")] + pub htlc_maximum_msat: u64, + /// Fees charged when the channel is used for routing. + #[prost(message, optional, tag = "6")] + pub fees: ::core::option::Option, +} +/// Details about a channel in the network graph (both directions). +/// Received within a channel announcement. +#[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 GraphChannel { + /// Source node of the first direction of the channel (hex-encoded public key). + #[prost(string, tag = "1")] + pub node_one: ::prost::alloc::string::String, + /// Source node of the second direction of the channel (hex-encoded public key). + #[prost(string, tag = "2")] + pub node_two: ::prost::alloc::string::String, + /// The channel capacity as seen on-chain, if chain lookup is available. + #[prost(uint64, optional, tag = "3")] + pub capacity_sats: ::core::option::Option, + /// Details about the first direction of a channel. + #[prost(message, optional, tag = "4")] + pub one_to_two: ::core::option::Option, + /// Details about the second direction of a channel. + #[prost(message, optional, tag = "5")] + pub two_to_one: ::core::option::Option, +} +/// Information received in the latest node_announcement from this 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 GraphNodeAnnouncement { + /// When the last known update to the node state was issued. + /// Value is opaque, as set in the announcement. + #[prost(uint32, tag = "1")] + pub last_update: u32, + /// Moniker assigned to the node. + /// May be invalid or malicious (eg control chars), should not be exposed to the user. + #[prost(string, tag = "2")] + pub alias: ::prost::alloc::string::String, + /// Color assigned to the node as a hex-encoded RGB string, e.g. "ff0000". + #[prost(string, tag = "3")] + pub rgb: ::prost::alloc::string::String, + /// List of addresses on which this node is reachable. + #[prost(string, repeated, tag = "4")] + pub addresses: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, +} +/// Details about a node in the network graph, known from the network announcement. +#[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 GraphNode { + /// All valid channels a node has announced. + #[prost(uint64, repeated, tag = "1")] + pub channels: ::prost::alloc::vec::Vec, + /// More information about a node from node_announcement. + /// Optional because we store a node entry after learning about it from + /// a channel announcement, but before receiving a node announcement. + #[prost(message, optional, tag = "2")] + pub announcement_info: ::core::option::Option, +} /// Represents the direction of a payment. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] From 3ae3dda621655a90c47377fe3a6ae843a9aa6451 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 3 Mar 2026 13:09:00 +0100 Subject: [PATCH 2/6] Add network graph proto conversion functions Add 5 conversion functions to `proto_adapter.rs` for mapping LDK network graph types to their protobuf representations: - `graph_routing_fees_to_proto` - `graph_channel_update_to_proto` - `graph_channel_to_proto` - `graph_node_announcement_to_proto` - `graph_node_to_proto` Co-Authored-By: HAL 9000 --- ldk-server/src/util/proto_adapter.rs | 56 ++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/ldk-server/src/util/proto_adapter.rs b/ldk-server/src/util/proto_adapter.rs index 2eece481..dea34ac7 100644 --- a/ldk-server/src/util/proto_adapter.rs +++ b/ldk-server/src/util/proto_adapter.rs @@ -14,6 +14,9 @@ use ldk_node::bitcoin::hashes::sha256; use ldk_node::bitcoin::secp256k1::PublicKey; use ldk_node::config::{ChannelConfig, MaxDustHTLCExposure}; use ldk_node::lightning::ln::types::ChannelId; +use ldk_node::lightning::routing::gossip::{ + ChannelInfo, ChannelUpdateInfo, NodeAnnouncementInfo, NodeInfo, RoutingFees, +}; use ldk_node::lightning_invoice::{Bolt11InvoiceDescription, Description, Sha256}; use ldk_node::payment::{ ConfirmationStatus, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, @@ -441,6 +444,59 @@ pub(crate) fn proto_to_bolt11_description( }) } +pub(crate) fn graph_routing_fees_to_proto( + fees: RoutingFees, +) -> ldk_server_protos::types::GraphRoutingFees { + ldk_server_protos::types::GraphRoutingFees { + base_msat: fees.base_msat, + proportional_millionths: fees.proportional_millionths, + } +} + +pub(crate) fn graph_channel_update_to_proto( + update: ChannelUpdateInfo, +) -> ldk_server_protos::types::GraphChannelUpdate { + ldk_server_protos::types::GraphChannelUpdate { + last_update: update.last_update, + enabled: update.enabled, + cltv_expiry_delta: update.cltv_expiry_delta as u32, + htlc_minimum_msat: update.htlc_minimum_msat, + htlc_maximum_msat: update.htlc_maximum_msat, + fees: Some(graph_routing_fees_to_proto(update.fees)), + } +} + +pub(crate) fn graph_channel_to_proto( + channel: ChannelInfo, +) -> ldk_server_protos::types::GraphChannel { + ldk_server_protos::types::GraphChannel { + node_one: channel.node_one.to_string(), + node_two: channel.node_two.to_string(), + capacity_sats: channel.capacity_sats, + one_to_two: channel.one_to_two.map(graph_channel_update_to_proto), + two_to_one: channel.two_to_one.map(graph_channel_update_to_proto), + } +} + +pub(crate) fn graph_node_announcement_to_proto( + announcement: NodeAnnouncementInfo, +) -> ldk_server_protos::types::GraphNodeAnnouncement { + let rgb = announcement.rgb(); + ldk_server_protos::types::GraphNodeAnnouncement { + last_update: announcement.last_update(), + alias: announcement.alias().to_string(), + rgb: format!("{:02x}{:02x}{:02x}", rgb[0], rgb[1], rgb[2]), + addresses: announcement.addresses().iter().map(|a| a.to_string()).collect(), + } +} + +pub(crate) fn graph_node_to_proto(node: NodeInfo) -> ldk_server_protos::types::GraphNode { + ldk_server_protos::types::GraphNode { + channels: node.channels, + announcement_info: node.announcement_info.map(graph_node_announcement_to_proto), + } +} + pub(crate) fn to_error_response(ldk_error: LdkServerError) -> (ErrorResponse, StatusCode) { let error_code = match ldk_error.error_code { InvalidRequestError => ErrorCode::InvalidRequestError, From 10eff7c065aa6bb3291456cde8c735e8eee1fc52 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 3 Mar 2026 13:09:10 +0100 Subject: [PATCH 3/6] Add network graph API handlers and service routing Add 4 new API handlers for querying the network graph: - `graph_list_channels`: returns all known short channel IDs - `graph_get_channel`: returns channel info by short channel ID - `graph_list_nodes`: returns all known node IDs - `graph_get_node`: returns node info by node ID Register the new modules in `mod.rs` and wire up service routing in `service.rs`. Co-Authored-By: HAL 9000 --- ldk-server/src/api/graph_get_channel.rs | 33 ++++++++++++++++++++ ldk-server/src/api/graph_get_node.rs | 37 +++++++++++++++++++++++ ldk-server/src/api/graph_list_channels.rs | 22 ++++++++++++++ ldk-server/src/api/graph_list_nodes.rs | 23 ++++++++++++++ ldk-server/src/api/mod.rs | 4 +++ ldk-server/src/service.rs | 33 ++++++++++++++++++++ 6 files changed, 152 insertions(+) create mode 100644 ldk-server/src/api/graph_get_channel.rs create mode 100644 ldk-server/src/api/graph_get_node.rs create mode 100644 ldk-server/src/api/graph_list_channels.rs create mode 100644 ldk-server/src/api/graph_list_nodes.rs diff --git a/ldk-server/src/api/graph_get_channel.rs b/ldk-server/src/api/graph_get_channel.rs new file mode 100644 index 00000000..3e20a2a0 --- /dev/null +++ b/ldk-server/src/api/graph_get_channel.rs @@ -0,0 +1,33 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +use ldk_server_protos::api::{GraphGetChannelRequest, GraphGetChannelResponse}; + +use crate::api::error::LdkServerError; +use crate::api::error::LdkServerErrorCode::InvalidRequestError; +use crate::service::Context; +use crate::util::proto_adapter::graph_channel_to_proto; + +pub(crate) fn handle_graph_get_channel_request( + context: Context, request: GraphGetChannelRequest, +) -> Result { + let channel_info = + context.node.network_graph().channel(request.short_channel_id).ok_or_else(|| { + LdkServerError::new( + InvalidRequestError, + format!( + "Channel with short_channel_id {} not found in the network graph.", + request.short_channel_id + ), + ) + })?; + + let response = GraphGetChannelResponse { channel: Some(graph_channel_to_proto(channel_info)) }; + Ok(response) +} diff --git a/ldk-server/src/api/graph_get_node.rs b/ldk-server/src/api/graph_get_node.rs new file mode 100644 index 00000000..9ddb0eac --- /dev/null +++ b/ldk-server/src/api/graph_get_node.rs @@ -0,0 +1,37 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +use ldk_node::lightning::routing::gossip::NodeId; +use ldk_server_protos::api::{GraphGetNodeRequest, GraphGetNodeResponse}; + +use crate::api::error::LdkServerError; +use crate::api::error::LdkServerErrorCode::InvalidRequestError; +use crate::service::Context; +use crate::util::proto_adapter::graph_node_to_proto; + +pub(crate) fn handle_graph_get_node_request( + context: Context, request: GraphGetNodeRequest, +) -> Result { + let node_id: NodeId = request.node_id.parse().map_err(|_| { + LdkServerError::new( + InvalidRequestError, + format!("Invalid node_id: {}. Expected a hex-encoded public key.", request.node_id), + ) + })?; + + let node_info = context.node.network_graph().node(&node_id).ok_or_else(|| { + LdkServerError::new( + InvalidRequestError, + format!("Node with ID {} not found in the network graph.", request.node_id), + ) + })?; + + let response = GraphGetNodeResponse { node: Some(graph_node_to_proto(node_info)) }; + Ok(response) +} diff --git a/ldk-server/src/api/graph_list_channels.rs b/ldk-server/src/api/graph_list_channels.rs new file mode 100644 index 00000000..60543292 --- /dev/null +++ b/ldk-server/src/api/graph_list_channels.rs @@ -0,0 +1,22 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +use ldk_server_protos::api::{GraphListChannelsRequest, GraphListChannelsResponse}; + +use crate::api::error::LdkServerError; +use crate::service::Context; + +pub(crate) fn handle_graph_list_channels_request( + context: Context, _request: GraphListChannelsRequest, +) -> Result { + let short_channel_ids = context.node.network_graph().list_channels(); + + let response = GraphListChannelsResponse { short_channel_ids }; + Ok(response) +} diff --git a/ldk-server/src/api/graph_list_nodes.rs b/ldk-server/src/api/graph_list_nodes.rs new file mode 100644 index 00000000..64ccd295 --- /dev/null +++ b/ldk-server/src/api/graph_list_nodes.rs @@ -0,0 +1,23 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +use ldk_server_protos::api::{GraphListNodesRequest, GraphListNodesResponse}; + +use crate::api::error::LdkServerError; +use crate::service::Context; + +pub(crate) fn handle_graph_list_nodes_request( + context: Context, _request: GraphListNodesRequest, +) -> Result { + let node_ids = + context.node.network_graph().list_nodes().into_iter().map(|n| n.to_string()).collect(); + + let response = GraphListNodesResponse { node_ids }; + Ok(response) +} diff --git a/ldk-server/src/api/mod.rs b/ldk-server/src/api/mod.rs index 1bba0574..b4a6089b 100644 --- a/ldk-server/src/api/mod.rs +++ b/ldk-server/src/api/mod.rs @@ -26,6 +26,10 @@ pub(crate) mod export_pathfinding_scores; pub(crate) mod get_balances; pub(crate) mod get_node_info; pub(crate) mod get_payment_details; +pub(crate) mod graph_get_channel; +pub(crate) mod graph_get_node; +pub(crate) mod graph_list_channels; +pub(crate) mod graph_list_nodes; pub(crate) mod list_channels; pub(crate) mod list_forwarded_payments; pub(crate) mod list_payments; diff --git a/ldk-server/src/service.rs b/ldk-server/src/service.rs index cdf48278..d10d6156 100644 --- a/ldk-server/src/service.rs +++ b/ldk-server/src/service.rs @@ -22,6 +22,7 @@ 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, @@ -41,6 +42,10 @@ use crate::api::export_pathfinding_scores::handle_export_pathfinding_scores_requ use crate::api::get_balances::handle_get_balances_request; use crate::api::get_node_info::handle_get_node_info_request; use crate::api::get_payment_details::handle_get_payment_details_request; +use crate::api::graph_get_channel::handle_graph_get_channel_request; +use crate::api::graph_get_node::handle_graph_get_node_request; +use crate::api::graph_list_channels::handle_graph_list_channels_request; +use crate::api::graph_list_nodes::handle_graph_list_nodes_request; use crate::api::list_channels::handle_list_channels_request; use crate::api::list_forwarded_payments::handle_list_forwarded_payments_request; use crate::api::list_payments::handle_list_payments_request; @@ -333,6 +338,34 @@ impl Service> for NodeService { api_key, handle_export_pathfinding_scores_request, )), + GRAPH_LIST_CHANNELS_PATH => Box::pin(handle_request( + context, + req, + auth_params, + api_key, + handle_graph_list_channels_request, + )), + GRAPH_GET_CHANNEL_PATH => Box::pin(handle_request( + context, + req, + auth_params, + api_key, + handle_graph_get_channel_request, + )), + GRAPH_LIST_NODES_PATH => Box::pin(handle_request( + context, + req, + auth_params, + api_key, + handle_graph_list_nodes_request, + )), + GRAPH_GET_NODE_PATH => Box::pin(handle_request( + context, + req, + auth_params, + api_key, + handle_graph_get_node_request, + )), path => { let error = format!("Unknown request: {}", path).into_bytes(); Box::pin(async { From 860bd6c3f493b2fe5c5ce3b1882ecd468fbea28b Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 3 Mar 2026 13:09:19 +0100 Subject: [PATCH 4/6] Add network graph client methods to `ldk-server-client` Add 4 async methods to `LdkServerClient` for querying the network graph: - `graph_list_channels` - `graph_get_channel` - `graph_list_nodes` - `graph_get_node` Co-Authored-By: HAL 9000 --- ldk-server-client/src/client.rs | 51 +++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/ldk-server-client/src/client.rs b/ldk-server-client/src/client.rs index f810467e..04b1dc53 100644 --- a/ldk-server-client/src/client.rs +++ b/ldk-server-client/src/client.rs @@ -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, @@ -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 { + 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 { + 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 { + 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 { + let url = format!("https://{}/{GRAPH_GET_NODE_PATH}", self.base_url); + self.post_request(&request, &url).await + } + async fn post_request( &self, request: &Rq, url: &str, ) -> Result { From 74a55bf14a06b157b1ff17eb92986d5e5fa18680 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 3 Mar 2026 13:09:27 +0100 Subject: [PATCH 5/6] Add network graph CLI commands to `ldk-server-cli` Add 4 new subcommands for querying the network graph: - `graph-list-channels`: list all known short channel IDs - `graph-get-channel `: get channel info by SCID - `graph-list-nodes`: list all known node IDs - `graph-get-node `: get node info by node ID Co-Authored-By: HAL 9000 --- ldk-server-cli/src/main.rs | 45 ++++++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/ldk-server-cli/src/main.rs b/ldk-server-cli/src/main.rs index 48d24ef4..139380aa 100644 --- a/ldk-server-cli/src/main.rs +++ b/ldk-server-cli/src/main.rs @@ -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, }; @@ -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( @@ -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"), } } From 4d90c747872e7d0721351b9ed29da7c9611e0e3b Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 3 Mar 2026 14:58:09 +0100 Subject: [PATCH 6/6] Add e2e tests for network graph API endpoints Co-Authored-By: HAL 9000 --- e2e-tests/tests/e2e.rs | 64 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/e2e-tests/tests/e2e.rs b/e2e-tests/tests/e2e.rs index 9a016999..034926e0 100644 --- a/e2e-tests/tests/e2e.rs +++ b/e2e-tests/tests/e2e.rs @@ -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();