diff --git a/protos/src/lib.rs b/protos/src/lib.rs index 3d62d28..f20bb9e 100644 --- a/protos/src/lib.rs +++ b/protos/src/lib.rs @@ -1,3 +1,50 @@ +/// Retrieve the latest node info like `node_id`, `current_best_block` etc. +/// See more: +/// - +/// - +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetNodeInfoRequest {} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetNodeInfoResponse { + /// The hex-encoded `node-id` or public key for our own lightning node. + #[prost(string, tag = "1")] + pub node_id: ::prost::alloc::string::String, + /// The best block to which our Lightning wallet is currently synced. + /// + /// Should be always set, will never be `None`. + #[prost(message, optional, tag = "3")] + pub current_best_block: ::core::option::Option, + /// The timestamp, in seconds since start of the UNIX epoch, when we last successfully synced our Lightning wallet + /// to the chain tip. + /// + /// Will be `None` if the wallet hasn’t been synced since the node was initialized. + #[prost(uint64, optional, tag = "4")] + pub latest_wallet_sync_timestamp: ::core::option::Option, + /// The timestamp, in seconds since start of the UNIX epoch, when we last successfully synced our on-chain + /// wallet to the chain tip. + /// + /// Will be `None` if the wallet hasn’t been synced since the node was initialized. + #[prost(uint64, optional, tag = "5")] + pub latest_onchain_wallet_sync_timestamp: ::core::option::Option, + /// The timestamp, in seconds since start of the UNIX epoch, when we last successfully update our fee rate cache. + /// + /// Will be `None` if the cache hasn’t been updated since the node was initialized. + #[prost(uint64, optional, tag = "6")] + pub latest_fee_rate_cache_update_timestamp: ::core::option::Option, + /// The timestamp, in seconds since start of the UNIX epoch, when the last rapid gossip sync (RGS) snapshot we + /// successfully applied was generated. + /// + /// Will be `None` if RGS isn’t configured or the snapshot hasn’t been updated since the node was initialized. + #[prost(uint64, optional, tag = "7")] + pub latest_rgs_snapshot_timestamp: ::core::option::Option, + /// The timestamp, in seconds since start of the UNIX epoch, when we last broadcasted a node announcement. + /// + /// Will be `None` if we have no public channels or we haven’t broadcasted since the node was initialized. + #[prost(uint64, optional, tag = "8")] + pub latest_node_announcement_broadcast_timestamp: ::core::option::Option, +} /// Retrieve a new on-chain funding address. /// See more: #[allow(clippy::derive_partial_eq_without_eq)] @@ -184,28 +231,28 @@ pub struct ChannelConfig { /// Amount (in millionths of a satoshi) charged per satoshi for payments forwarded outbound /// over the channel. /// See more: - #[prost(uint32, tag = "1")] - pub forwarding_fee_proportional_millionths: u32, + #[prost(uint32, optional, tag = "1")] + pub forwarding_fee_proportional_millionths: ::core::option::Option, /// Amount (in milli-satoshi) charged for payments forwarded outbound over the channel, /// in excess of forwarding_fee_proportional_millionths. /// See more: - #[prost(uint32, tag = "2")] - pub forwarding_fee_base_msat: u32, + #[prost(uint32, optional, tag = "2")] + pub forwarding_fee_base_msat: ::core::option::Option, /// The difference in the CLTV value between incoming HTLCs and an outbound HTLC forwarded /// over the channel this config applies to. /// See more: - #[prost(uint32, tag = "3")] - pub cltv_expiry_delta: u32, + #[prost(uint32, optional, tag = "3")] + pub cltv_expiry_delta: ::core::option::Option, /// The maximum additional fee we’re willing to pay to avoid waiting for the counterparty’s /// to_self_delay to reclaim funds. /// See more: - #[prost(uint64, tag = "4")] - pub force_close_avoidance_max_fee_satoshis: u64, + #[prost(uint64, optional, tag = "4")] + pub force_close_avoidance_max_fee_satoshis: ::core::option::Option, /// If set, allows this channel’s counterparty to skim an additional fee off this node’s /// inbound HTLCs. Useful for liquidity providers to offload on-chain channel costs to end users. /// See more: - #[prost(bool, tag = "5")] - pub accept_underpaying_htlcs: bool, + #[prost(bool, optional, tag = "5")] + pub accept_underpaying_htlcs: ::core::option::Option, /// Limit our total exposure to potential loss to on-chain fees on close, including /// in-flight HTLCs which are burned to fees as they are too small to claim on-chain /// and fees on commitment transaction(s) broadcasted by our counterparty in excess of @@ -284,9 +331,9 @@ pub struct Channel { /// our counterparty already. #[prost(message, optional, tag = "3")] pub funding_txo: ::core::option::Option, - /// The local `user_channel_id` of this channel. - #[prost(bytes = "bytes", tag = "4")] - pub user_channel_id: ::prost::bytes::Bytes, + /// The hex-encoded local `user_channel_id` of this channel. + #[prost(string, tag = "4")] + pub user_channel_id: ::prost::alloc::string::String, /// The value, in satoshis, that must always be held as a reserve in the channel for us. This /// value ensures that if we broadcast a revoked state, our counterparty can punish us by /// claiming at least this value on chain. @@ -383,8 +430,8 @@ pub struct Channel { /// claiming at least this value on chain. /// /// This value is not included in `inbound_capacity_msat` as it can never be spent. - #[prost(uint64, optional, tag = "22")] - pub counterparty_unspendable_punishment_reserve: ::core::option::Option, + #[prost(uint64, tag = "22")] + pub counterparty_unspendable_punishment_reserve: u64, /// Base routing fee in millisatoshis. #[prost(uint32, optional, tag = "23")] pub counterparty_forwarding_info_fee_base_msat: ::core::option::Option, @@ -407,3 +454,13 @@ pub struct OutPoint { #[prost(uint32, tag = "2")] pub vout: u32, } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct BestBlock { + /// The block’s hash + #[prost(string, tag = "1")] + pub block_hash: ::prost::alloc::string::String, + /// The height at which the block was confirmed. + #[prost(uint32, tag = "2")] + pub height: u32, +} diff --git a/protos/src/proto/ldk_node_server.proto b/protos/src/proto/ldk_node_server.proto index e415ba1..588a5bf 100644 --- a/protos/src/proto/ldk_node_server.proto +++ b/protos/src/proto/ldk_node_server.proto @@ -1,6 +1,52 @@ syntax = "proto3"; package ldk_node_server; +// Retrieve the latest node info like `node_id`, `current_best_block` etc. +// See more: +// - https://docs.rs/ldk-node/latest/ldk_node/struct.Node.html#method.node_id +// - https://docs.rs/ldk-node/latest/ldk_node/struct.Node.html#method.status +message GetNodeInfoRequest { +} + +message GetNodeInfoResponse { + + // The hex-encoded `node-id` or public key for our own lightning node. + string node_id = 1; + + // The best block to which our Lightning wallet is currently synced. + // + // Should be always set, will never be `None`. + BestBlock current_best_block = 3; + + // The timestamp, in seconds since start of the UNIX epoch, when we last successfully synced our Lightning wallet + // to the chain tip. + // + // Will be `None` if the wallet hasn’t been synced since the node was initialized. + optional uint64 latest_wallet_sync_timestamp = 4; + + // The timestamp, in seconds since start of the UNIX epoch, when we last successfully synced our on-chain + // wallet to the chain tip. + // + // Will be `None` if the wallet hasn’t been synced since the node was initialized. + optional uint64 latest_onchain_wallet_sync_timestamp = 5; + + // The timestamp, in seconds since start of the UNIX epoch, when we last successfully update our fee rate cache. + // + // Will be `None` if the cache hasn’t been updated since the node was initialized. + optional uint64 latest_fee_rate_cache_update_timestamp = 6; + + // The timestamp, in seconds since start of the UNIX epoch, when the last rapid gossip sync (RGS) snapshot we + // successfully applied was generated. + // + // Will be `None` if RGS isn’t configured or the snapshot hasn’t been updated since the node was initialized. + optional uint64 latest_rgs_snapshot_timestamp = 7; + + // The timestamp, in seconds since start of the UNIX epoch, when we last broadcasted a node announcement. + // + // Will be `None` if we have no public channels or we haven’t broadcasted since the node was initialized. + optional uint64 latest_node_announcement_broadcast_timestamp = 8; +} + // Retrieve a new on-chain funding address. // See more: https://docs.rs/ldk-node/latest/ldk_node/payment/struct.OnchainPayment.html#method.new_address message OnchainReceiveRequest { @@ -174,27 +220,27 @@ message ChannelConfig { // Amount (in millionths of a satoshi) charged per satoshi for payments forwarded outbound // over the channel. // See more: https://docs.rs/lightning/latest/lightning/util/config/struct.ChannelConfig.html#structfield.forwarding_fee_proportional_millionths - uint32 forwarding_fee_proportional_millionths = 1; + optional uint32 forwarding_fee_proportional_millionths = 1; // Amount (in milli-satoshi) charged for payments forwarded outbound over the channel, // in excess of forwarding_fee_proportional_millionths. // See more: https://docs.rs/lightning/latest/lightning/util/config/struct.ChannelConfig.html#structfield.forwarding_fee_base_msat - uint32 forwarding_fee_base_msat = 2; + optional uint32 forwarding_fee_base_msat = 2; // The difference in the CLTV value between incoming HTLCs and an outbound HTLC forwarded // over the channel this config applies to. // See more: https://docs.rs/lightning/latest/lightning/util/config/struct.ChannelConfig.html#structfield.cltv_expiry_delta - uint32 cltv_expiry_delta = 3; + optional uint32 cltv_expiry_delta = 3; // The maximum additional fee we’re willing to pay to avoid waiting for the counterparty’s // to_self_delay to reclaim funds. // See more: https://docs.rs/lightning/latest/lightning/util/config/struct.ChannelConfig.html#structfield.force_close_avoidance_max_fee_satoshis - uint64 force_close_avoidance_max_fee_satoshis = 4; + optional uint64 force_close_avoidance_max_fee_satoshis = 4; // If set, allows this channel’s counterparty to skim an additional fee off this node’s // inbound HTLCs. Useful for liquidity providers to offload on-chain channel costs to end users. // See more: https://docs.rs/lightning/latest/lightning/util/config/struct.ChannelConfig.html#structfield.accept_underpaying_htlcs - bool accept_underpaying_htlcs = 5; + optional bool accept_underpaying_htlcs = 5; // Limit our total exposure to potential loss to on-chain fees on close, including // in-flight HTLCs which are burned to fees as they are too small to claim on-chain @@ -259,8 +305,8 @@ message Channel { // our counterparty already. optional OutPoint funding_txo = 3; - // The local `user_channel_id` of this channel. - bytes user_channel_id = 4; + // The hex-encoded local `user_channel_id` of this channel. + string user_channel_id = 4; // The value, in satoshis, that must always be held as a reserve in the channel for us. This // value ensures that if we broadcast a revoked state, our counterparty can punish us by @@ -358,7 +404,7 @@ message Channel { // claiming at least this value on chain. // // This value is not included in `inbound_capacity_msat` as it can never be spent. - optional uint64 counterparty_unspendable_punishment_reserve = 22; + uint64 counterparty_unspendable_punishment_reserve = 22; // Base routing fee in millisatoshis. optional uint32 counterparty_forwarding_info_fee_base_msat = 23; @@ -378,4 +424,14 @@ message OutPoint { // The index of the referenced output in its transaction's vout. uint32 vout = 2; -} \ No newline at end of file +} + +message BestBlock { + + // The block’s hash + string block_hash = 1; + + // The height at which the block was confirmed. + uint32 height = 2; + +} diff --git a/server/Cargo.toml b/server/Cargo.toml index 285cbe2..ba21003 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -14,3 +14,4 @@ tokio = { version = "1.38.0", default-features = false, features = ["time", "sig prost = { version = "0.11.6", default-features = false, features = ["std"] } protos = { path = "../protos" } bytes = "1.4.0" +hex = { package = "hex-conservative", version = "0.2.1", default-features = false } diff --git a/server/src/api/get_node_info.rs b/server/src/api/get_node_info.rs new file mode 100644 index 0000000..d98c8f1 --- /dev/null +++ b/server/src/api/get_node_info.rs @@ -0,0 +1,28 @@ +use ldk_node::Node; +use protos::{BestBlock, GetNodeInfoRequest, GetNodeInfoResponse}; +use std::sync::Arc; + +pub(crate) const GET_NODE_INFO: &str = "GetNodeInfo"; + +pub(crate) fn handle_get_node_info_request( + node: Arc, _request: GetNodeInfoRequest, +) -> Result { + let node_status = node.status(); + + let best_block = BestBlock { + block_hash: node_status.current_best_block.block_hash.to_string(), + height: node_status.current_best_block.height, + }; + + let response = GetNodeInfoResponse { + node_id: node.node_id().to_string(), + current_best_block: Some(best_block), + latest_wallet_sync_timestamp: node_status.latest_wallet_sync_timestamp, + latest_onchain_wallet_sync_timestamp: node_status.latest_onchain_wallet_sync_timestamp, + latest_fee_rate_cache_update_timestamp: node_status.latest_fee_rate_cache_update_timestamp, + latest_rgs_snapshot_timestamp: node_status.latest_rgs_snapshot_timestamp, + latest_node_announcement_broadcast_timestamp: node_status + .latest_node_announcement_broadcast_timestamp, + }; + Ok(response) +} diff --git a/server/src/api/list_channels.rs b/server/src/api/list_channels.rs new file mode 100644 index 0000000..32688d6 --- /dev/null +++ b/server/src/api/list_channels.rs @@ -0,0 +1,15 @@ +use crate::util::proto_adapter::channel_to_proto; +use ldk_node::Node; +use protos::{ListChannelsRequest, ListChannelsResponse}; +use std::sync::Arc; + +pub(crate) const LIST_CHANNELS_PATH: &str = "ListChannels"; + +pub(crate) fn handle_list_channels_request( + node: Arc, _request: ListChannelsRequest, +) -> Result { + let channels = node.list_channels().into_iter().map(|c| channel_to_proto(c)).collect(); + + let response = ListChannelsResponse { channels }; + Ok(response) +} diff --git a/server/src/api/mod.rs b/server/src/api/mod.rs index 42aa262..6355e9d 100644 --- a/server/src/api/mod.rs +++ b/server/src/api/mod.rs @@ -3,6 +3,8 @@ pub(crate) mod bolt11_send; pub(crate) mod bolt12_receive; pub(crate) mod bolt12_send; pub(crate) mod close_channel; +pub(crate) mod get_node_info; +pub(crate) mod list_channels; pub(crate) mod onchain_receive; pub(crate) mod onchain_send; pub(crate) mod open_channel; diff --git a/server/src/main.rs b/server/src/main.rs index 9c9fd65..0bebbdc 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,5 +1,6 @@ mod api; mod service; +mod util; use crate::service::NodeService; diff --git a/server/src/service.rs b/server/src/service.rs index 5f1f8f7..15eaa79 100644 --- a/server/src/service.rs +++ b/server/src/service.rs @@ -11,22 +11,16 @@ use std::future::Future; use std::pin::Pin; use std::sync::Arc; -use crate::api::bolt11_receive::handle_bolt11_receive_request; -use crate::api::bolt11_receive::BOLT11_RECEIVE_PATH; -use crate::api::bolt11_send::handle_bolt11_send_request; -use crate::api::bolt11_send::BOLT11_SEND_PATH; -use crate::api::bolt12_receive::handle_bolt12_receive_request; -use crate::api::bolt12_receive::BOLT12_RECEIVE_PATH; -use crate::api::bolt12_send::handle_bolt12_send_request; -use crate::api::bolt12_send::BOLT12_SEND_PATH; -use crate::api::close_channel::handle_close_channel_request; -use crate::api::close_channel::CLOSE_CHANNEL_PATH; -use crate::api::onchain_receive::handle_onchain_receive_request; -use crate::api::onchain_receive::ONCHAIN_RECEIVE_PATH; -use crate::api::onchain_send::handle_onchain_send_request; -use crate::api::onchain_send::ONCHAIN_SEND_PATH; -use crate::api::open_channel::handle_open_channel; -use crate::api::open_channel::OPEN_CHANNEL_PATH; +use crate::api::bolt11_receive::{handle_bolt11_receive_request, BOLT11_RECEIVE_PATH}; +use crate::api::bolt11_send::{handle_bolt11_send_request, BOLT11_SEND_PATH}; +use crate::api::bolt12_receive::{handle_bolt12_receive_request, BOLT12_RECEIVE_PATH}; +use crate::api::bolt12_send::{handle_bolt12_send_request, BOLT12_SEND_PATH}; +use crate::api::close_channel::{handle_close_channel_request, CLOSE_CHANNEL_PATH}; +use crate::api::get_node_info::{handle_get_node_info_request, GET_NODE_INFO}; +use crate::api::list_channels::{handle_list_channels_request, LIST_CHANNELS_PATH}; +use crate::api::onchain_receive::{handle_onchain_receive_request, ONCHAIN_RECEIVE_PATH}; +use crate::api::onchain_send::{handle_onchain_send_request, ONCHAIN_SEND_PATH}; +use crate::api::open_channel::{handle_open_channel, OPEN_CHANNEL_PATH}; #[derive(Clone)] pub struct NodeService { @@ -48,6 +42,7 @@ impl Service> for NodeService { let node = Arc::clone(&self.node); // Exclude '/' from path pattern matching. match &req.uri().path()[1..] { + GET_NODE_INFO => Box::pin(handle_request(node, req, handle_get_node_info_request)), ONCHAIN_RECEIVE_PATH => { Box::pin(handle_request(node, req, handle_onchain_receive_request)) }, @@ -62,6 +57,7 @@ impl Service> for NodeService { BOLT12_SEND_PATH => Box::pin(handle_request(node, req, handle_bolt12_send_request)), OPEN_CHANNEL_PATH => Box::pin(handle_request(node, req, handle_open_channel)), CLOSE_CHANNEL_PATH => Box::pin(handle_request(node, req, handle_close_channel_request)), + LIST_CHANNELS_PATH => Box::pin(handle_request(node, req, handle_list_channels_request)), path => { let error = format!("Unknown request: {}", path).into_bytes(); Box::pin(async { diff --git a/server/src/util/mod.rs b/server/src/util/mod.rs new file mode 100644 index 0000000..8f7d1a3 --- /dev/null +++ b/server/src/util/mod.rs @@ -0,0 +1 @@ +pub(crate) mod proto_adapter; diff --git a/server/src/util/proto_adapter.rs b/server/src/util/proto_adapter.rs new file mode 100644 index 0000000..09d5fc9 --- /dev/null +++ b/server/src/util/proto_adapter.rs @@ -0,0 +1,55 @@ +use hex::prelude::*; +use ldk_node::{ChannelConfig, ChannelDetails}; +use protos::{Channel, OutPoint}; +pub(crate) fn channel_to_proto(channel: ChannelDetails) -> Channel { + Channel { + channel_id: channel.channel_id.0.to_lower_hex_string(), + counterparty_node_id: channel.counterparty_node_id.to_string(), + funding_txo: channel + .funding_txo + .map(|o| OutPoint { txid: o.txid.to_string(), vout: o.vout }), + user_channel_id: channel.user_channel_id.0.to_string(), + unspendable_punishment_reserve: channel.unspendable_punishment_reserve, + channel_value_sats: channel.channel_value_sats, + feerate_sat_per_1000_weight: channel.feerate_sat_per_1000_weight, + outbound_capacity_msat: channel.outbound_capacity_msat, + inbound_capacity_msat: channel.inbound_capacity_msat, + confirmations_required: channel.confirmations_required, + confirmations: channel.confirmations, + is_outbound: channel.is_outbound, + is_channel_ready: channel.is_channel_ready, + is_usable: channel.is_usable, + is_public: channel.is_public, + channel_config: Some(channel_config_to_proto(channel.config.as_ref())), + next_outbound_htlc_limit_msat: channel.next_outbound_htlc_limit_msat, + next_outbound_htlc_minimum_msat: channel.next_outbound_htlc_minimum_msat, + force_close_spend_delay: channel.force_close_spend_delay.map(|x| x as u32), + counterparty_outbound_htlc_minimum_msat: channel.counterparty_outbound_htlc_minimum_msat, + counterparty_outbound_htlc_maximum_msat: channel.counterparty_outbound_htlc_maximum_msat, + counterparty_unspendable_punishment_reserve: channel + .counterparty_unspendable_punishment_reserve, + counterparty_forwarding_info_fee_base_msat: channel + .counterparty_forwarding_info_fee_base_msat, + counterparty_forwarding_info_fee_proportional_millionths: channel + .counterparty_forwarding_info_fee_proportional_millionths, + counterparty_forwarding_info_cltv_expiry_delta: channel + .counterparty_forwarding_info_cltv_expiry_delta + .map(|x| x as u32), + } +} + +pub(crate) fn channel_config_to_proto(channel_config: &ChannelConfig) -> protos::ChannelConfig { + protos::ChannelConfig { + forwarding_fee_proportional_millionths: Some( + channel_config.forwarding_fee_proportional_millionths(), + ), + forwarding_fee_base_msat: Some(channel_config.forwarding_fee_base_msat()), + cltv_expiry_delta: Some(channel_config.cltv_expiry_delta() as u32), + force_close_avoidance_max_fee_satoshis: Some( + channel_config.force_close_avoidance_max_fee_satoshis(), + ), + accept_underpaying_htlcs: Some(channel_config.accept_underpaying_htlcs()), + // FIXME: Pending ldk-node upgrade. + max_dust_htlc_exposure: None, + } +}