From 4222171e479a290eb069f82e525e288a00df5344 Mon Sep 17 00:00:00 2001 From: Enigbe Date: Thu, 26 Feb 2026 17:47:03 +0100 Subject: [PATCH 1/2] Expose node features in get-node-info We return the node-announcement feature set from GetNodeInfoResponse so clients can inspect advertised node capabilities, such as keysend support, directly from the node info API. For now, only features advertised in node_announcement are populated. We document this narrower contract explicitly while leaving the Features message shape open for future parity with APIs such as cln-grpc, where the default feature set may also include init, channel, and invoice features. AI-Assisted-By: OpenAI Codex --- e2e-tests/tests/e2e.rs | 13 +++++++++++++ ldk-server-grpc/src/api.rs | 4 ++++ ldk-server-grpc/src/proto/api.proto | 4 ++++ ldk-server-grpc/src/proto/types.proto | 7 +++++++ ldk-server-grpc/src/types.rs | 12 ++++++++++++ ldk-server/src/api/get_node_info.rs | 6 +++++- 6 files changed, 45 insertions(+), 1 deletion(-) diff --git a/e2e-tests/tests/e2e.rs b/e2e-tests/tests/e2e.rs index b51bb460..a1c3f7dd 100644 --- a/e2e-tests/tests/e2e.rs +++ b/e2e-tests/tests/e2e.rs @@ -20,6 +20,8 @@ use hex_conservative::{DisplayHex, FromHex}; use ldk_node::bitcoin::hashes::{sha256, Hash}; use ldk_node::lightning::ln::msgs::SocketAddress; use ldk_node::lightning::offers::offer::Offer; +use ldk_node::lightning::util::ser::Readable; +use ldk_node::lightning_types::features::NodeFeatures; use ldk_node::lightning_invoice::Bolt11Invoice; use ldk_server_client::client::EventStream; use ldk_server_client::ldk_server_grpc::api::{ @@ -56,6 +58,17 @@ async fn test_cli_get_node_info() { let output = run_cli(&server, &["get-node-info"]); assert!(output.get("node_id").is_some()); assert_eq!(output["node_id"], server.node_id()); + + // Ensure clients can decode advertised node capabilities from get-node-info. + let node_feature_bytes: Vec = output["features"]["node"] + .as_array() + .unwrap() + .iter() + .map(|byte| byte.as_u64().unwrap() as u8) + .collect(); + let mut node_feature_bytes = node_feature_bytes.as_slice(); + let node_features = NodeFeatures::read(&mut node_feature_bytes).unwrap(); + assert!(node_features.supports_keysend()); } #[tokio::test] diff --git a/ldk-server-grpc/src/api.rs b/ldk-server-grpc/src/api.rs index b4eda4ed..2aad69b9 100644 --- a/ldk-server-grpc/src/api.rs +++ b/ldk-server-grpc/src/api.rs @@ -85,6 +85,10 @@ pub struct GetNodeInfoResponse { #[prost(enumeration = "super::types::Network", tag = "13")] #[cfg_attr(feature = "serde", serde(serialize_with = "crate::serde_utils::serialize_network"))] pub network: i32, + /// The feature sets advertised by this node. Currently only node-announcement + /// features are populated. + #[prost(message, optional, tag = "14")] + pub features: ::core::option::Option, } /// Retrieve a new on-chain funding address. /// See more: diff --git a/ldk-server-grpc/src/proto/api.proto b/ldk-server-grpc/src/proto/api.proto index 516c406b..a45443df 100644 --- a/ldk-server-grpc/src/proto/api.proto +++ b/ldk-server-grpc/src/proto/api.proto @@ -73,6 +73,10 @@ message GetNodeInfoResponse { // The Bitcoin network the node is running on (e.g., "bitcoin", "testnet", "signet", "regtest"). types.Network network = 13; + + // The feature sets advertised by this node. Currently only node-announcement + // features are populated. + types.Features features = 14; } // Retrieve a new on-chain funding address. diff --git a/ldk-server-grpc/src/proto/types.proto b/ldk-server-grpc/src/proto/types.proto index efe152d6..63f1acf2 100644 --- a/ldk-server-grpc/src/proto/types.proto +++ b/ldk-server-grpc/src/proto/types.proto @@ -943,3 +943,10 @@ message CustomTlvRecord { // Raw TLV value. bytes value = 2; } + +// The feature sets advertised by this node. Currently only node-announcement +// features are populated. +message Features { + // Serialized node-announcement features. + bytes node = 1; +} diff --git a/ldk-server-grpc/src/types.rs b/ldk-server-grpc/src/types.rs index 54e47f87..901da7ca 100644 --- a/ldk-server-grpc/src/types.rs +++ b/ldk-server-grpc/src/types.rs @@ -1243,6 +1243,18 @@ pub struct CustomTlvRecord { #[prost(bytes = "bytes", tag = "2")] pub value: ::prost::bytes::Bytes, } +/// The feature sets advertised by this node. Currently only node-announcement +/// features are populated. +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +#[cfg_attr(feature = "serde", serde(default))] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Features { + /// Serialized node-announcement features. + #[prost(bytes = "bytes", tag = "1")] + pub node: ::prost::bytes::Bytes, +} /// Represents the direction of a payment. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] diff --git a/ldk-server/src/api/get_node_info.rs b/ldk-server/src/api/get_node_info.rs index 1b5aa109..2044eb9b 100644 --- a/ldk-server/src/api/get_node_info.rs +++ b/ldk-server/src/api/get_node_info.rs @@ -9,8 +9,9 @@ use std::sync::Arc; +use ldk_node::lightning::util::ser::Writeable; use ldk_server_grpc::api::{GetNodeInfoRequest, GetNodeInfoResponse}; -use ldk_server_grpc::types::BestBlock; +use ldk_server_grpc::types::{BestBlock, Features}; use crate::api::error::LdkServerError; use crate::service::Context; @@ -26,6 +27,8 @@ pub(crate) async fn handle_get_node_info_request( height: node_status.current_best_block.height, }; + let features = Features { node: node_status.node_features.encode().into() }; + let listening_addresses: Vec = context .node .listening_addresses() @@ -66,6 +69,7 @@ pub(crate) async fn handle_get_node_info_request( node_alias, node_uris, network, + features: Some(features), }; Ok(response) } From 795b6955a10cf8e6ee2f45ebf71872b54ed48b44 Mon Sep 17 00:00:00 2001 From: Enigbe Date: Sat, 13 Jun 2026 01:43:35 +0100 Subject: [PATCH 2/2] fixup! Expose node features in get-node-info Decode exposed features into semantic entries We replace raw feature bytes, as previously used in NodeFeatures, with decoded Feature entries so clients can inspect the feature name, support bit, required bit, and known-ness directly. Given the structure of LDK Features, which are different based on contexts, we create and use this shared Feature shape for invoice, offer, and node feature contexts, with the conversion logic centralized in proto_adapter (see features_to_proto). Additionally, we also update the CLI e2e assertions to check keysend support through the readable get-node-info feature response. --- e2e-tests/tests/e2e.rs | 28 +++++---- ldk-server-grpc/src/api.rs | 14 +++-- ldk-server-grpc/src/proto/api.proto | 8 +-- ldk-server-grpc/src/proto/types.proto | 27 ++++++--- ldk-server-grpc/src/types.rs | 27 ++++++--- ldk-server/src/api/decode_invoice.rs | 4 +- ldk-server/src/api/decode_offer.rs | 4 +- ldk-server/src/api/get_node_info.rs | 10 +++- ldk-server/src/api/mod.rs | 58 +------------------ ldk-server/src/util/proto_adapter.rs | 83 ++++++++++++++++++++++++++- 10 files changed, 157 insertions(+), 106 deletions(-) diff --git a/e2e-tests/tests/e2e.rs b/e2e-tests/tests/e2e.rs index a1c3f7dd..cf35bf45 100644 --- a/e2e-tests/tests/e2e.rs +++ b/e2e-tests/tests/e2e.rs @@ -20,8 +20,6 @@ use hex_conservative::{DisplayHex, FromHex}; use ldk_node::bitcoin::hashes::{sha256, Hash}; use ldk_node::lightning::ln::msgs::SocketAddress; use ldk_node::lightning::offers::offer::Offer; -use ldk_node::lightning::util::ser::Readable; -use ldk_node::lightning_types::features::NodeFeatures; use ldk_node::lightning_invoice::Bolt11Invoice; use ldk_server_client::client::EventStream; use ldk_server_client::ldk_server_grpc::api::{ @@ -59,16 +57,14 @@ async fn test_cli_get_node_info() { assert!(output.get("node_id").is_some()); assert_eq!(output["node_id"], server.node_id()); - // Ensure clients can decode advertised node capabilities from get-node-info. - let node_feature_bytes: Vec = output["features"]["node"] - .as_array() - .unwrap() - .iter() - .map(|byte| byte.as_u64().unwrap() as u8) - .collect(); - let mut node_feature_bytes = node_feature_bytes.as_slice(); - let node_features = NodeFeatures::read(&mut node_feature_bytes).unwrap(); - assert!(node_features.supports_keysend()); + // Ensure clients can inspect advertised node capabilities from get-node-info. + let keysend = &output["features"]["node"]["Keysend"]; + assert_eq!(keysend["name"], "Keysend"); + assert_eq!(keysend["is_supported"], true); + assert_eq!(keysend["is_required"], false); + assert_eq!(keysend["is_known"], true); + assert_eq!(keysend["supported_bit"], 55); + assert_eq!(keysend["required_bit"], 54); } #[tokio::test] @@ -239,12 +235,14 @@ async fn test_cli_decode_invoice() { feature_names ); - // Every entry should have the expected structure - for (bit, feature) in features { - assert!(bit.parse::().is_ok(), "Feature key should be a bit number: {}", bit); + // Every entry should have the expected structure. + for (name, feature) in features { + assert_eq!(feature["name"], *name); assert!(feature.get("name").is_some(), "Feature missing name field"); + assert!(feature.get("is_supported").is_some(), "Feature missing is_supported field"); assert!(feature.get("is_required").is_some(), "Feature missing is_required field"); assert!(feature.get("is_known").is_some(), "Feature missing is_known field"); + assert!(feature.get("supported_bit").is_some(), "Feature missing supported_bit field"); } // Also test a variable-amount invoice diff --git a/ldk-server-grpc/src/api.rs b/ldk-server-grpc/src/api.rs index 2aad69b9..38d602b7 100644 --- a/ldk-server-grpc/src/api.rs +++ b/ldk-server-grpc/src/api.rs @@ -1168,9 +1168,10 @@ pub struct DecodeInvoiceResponse { /// Route hints for finding a path to the payee. #[prost(message, repeated, tag = "10")] pub route_hints: ::prost::alloc::vec::Vec, - /// Feature bits advertised in the invoice, keyed by bit number. - #[prost(map = "uint32, message", tag = "11")] - pub features: ::std::collections::HashMap, + /// Features advertised in the invoice, keyed by feature name. + #[prost(map = "string, message", tag = "11")] + pub features: + ::std::collections::HashMap<::prost::alloc::string::String, super::types::Feature>, /// The currency or network (e.g., "bitcoin", "testnet", "signet", "regtest"). #[prost(string, tag = "12")] pub currency: ::prost::alloc::string::String, @@ -1224,9 +1225,10 @@ pub struct DecodeOfferResponse { /// Blinded paths to the offer recipient. #[prost(message, repeated, tag = "8")] pub paths: ::prost::alloc::vec::Vec, - /// Feature bits advertised in the offer, keyed by bit number. - #[prost(map = "uint32, message", tag = "9")] - pub features: ::std::collections::HashMap, + /// Features advertised in the offer, keyed by feature name. + #[prost(map = "string, message", tag = "9")] + pub features: + ::std::collections::HashMap<::prost::alloc::string::String, super::types::Feature>, /// Supported blockchain networks (e.g., "bitcoin", "testnet", "signet", "regtest"). #[prost(string, repeated, tag = "10")] pub chains: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, diff --git a/ldk-server-grpc/src/proto/api.proto b/ldk-server-grpc/src/proto/api.proto index a45443df..e2be4908 100644 --- a/ldk-server-grpc/src/proto/api.proto +++ b/ldk-server-grpc/src/proto/api.proto @@ -844,8 +844,8 @@ message DecodeInvoiceResponse { // Route hints for finding a path to the payee. repeated types.Bolt11RouteHint route_hints = 10; - // Feature bits advertised in the invoice, keyed by bit number. - map features = 11; + // Features advertised in the invoice, keyed by feature name. + map features = 11; // The currency or network (e.g., "bitcoin", "testnet", "signet", "regtest"). string currency = 12; @@ -890,8 +890,8 @@ message DecodeOfferResponse { // Blinded paths to the offer recipient. repeated types.BlindedPath paths = 8; - // Feature bits advertised in the offer, keyed by bit number. - map features = 9; + // Features advertised in the offer, keyed by feature name. + map features = 9; // Supported blockchain networks (e.g., "bitcoin", "testnet", "signet", "regtest"). repeated string chains = 10; diff --git a/ldk-server-grpc/src/proto/types.proto b/ldk-server-grpc/src/proto/types.proto index 63f1acf2..ab0a16d4 100644 --- a/ldk-server-grpc/src/proto/types.proto +++ b/ldk-server-grpc/src/proto/types.proto @@ -924,16 +924,27 @@ enum ChannelDirection { NODE_TWO = 1; } -// A feature bit advertised in a BOLT11 invoice. -message Bolt11Feature { +// A feature advertised in a BOLT feature context. +message Feature { // Human-readable feature name. string name = 1; - // Whether this feature is required. - bool is_required = 2; + // Whether this feature's support bit is set. + bool is_supported = 2; - // Whether this feature is known. - bool is_known = 3; + // Whether this feature's required bit is set. + bool is_required = 3; + + // Whether this feature is known by LDK. + bool is_known = 4; + + // The BOLT 9 bit that indicates support for this feature. + uint32 supported_bit = 5; + + // The BOLT 9 bit that requires support for this feature, if one exists. + // Optional because some feature contexts include support-only features without + // a corresponding required bit, e.g. `initial_routing_sync` in init features. + optional uint32 required_bit = 6; } // Custom TLV record attached to a payment. @@ -947,6 +958,6 @@ message CustomTlvRecord { // The feature sets advertised by this node. Currently only node-announcement // features are populated. message Features { - // Serialized node-announcement features. - bytes node = 1; + // Node-announcement features keyed by feature name. + map node = 1; } diff --git a/ldk-server-grpc/src/types.rs b/ldk-server-grpc/src/types.rs index 901da7ca..85fd62bb 100644 --- a/ldk-server-grpc/src/types.rs +++ b/ldk-server-grpc/src/types.rs @@ -1212,22 +1212,33 @@ pub struct DirectedShortChannelId { )] pub direction: i32, } -/// A feature bit advertised in a BOLT11 invoice. +/// A feature advertised in a BOLT feature context. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] #[cfg_attr(feature = "serde", serde(default))] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct Bolt11Feature { +pub struct Feature { /// Human-readable feature name. #[prost(string, tag = "1")] pub name: ::prost::alloc::string::String, - /// Whether this feature is required. + /// Whether this feature's support bit is set. #[prost(bool, tag = "2")] - pub is_required: bool, - /// Whether this feature is known. + pub is_supported: bool, + /// Whether this feature's required bit is set. #[prost(bool, tag = "3")] + pub is_required: bool, + /// Whether this feature is known by LDK. + #[prost(bool, tag = "4")] pub is_known: bool, + /// The BOLT 9 bit that indicates support for this feature. + #[prost(uint32, tag = "5")] + pub supported_bit: u32, + /// The BOLT 9 bit that requires support for this feature, if one exists. + /// Optional because some feature contexts include support-only features without + /// a corresponding required bit, e.g. `initial_routing_sync` in init features. + #[prost(uint32, optional, tag = "6")] + pub required_bit: ::core::option::Option, } /// Custom TLV record attached to a payment. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -1251,9 +1262,9 @@ pub struct CustomTlvRecord { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct Features { - /// Serialized node-announcement features. - #[prost(bytes = "bytes", tag = "1")] - pub node: ::prost::bytes::Bytes, + /// Node-announcement features keyed by feature name. + #[prost(map = "string, message", tag = "1")] + pub node: ::std::collections::HashMap<::prost::alloc::string::String, Feature>, } /// Represents the direction of a payment. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] diff --git a/ldk-server/src/api/decode_invoice.rs b/ldk-server/src/api/decode_invoice.rs index e12b9de1..ef7479e2 100644 --- a/ldk-server/src/api/decode_invoice.rs +++ b/ldk-server/src/api/decode_invoice.rs @@ -16,9 +16,9 @@ use ldk_node::lightning_types::features::Bolt11InvoiceFeatures; use ldk_server_grpc::api::{DecodeInvoiceRequest, DecodeInvoiceResponse}; use ldk_server_grpc::types::{Bolt11HopHint, Bolt11RouteHint}; -use crate::api::decode_features; use crate::api::error::LdkServerError; use crate::service::Context; +use crate::util::proto_adapter::features_to_proto; pub(crate) async fn handle_decode_invoice_request( _context: Arc, request: DecodeInvoiceRequest, @@ -66,7 +66,7 @@ pub(crate) async fn handle_decode_invoice_request( let features = invoice .features() .map(|f| { - decode_features(f.le_flags(), |bytes| { + features_to_proto(f.le_flags(), |bytes| { Bolt11InvoiceFeatures::from_le_bytes(bytes).to_string() }) }) diff --git a/ldk-server/src/api/decode_offer.rs b/ldk-server/src/api/decode_offer.rs index 7669ba5d..c5227859 100644 --- a/ldk-server/src/api/decode_offer.rs +++ b/ldk-server/src/api/decode_offer.rs @@ -24,9 +24,9 @@ use ldk_server_grpc::types::{ OfferQuantity, }; -use crate::api::decode_features; use crate::api::error::LdkServerError; use crate::service::Context; +use crate::util::proto_adapter::features_to_proto; pub(crate) async fn handle_decode_offer_request( _context: Arc, request: DecodeOfferRequest, @@ -104,7 +104,7 @@ pub(crate) async fn handle_decode_offer_request( }) .collect(); - let features = decode_features(offer.offer_features().le_flags(), |bytes| { + let features = features_to_proto(offer.offer_features().le_flags(), |bytes| { OfferFeatures::from_le_bytes(bytes).to_string() }); diff --git a/ldk-server/src/api/get_node_info.rs b/ldk-server/src/api/get_node_info.rs index 2044eb9b..e76b3236 100644 --- a/ldk-server/src/api/get_node_info.rs +++ b/ldk-server/src/api/get_node_info.rs @@ -9,13 +9,13 @@ use std::sync::Arc; -use ldk_node::lightning::util::ser::Writeable; +use ldk_node::lightning_types::features::NodeFeatures; use ldk_server_grpc::api::{GetNodeInfoRequest, GetNodeInfoResponse}; use ldk_server_grpc::types::{BestBlock, Features}; use crate::api::error::LdkServerError; use crate::service::Context; -use crate::util::proto_adapter::network_to_proto; +use crate::util::proto_adapter::{features_to_proto, network_to_proto}; pub(crate) async fn handle_get_node_info_request( context: Arc, _request: GetNodeInfoRequest, @@ -27,7 +27,11 @@ pub(crate) async fn handle_get_node_info_request( height: node_status.current_best_block.height, }; - let features = Features { node: node_status.node_features.encode().into() }; + let features = Features { + node: features_to_proto(node_status.node_features.le_flags(), |bytes| { + NodeFeatures::from_le_bytes(bytes).to_string() + }), + }; let listening_addresses: Vec = context .node diff --git a/ldk-server/src/api/mod.rs b/ldk-server/src/api/mod.rs index 7163c53a..2947257d 100644 --- a/ldk-server/src/api/mod.rs +++ b/ldk-server/src/api/mod.rs @@ -7,13 +7,11 @@ // You may not use this file except in accordance with one or both of these // licenses. -use std::collections::HashMap; - use ldk_node::config::{ChannelConfig, MaxDustHTLCExposure}; use ldk_node::lightning::routing::router::RouteParametersConfig; use ldk_node::CustomTlvRecord as NodeCustomTlvRecord; use ldk_server_grpc::types::channel_config::MaxDustHtlcExposure; -use ldk_server_grpc::types::{Bolt11Feature, CustomTlvRecord as ProtoCustomTlvRecord}; +use ldk_server_grpc::types::CustomTlvRecord as ProtoCustomTlvRecord; use crate::api::error::LdkServerError; use crate::api::error::LdkServerErrorCode::InvalidRequestError; @@ -138,60 +136,6 @@ pub(crate) fn node_to_proto_custom_tlv(node: &NodeCustomTlvRecord) -> ProtoCusto ProtoCustomTlvRecord { type_num: node.type_num, value: node.value.clone().into() } } -/// Decodes feature flags into a map keyed by bit number. Feature names are derived -/// from LDK's `Features::Display` impl, so they stay in sync automatically. -/// -/// `make_display` should construct a `Features` from the given LE bytes and return -/// its `to_string()` output — this lets us probe LDK for the name of each set bit. -pub(crate) fn decode_features( - le_flags: &[u8], make_display: impl Fn(Vec) -> String, -) -> HashMap { - let mut features = HashMap::new(); - for (byte_idx, &byte) in le_flags.iter().enumerate() { - if byte == 0 { - continue; - } - for bit_pos in 0..8u32 { - if byte & (1 << bit_pos) != 0 { - let bit_number = (byte_idx as u32) * 8 + bit_pos; - let is_required = bit_number % 2 == 0; - - // Create Features with just this bit set and use Display to get the name. - let mut single_bit = vec![0u8; byte_idx + 1]; - single_bit[byte_idx] = 1 << bit_pos; - let display = make_display(single_bit); - let (name, is_known) = parse_feature_name(&display); - - features.insert( - bit_number, - Bolt11Feature { name: name.to_string(), is_required, is_known }, - ); - } - } - } - features -} - -/// Parse the Display output of a single-bit Features to find which feature is set. -/// -/// LDK's Display format is: "Name: status, Name: status, ..., unknown flags: status" -/// where status is "required", "supported", or "not supported". -/// For a single-bit Features, exactly one entry will be "required" or "supported". -fn parse_feature_name(display: &str) -> (&str, bool) { - for entry in display.split(", ") { - if let Some((name, status)) = entry.split_once(": ") { - if name == "unknown flags" { - if status == "required" || status == "supported" { - return ("unknown", false); - } - } else if status == "required" || status == "supported" { - return (name, true); - } - } - } - ("unknown", false) -} - #[cfg(test)] mod tests { use super::*; diff --git a/ldk-server/src/util/proto_adapter.rs b/ldk-server/src/util/proto_adapter.rs index ae75ebe5..475b0977 100644 --- a/ldk-server/src/util/proto_adapter.rs +++ b/ldk-server/src/util/proto_adapter.rs @@ -9,6 +9,8 @@ use bytes::Bytes; use hex::prelude::*; +use std::collections::HashMap; + use ldk_node::bitcoin::hashes::sha256; use ldk_node::bitcoin::Network; use ldk_node::config::{ChannelConfig, MaxDustHTLCExposure}; @@ -33,7 +35,8 @@ use ldk_server_grpc::types::pending_sweep_balance::BalanceType::{ AwaitingThresholdConfirmations, BroadcastAwaitingConfirmation, PendingBroadcast, }; use ldk_server_grpc::types::{ - bolt11_invoice_description, Channel, ForwardedPayment, HtlcLocator, OutPoint, Payment, Peer, + bolt11_invoice_description, Channel, Feature, ForwardedPayment, HtlcLocator, OutPoint, Payment, + Peer, }; use crate::api::error::LdkServerError; @@ -484,6 +487,84 @@ pub(crate) fn graph_node_to_proto(node: NodeInfo) -> ldk_server_grpc::types::Gra } } +/// Converts LDK feature flags into proto features keyed by feature name. +/// +/// Feature names are derived from LDK's `Features::Display` impl, so they stay +/// in sync automatically. If both the required and supported bits are set for +/// the same feature, they are merged into a single proto `Feature`. +pub(crate) fn features_to_proto( + le_flags: &[u8], make_display: impl Fn(Vec) -> String, +) -> HashMap { + let mut features = HashMap::new(); + for (byte_idx, &byte) in le_flags.iter().enumerate() { + if byte == 0 { + continue; + } + for bit_pos in 0..8u32 { + if byte & (1 << bit_pos) != 0 { + let bit_number = (byte_idx as u32) * 8 + bit_pos; + let is_required = bit_number % 2 == 0; + let required_bit = if is_required { + Some(bit_number) + } else if bit_number == 3 { + // `initial_routing_sync` uses bit 3 and has no required/even bit per BOLT 9. + None + } else { + // BOLT 9 feature pairs use an even required bit followed by an odd support bit. + Some(bit_number - 1) + }; + let supported_bit = if is_required { bit_number + 1 } else { bit_number }; + + // Create Features with just this bit set and use Display to get the name. + let mut single_bit = vec![0u8; byte_idx + 1]; + single_bit[byte_idx] = 1 << bit_pos; + let display = make_display(single_bit); + let (parsed_name, is_known) = parse_feature_name(&display); + let name = if is_known { + parsed_name.to_string() + } else { + let required = required_bit + .map(|bit| bit.to_string()) + .unwrap_or_else(|| "none".to_string()); + format!("UnknownFeature{required}_{supported_bit}") + }; + + let feature = features.entry(name.clone()).or_insert_with(|| Feature { + name, + is_supported: false, + is_required: false, + is_known, + supported_bit, + required_bit, + }); + feature.is_supported = true; + feature.is_required |= is_required; + } + } + } + features +} + +/// Parse the Display output of a single-bit Features to find which feature is set. +/// +/// LDK's Display format is: "Name: status, Name: status, ..., unknown flags: status" +/// where status is "required", "supported", or "not supported". +/// For a single-bit Features, exactly one entry will be "required" or "supported". +fn parse_feature_name(display: &str) -> (&str, bool) { + for entry in display.split(", ") { + if let Some((name, status)) = entry.split_once(": ") { + if name == "unknown flags" { + if status == "required" || status == "supported" { + return ("unknown", false); + } + } else if status == "required" || status == "supported" { + return (name, true); + } + } + } + ("unknown", false) +} + pub(crate) fn network_to_proto(network: Network) -> ldk_server_grpc::types::Network { use ldk_server_grpc::types::Network as ProtoNetwork; match network {