From a97cff1297450942fbfbae93a7b75d3d035aa51a Mon Sep 17 00:00:00 2001 From: Alex Sears Date: Sun, 1 May 2022 16:11:50 -0400 Subject: [PATCH 1/4] Add endpoint for decoding invoices --- src/http/node.rs | 30 ++++++++++++++++++++++++++++++ src/node.rs | 32 +++++++++++++++++++++++++++++++- src/services/node.rs | 9 ++++++++- 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/http/node.rs b/src/http/node.rs index 2e24aea..fdd35f4 100644 --- a/src/http/node.rs +++ b/src/http/node.rs @@ -97,6 +97,19 @@ impl From for NodeRequest { } } +#[derive(Deserialize)] +pub struct DecodeInvoiceParams { + pub invoice: String, +} + +impl From for NodeRequest { + fn from(params: DecodeInvoiceParams) -> Self { + Self::DecodeInvoice { + invoice: params.invoice, + } + } +} + #[derive(Deserialize)] pub struct KeysendParams { pub dest_pubkey: String, @@ -194,6 +207,7 @@ pub fn add_routes(router: Router) -> Router { .route("/v1/node/start", post(start_node)) .route("/v1/node/invoices", post(create_invoice)) .route("/v1/node/invoices/pay", post(pay_invoice)) + .route("/v1/node/invoices/decode", post(decode_invoice)) .route("/v1/node/payments/label", post(label_payment)) .route("/v1/node/payments/delete", post(delete_payment)) .route("/v1/node/channels/open", post(open_channel)) @@ -444,6 +458,22 @@ pub async fn pay_invoice( handle_authenticated_request(request_context, request, macaroon, cookies).await } +pub async fn decode_invoice( + Extension(request_context): Extension>, + Json(payload): Json, + AuthHeader { macaroon, token: _ }: AuthHeader, + cookies: Cookies, +) -> Result, StatusCode> { + let request = { + let params: Result = serde_json::from_value(payload); + match params { + Ok(params) => Ok(params.into()), + Err(_) => Err(StatusCode::UNPROCESSABLE_ENTITY), + } + }?; + handle_authenticated_request(request_context, request, macaroon, cookies).await +} + pub async fn open_channel( Extension(request_context): Extension>, Json(payload): Json, diff --git a/src/node.rs b/src/node.rs index d209f31..61f782f 100644 --- a/src/node.rs +++ b/src/node.rs @@ -55,7 +55,7 @@ use lightning::util::config::{ChannelConfig, ChannelHandshakeLimits, UserConfig} use lightning::util::ser::ReadableArgs; use lightning_background_processor::BackgroundProcessor; use lightning_invoice::utils::DefaultRouter; -use lightning_invoice::{payment, utils, Currency, Invoice}; +use lightning_invoice::{payment, utils, Currency, Invoice, InvoiceDescription}; use lightning_net_tokio::SocketDescriptor; use macaroon::Macaroon; use rand::{thread_rng, Rng}; @@ -73,6 +73,30 @@ use std::{fmt, fs}; use tokio::runtime::Handle; use tokio::task::JoinHandle; +#[derive(Serialize, Debug)] +pub struct LocalInvoice { + hash: String, + currency: String, + amount: u64, + description: String, + expiry: u64, +} + +impl std::convert::From for LocalInvoice { + fn from(invoice: Invoice) -> Self { + Self { + hash: invoice.payment_hash().to_string(), + currency: invoice.currency().to_string(), + amount: invoice.amount_milli_satoshis().unwrap_or_default(), + description: match invoice.description() { + InvoiceDescription::Direct(description) => description.into_inner(), + _ => String::from(""), + }, + expiry: invoice.expiry_time().as_secs(), + } + } +} + #[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] pub enum HTLCStatus { Pending, @@ -1327,6 +1351,12 @@ impl LightningNode { self.send_payment(&invoice).await?; Ok(NodeResponse::SendPayment {}) } + NodeRequest::DecodeInvoice { invoice } => { + let invoice = self.get_invoice_from_str(&invoice)?; + Ok(NodeResponse::DecodeInvoice { + invoice: invoice.into(), + }) + } NodeRequest::Keysend { dest_pubkey, amt_msat, diff --git a/src/services/node.rs b/src/services/node.rs index 83d9788..c8b519f 100644 --- a/src/services/node.rs +++ b/src/services/node.rs @@ -6,7 +6,8 @@ // , at your option. // You may not use this file except in accordance with one or both of these // licenses. -use crate::node::LightningNode; + +use crate::node::{LightningNode, LocalInvoice}; use bdk::TransactionDetails; use futures::Future; use std::pin::Pin; @@ -133,6 +134,9 @@ pub enum NodeRequest { dest_pubkey: String, amt_msat: u64, }, + DecodeInvoice { + invoice: String, + }, GetInvoice { amt_msat: u64, description: String, @@ -185,6 +189,9 @@ pub enum NodeResponse { }, OpenChannel {}, SendPayment {}, + DecodeInvoice { + invoice: LocalInvoice, + }, Keysend {}, GetInvoice { invoice: String, From dc97f317eea503d8a92a488040b1bd7e18b6b3cc Mon Sep 17 00:00:00 2001 From: Alex Sears Date: Mon, 2 May 2022 13:12:12 -0400 Subject: [PATCH 2/4] Add more attributes to returned invoice from decode endpoint --- src/node.rs | 120 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 113 insertions(+), 7 deletions(-) diff --git a/src/node.rs b/src/node.rs index 61f782f..3b20d44 100644 --- a/src/node.rs +++ b/src/node.rs @@ -29,6 +29,7 @@ use bitcoin::hashes::Hash; use entity::sea_orm::{ActiveModelTrait, ActiveValue}; use lightning::chain::channelmonitor::ChannelMonitor; +use lightning::ln::features::InvoiceFeatures; use lightning::ln::msgs::NetAddress; use lightning_invoice::payment::PaymentError; use tindercrypt::cryptors::RingCryptor; @@ -49,7 +50,8 @@ use lightning::ln::peer_handler::{ IgnoringMessageHandler, MessageHandler, PeerManager as LdkPeerManager, }; use lightning::ln::{PaymentHash, PaymentPreimage, PaymentSecret}; -use lightning::routing::network_graph::{NetGraphMsgHandler, NetworkGraph, NodeId}; +use lightning::routing::network_graph::{NetGraphMsgHandler, NetworkGraph, NodeId, RoutingFees}; +use lightning::routing::router::{RouteHint, RouteHintHop}; use lightning::routing::scoring::{ProbabilisticScorer, ProbabilisticScorerUsingTime}; use lightning::util::config::{ChannelConfig, ChannelHandshakeLimits, UserConfig}; use lightning::util::ser::ReadableArgs; @@ -59,7 +61,7 @@ use lightning_invoice::{payment, utils, Currency, Invoice, InvoiceDescription}; use lightning_net_tokio::SocketDescriptor; use macaroon::Macaroon; use rand::{thread_rng, Rng}; -use serde::{Deserialize, Serialize}; +use serde::{ser::SerializeSeq, Deserialize, Serialize, Serializer}; use std::fmt::Display; use std::fs::File; use std::io::Cursor; @@ -69,30 +71,134 @@ use std::str::FromStr; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant, SystemTime}; -use std::{fmt, fs}; +use std::{convert::From, fmt, fs}; use tokio::runtime::Handle; use tokio::task::JoinHandle; #[derive(Serialize, Debug)] pub struct LocalInvoice { - hash: String, + payment_hash: String, currency: String, amount: u64, description: String, expiry: u64, + timestamp: u64, + min_final_cltv_expiry: u64, + #[serde(serialize_with = "serialize_route_hints")] + route_hints: Vec, + features: Option, + payee_pub_key: PublicKey, } -impl std::convert::From for LocalInvoice { +fn serialize_route_hints(vector: &Vec, serializer: S) -> Result +where + S: Serializer, +{ + let mut seq = serializer.serialize_seq(Some(vector.len()))?; + for item in vector { + let local_hint: LocalRouteHint = item.into(); + seq.serialize_element(&local_hint)?; + } + seq.end() +} + +impl From for LocalInvoice { fn from(invoice: Invoice) -> Self { Self { - hash: invoice.payment_hash().to_string(), + payment_hash: invoice.payment_hash().to_string(), currency: invoice.currency().to_string(), amount: invoice.amount_milli_satoshis().unwrap_or_default(), description: match invoice.description() { - InvoiceDescription::Direct(description) => description.into_inner(), + InvoiceDescription::Direct(description) => description.clone().into_inner(), _ => String::from(""), }, expiry: invoice.expiry_time().as_secs(), + timestamp: invoice + .timestamp() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(), + min_final_cltv_expiry: invoice.min_final_cltv_expiry(), + route_hints: invoice.route_hints().clone(), + features: invoice.features().map(|f| f.into()), + payee_pub_key: invoice.recover_payee_pub_key(), + } + } +} + +#[derive(Serialize, Debug)] +pub struct LocalRouteHint { + #[serde(serialize_with = "serialize_route_hint_hops")] + pub hops: Vec, +} + +fn serialize_route_hint_hops( + vector: &Vec, + serializer: S, +) -> Result +where + S: Serializer, +{ + let mut seq = serializer.serialize_seq(Some(vector.len()))?; + for item in vector { + let local_hint: LocalRouteHintHop = item.into(); + seq.serialize_element(&local_hint)?; + } + seq.end() +} + +impl From<&RouteHint> for LocalRouteHint { + fn from(hint: &RouteHint) -> Self { + Self { + hops: hint.0.clone(), + } + } +} + +#[derive(Serialize, Debug)] +pub struct LocalRouteHintHop { + pub src_node_id: PublicKey, + pub short_channel_id: u64, + #[serde(with = "LocalRoutingFees")] + pub fees: RoutingFees, + pub cltv_expiry_delta: u16, + pub htlc_minimum_msat: Option, + pub htlc_maximum_msat: Option, +} + +impl From<&RouteHintHop> for LocalRouteHintHop { + fn from(hop: &RouteHintHop) -> Self { + Self { + src_node_id: hop.src_node_id, + short_channel_id: hop.short_channel_id, + fees: hop.fees, + cltv_expiry_delta: hop.cltv_expiry_delta, + htlc_minimum_msat: hop.htlc_minimum_msat, + htlc_maximum_msat: hop.htlc_maximum_msat, + } + } +} + +#[derive(Serialize, Debug)] +#[serde(remote = "RoutingFees")] +pub struct LocalRoutingFees { + pub base_msat: u32, + pub proportional_millionths: u32, +} + +#[derive(Serialize, Debug)] +pub struct LocalInvoiceFeatures { + pub variable_length_onion: bool, + pub payment_secret: bool, + pub basic_mpp: bool, +} + +impl From<&InvoiceFeatures> for LocalInvoiceFeatures { + fn from(features: &InvoiceFeatures) -> Self { + Self { + variable_length_onion: features.supports_variable_length_onion(), + payment_secret: features.supports_payment_secret(), + basic_mpp: features.supports_basic_mpp(), } } } From b29c21579541553f6a50533953577107b788749b Mon Sep 17 00:00:00 2001 From: Alex Sears Date: Mon, 2 May 2022 13:15:18 -0400 Subject: [PATCH 3/4] Remove unneeded clone --- src/node.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node.rs b/src/node.rs index 3b20d44..46f1140 100644 --- a/src/node.rs +++ b/src/node.rs @@ -119,7 +119,7 @@ impl From for LocalInvoice { .unwrap() .as_secs(), min_final_cltv_expiry: invoice.min_final_cltv_expiry(), - route_hints: invoice.route_hints().clone(), + route_hints: invoice.route_hints(), features: invoice.features().map(|f| f.into()), payee_pub_key: invoice.recover_payee_pub_key(), } From f957aaa38379cc987566bb040ccb02b93d276c67 Mon Sep 17 00:00:00 2001 From: Alex Sears Date: Thu, 5 May 2022 09:49:03 -0400 Subject: [PATCH 4/4] Add endpoint for decoding invoices via gRPC --- proto/sensei.proto | 42 ++++++++++++++++++++++++++- src/grpc/adaptor.rs | 34 ++++++++++++++++++---- src/grpc/mod.rs | 69 +++++++++++++++++++++++++++++++++++++++++++++ src/grpc/node.rs | 27 ++++++++++++------ src/node.rs | 29 ++++++++++++------- 5 files changed, 176 insertions(+), 25 deletions(-) diff --git a/proto/sensei.proto b/proto/sensei.proto index 58d84f6..c732a65 100644 --- a/proto/sensei.proto +++ b/proto/sensei.proto @@ -22,6 +22,7 @@ service Node { rpc GetBalance (GetBalanceRequest) returns (GetBalanceResponse); rpc OpenChannel (OpenChannelRequest) returns (OpenChannelResponse); rpc PayInvoice (PayInvoiceRequest) returns (PayInvoiceResponse); + rpc DecodeInvoice (DecodeInvoiceRequest) returns (DecodeInvoiceResponse); rpc Keysend (KeysendRequest) returns (KeysendResponse); rpc CreateInvoice (CreateInvoiceRequest) returns (CreateInvoiceResponse); rpc LabelPayment (LabelPaymentRequest) returns (LabelPaymentResponse); @@ -197,6 +198,45 @@ message PayInvoiceRequest { } message PayInvoiceResponse {} +message DecodeInvoiceRequest { + string invoice = 1; +} +message DecodeInvoiceResponse { + Invoice invoice = 1; +} +message Invoice { + string payment_hash = 1; + string currency = 2; + uint64 amount = 3; + string description = 4; + uint64 expiry = 5; + uint64 timestamp = 6; + uint64 min_final_cltv_expiry = 7; + repeated RouteHint route_hints = 8; + Features features = 9; + string payee_pub_key = 10; +} +message RouteHint { + repeated RouteHintHop hops = 1; +} +message RouteHintHop { + string src_node_id = 1; + uint64 short_channel_id = 2; + RoutingFees fees = 3; + uint32 cltv_expiry_delta = 4; + optional uint64 htlc_minimum_msat = 5; + optional uint64 htlc_maximum_msat = 6; +} +message RoutingFees { + uint32 base_msat = 1; + uint32 proportional_millionths = 2; +} +message Features { + bool variable_length_onion = 1; + bool payment_secret = 2; + bool basic_mpp = 3; +} + message LabelPaymentRequest { string label = 1; string payment_hash = 2; @@ -325,4 +365,4 @@ message VerifyMessageRequest { message VerifyMessageResponse { bool valid = 1; string pubkey = 2; -} \ No newline at end of file +} diff --git a/src/grpc/adaptor.rs b/src/grpc/adaptor.rs index 2ff6e1c..63b26ab 100644 --- a/src/grpc/adaptor.rs +++ b/src/grpc/adaptor.rs @@ -16,12 +16,13 @@ use super::sensei::{ use super::sensei::{ CloseChannelRequest, CloseChannelResponse, ConnectPeerRequest, ConnectPeerResponse, - CreateInvoiceRequest, CreateInvoiceResponse, GetBalanceRequest, GetBalanceResponse, - GetUnusedAddressRequest, GetUnusedAddressResponse, InfoRequest, InfoResponse, KeysendRequest, - KeysendResponse, ListChannelsRequest, ListChannelsResponse, ListPaymentsRequest, - ListPaymentsResponse, ListPeersRequest, ListPeersResponse, OpenChannelRequest, - OpenChannelResponse, PayInvoiceRequest, PayInvoiceResponse, SignMessageRequest, - SignMessageResponse, VerifyMessageRequest, VerifyMessageResponse, + CreateInvoiceRequest, CreateInvoiceResponse, DecodeInvoiceRequest, DecodeInvoiceResponse, + GetBalanceRequest, GetBalanceResponse, GetUnusedAddressRequest, GetUnusedAddressResponse, + InfoRequest, InfoResponse, KeysendRequest, KeysendResponse, ListChannelsRequest, + ListChannelsResponse, ListPaymentsRequest, ListPaymentsResponse, ListPeersRequest, + ListPeersResponse, OpenChannelRequest, OpenChannelResponse, PayInvoiceRequest, + PayInvoiceResponse, SignMessageRequest, SignMessageResponse, VerifyMessageRequest, + VerifyMessageResponse, }; use crate::services::{ @@ -234,6 +235,27 @@ impl TryFrom for PayInvoiceResponse { } } +impl From for NodeRequest { + fn from(req: DecodeInvoiceRequest) -> Self { + NodeRequest::DecodeInvoice { + invoice: req.invoice, + } + } +} + +impl TryFrom for DecodeInvoiceResponse { + type Error = String; + + fn try_from(res: NodeResponse) -> Result { + match res { + NodeResponse::DecodeInvoice { invoice } => Ok(Self { + invoice: Some(invoice.into()), + }), + _ => Err("impossible".to_string()), + } + } +} + impl From for NodeRequest { fn from(req: KeysendRequest) -> Self { NodeRequest::Keysend { diff --git a/src/grpc/mod.rs b/src/grpc/mod.rs index e69984d..9bf6b0f 100644 --- a/src/grpc/mod.rs +++ b/src/grpc/mod.rs @@ -13,5 +13,74 @@ pub mod node; pub mod utils; pub mod sensei { + use crate::node::{ + LocalInvoice, LocalInvoiceFeatures, LocalRouteHint, LocalRouteHintHop, LocalRoutingFees, + }; + tonic::include_proto!("sensei"); + + impl From for Invoice { + fn from(invoice: LocalInvoice) -> Self { + Invoice { + payment_hash: invoice.payment_hash, + currency: invoice.currency, + amount: invoice.amount, + description: invoice.description, + expiry: invoice.expiry, + timestamp: invoice.timestamp, + min_final_cltv_expiry: invoice.min_final_cltv_expiry, + route_hints: invoice + .route_hints + .into_iter() + .map(|h| LocalRouteHint::from(&h).into()) + .collect(), + features: invoice.features.map(|f| f.into()), + payee_pub_key: invoice.payee_pub_key.to_string(), + } + } + } + + impl From for RouteHint { + fn from(hint: LocalRouteHint) -> Self { + Self { + hops: hint + .hops + .into_iter() + .map(|h| LocalRouteHintHop::from(&h).into()) + .collect(), + } + } + } + + impl From for RouteHintHop { + fn from(hop: LocalRouteHintHop) -> Self { + Self { + src_node_id: hop.src_node_id.to_string(), + short_channel_id: hop.short_channel_id, + fees: Some(LocalRoutingFees::from(hop.fees).into()), + cltv_expiry_delta: hop.cltv_expiry_delta.into(), + htlc_minimum_msat: hop.htlc_minimum_msat, + htlc_maximum_msat: hop.htlc_maximum_msat, + } + } + } + + impl From for RoutingFees { + fn from(fees: LocalRoutingFees) -> Self { + Self { + base_msat: fees.base_msat, + proportional_millionths: fees.proportional_millionths, + } + } + } + + impl From for Features { + fn from(features: LocalInvoiceFeatures) -> Self { + Self { + variable_length_onion: features.variable_length_onion, + payment_secret: features.payment_secret, + basic_mpp: features.basic_mpp, + } + } + } } diff --git a/src/grpc/node.rs b/src/grpc/node.rs index 63184ee..d872b13 100644 --- a/src/grpc/node.rs +++ b/src/grpc/node.rs @@ -14,14 +14,15 @@ pub use super::sensei::node_server::{Node, NodeServer}; use super::{ sensei::{ CloseChannelRequest, CloseChannelResponse, ConnectPeerRequest, ConnectPeerResponse, - CreateInvoiceRequest, CreateInvoiceResponse, DeletePaymentRequest, DeletePaymentResponse, - GetBalanceRequest, GetBalanceResponse, GetUnusedAddressRequest, GetUnusedAddressResponse, - InfoRequest, InfoResponse, KeysendRequest, KeysendResponse, LabelPaymentRequest, - LabelPaymentResponse, ListChannelsRequest, ListChannelsResponse, ListPaymentsRequest, - ListPaymentsResponse, ListPeersRequest, ListPeersResponse, OpenChannelRequest, - OpenChannelResponse, PayInvoiceRequest, PayInvoiceResponse, SignMessageRequest, - SignMessageResponse, StartNodeRequest, StartNodeResponse, StopNodeRequest, - StopNodeResponse, VerifyMessageRequest, VerifyMessageResponse, + CreateInvoiceRequest, CreateInvoiceResponse, DecodeInvoiceRequest, DecodeInvoiceResponse, + DeletePaymentRequest, DeletePaymentResponse, GetBalanceRequest, GetBalanceResponse, + GetUnusedAddressRequest, GetUnusedAddressResponse, InfoRequest, InfoResponse, + KeysendRequest, KeysendResponse, LabelPaymentRequest, LabelPaymentResponse, + ListChannelsRequest, ListChannelsResponse, ListPaymentsRequest, ListPaymentsResponse, + ListPeersRequest, ListPeersResponse, OpenChannelRequest, OpenChannelResponse, + PayInvoiceRequest, PayInvoiceResponse, SignMessageRequest, SignMessageResponse, + StartNodeRequest, StartNodeResponse, StopNodeRequest, StopNodeResponse, + VerifyMessageRequest, VerifyMessageResponse, }, utils::raw_macaroon_from_metadata, }; @@ -170,6 +171,16 @@ impl Node for NodeService { .map(Response::new) .map_err(|_e| Status::unknown("unknown error")) } + async fn decode_invoice( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + self.authenticated_request(request.metadata().clone(), request.into_inner().into()) + .await? + .try_into() + .map(Response::new) + .map_err(|_e| Status::unknown("unknown error")) + } async fn keysend( &self, request: tonic::Request, diff --git a/src/node.rs b/src/node.rs index 46f1140..d027145 100644 --- a/src/node.rs +++ b/src/node.rs @@ -77,17 +77,17 @@ use tokio::task::JoinHandle; #[derive(Serialize, Debug)] pub struct LocalInvoice { - payment_hash: String, - currency: String, - amount: u64, - description: String, - expiry: u64, - timestamp: u64, - min_final_cltv_expiry: u64, + pub payment_hash: String, + pub currency: String, + pub amount: u64, + pub description: String, + pub expiry: u64, + pub timestamp: u64, + pub min_final_cltv_expiry: u64, #[serde(serialize_with = "serialize_route_hints")] - route_hints: Vec, - features: Option, - payee_pub_key: PublicKey, + pub route_hints: Vec, + pub features: Option, + pub payee_pub_key: PublicKey, } fn serialize_route_hints(vector: &Vec, serializer: S) -> Result @@ -186,6 +186,15 @@ pub struct LocalRoutingFees { pub proportional_millionths: u32, } +impl From for LocalRoutingFees { + fn from(fees: RoutingFees) -> Self { + Self { + base_msat: fees.base_msat, + proportional_millionths: fees.proportional_millionths, + } + } +} + #[derive(Serialize, Debug)] pub struct LocalInvoiceFeatures { pub variable_length_onion: bool,