From fc7b78e601887120f79ece6d88ab70a734162812 Mon Sep 17 00:00:00 2001 From: SeraBocca Date: Tue, 27 Sep 2022 10:05:36 +0000 Subject: [PATCH 1/7] feat: add decodeInvoice method (#19) --- bindings/una-js/index.d.ts | 31 +++++++++++ bindings/una-js/src/lib.rs | 21 ++++++++ bindings/una-js/test.mjs | 8 +++ bindings/una-python/src/lib.rs | 20 ++++++- bindings/una-python/test_una.py | 15 ++++++ core/Cargo.toml | 1 + core/src/backends/cln/grpc/node.rs | 7 ++- core/src/backends/cln/grpc/pb.rs | 41 +++++++++++++- core/src/backends/eclair/rest/node.rs | 20 +++++-- core/src/backends/eclair/rest/types.rs | 75 +++++++++++++++++++++++++- core/src/backends/lnd/rest/config.rs | 4 +- core/src/backends/lnd/rest/node.rs | 19 +++++-- core/src/backends/lnd/rest/types.rs | 64 ++++++++++++++++++++++ core/src/node.rs | 9 +++- core/src/types.rs | 38 +++++++++++++ schemas/src/main.rs | 5 +- 16 files changed, 362 insertions(+), 16 deletions(-) diff --git a/bindings/una-js/index.d.ts b/bindings/una-js/index.d.ts index fcb1902..0cea36b 100644 --- a/bindings/una-js/index.d.ts +++ b/bindings/una-js/index.d.ts @@ -9,6 +9,7 @@ export class Node { createInvoice(invoice: CreateInvoiceParams): Promise getInfo(): Promise payInvoice(invoice: PayInvoiceParams): Promise + decodeInvoice(invoice: String): Promise } export type Backend = "LndRest" | "LndGrpc" | "ClnGrpc" | "EclairRest" | "InvalidBackend"; @@ -37,6 +38,36 @@ export interface CreateInvoiceResult { payment_request: string; } +export type FeatureActivationStatus = "Mandatory" | "Optional" | "Unknown"; + +export interface DecodeInvoiceResult { + amount?: number | null; + amount_msat?: number | null; + creation_date: number; + destination?: string | null; + expiry: number; + features?: InvoiceFeatures | null; + memo?: string | null; + min_final_cltv_expiry: number; + payment_hash: string; + route_hints: RoutingHint[]; +} + +export interface InvoiceFeatures { + basic_mpp: FeatureActivationStatus; + option_payment_metadata: FeatureActivationStatus; + payment_secret: FeatureActivationStatus; + var_onion_optin: FeatureActivationStatus; +} + +export interface RoutingHint { + chan_id: number; + cltv_expiry_delta: number; + fee_base_msat: number; + fee_proportional_millionths: number; + node_id: string; +} + export type Network = | ("mainnet" | "testnet" | "regtest") | { diff --git a/bindings/una-js/src/lib.rs b/bindings/una-js/src/lib.rs index 4d0ac69..06e2db9 100644 --- a/bindings/una-js/src/lib.rs +++ b/bindings/una-js/src/lib.rs @@ -123,4 +123,25 @@ impl JsNode { |&mut env, payreq| Ok(env.to_js_value(&payreq)), ) } + + #[napi( + ts_args_type = "invoice: String", + ts_return_type = "Promise" + )] + pub fn decode_invoice(&self, env: Env, invoice_str: String) -> Result { + let node = self.0.clone(); + + env.execute_tokio_future( + async move { + let invoice = node + .lock() + .await + .decode_invoice(invoice_str) + .await + .or_napi_error()?; + Ok(invoice) + }, + |&mut env, invoice| Ok(env.to_js_value(&invoice)), + ) + } } diff --git a/bindings/una-js/test.mjs b/bindings/una-js/test.mjs index fbf8166..9c7f944 100644 --- a/bindings/una-js/test.mjs +++ b/bindings/una-js/test.mjs @@ -1,4 +1,5 @@ import { Node } from "./index.js"; +import "ava"; const config = { lnd: { @@ -55,6 +56,13 @@ async function test_node(backend, config) { }) .then((result) => console.log(result)) .catch((err) => console.log(err.message, err.code)); + + node + .decodeInvoice( + "lnbcrt100u1p3n9m8asp5ytfumpvwnehfma235xn50vzffxtrestqzk4qn2up2anwkpucu4nqpp5avpr4r37qpltsjhz543cnnndnz6dqnagepkd35z06lk32ahr7rfsdpz2phkcctjypykuan0d93k2grxdaezqcn0vgxqyjw5qcqp29qyysgqnaa0dtkl4tj3kj8p887pepyjqxhwmwgl5f5xc3mqne6apg2lfg0zj04d79827h5c4ned2m45uc0jl4n63t3l6vvs0pkfdudy2gmmmvqp5qreyk" + ) + .then((res) => console.log(res)) + .catch((err) => console.error(err.message, err.code)); } // test_node("LndRest", config.lnd.rest); diff --git a/bindings/una-python/src/lib.rs b/bindings/una-python/src/lib.rs index ee134f0..5d63a9e 100644 --- a/bindings/una-python/src/lib.rs +++ b/bindings/una-python/src/lib.rs @@ -11,8 +11,8 @@ use una_core::{ }, node::{Node, NodeMethods}, types::{ - Backend, CreateInvoiceParams, CreateInvoiceResult, NodeConfig, NodeInfo, PayInvoiceParams, - PayInvoiceResult, + Backend, CreateInvoiceParams, CreateInvoiceResult, DecodeInvoiceResult, NodeConfig, + NodeInfo, PayInvoiceParams, PayInvoiceResult, }, }; @@ -105,6 +105,22 @@ impl PyNode { Ok(result) }) } + + pub fn decode_invoice<'p>(&self, py: Python<'p>, invoice_str: String) -> PyResult<&'p PyAny> { + let node = self.0.clone(); + + pyo3_asyncio::tokio::future_into_py(py, async move { + let result = node + .lock() + .await + .decode_invoice(invoice_str) + .await + .or_py_error()?; + let result = + Python::with_gil(|py| pythonize::(py, &result).or_py_error())?; + Ok(result) + }) + } } /// A Python module implemented in Rust. diff --git a/bindings/una-python/test_una.py b/bindings/una-python/test_una.py index 6789f3a..69225e6 100644 --- a/bindings/una-python/test_una.py +++ b/bindings/una-python/test_una.py @@ -4,6 +4,7 @@ import pytest pytest_plugins = ('pytest_asyncio',) + @pytest.fixture def lnd_rest(): config_lnd_rest = dict({ @@ -15,6 +16,7 @@ def lnd_rest(): lnd_rest = una.Node("LndRest", config_lnd_rest) yield lnd_rest + @pytest.fixture def cln_grpc(): config_cln_grpc = dict({ @@ -27,11 +29,13 @@ def cln_grpc(): cln_grpc = una.Node("ClnGrpc", config_cln_grpc) yield cln_grpc + @pytest.mark.asyncio async def test_get_info(lnd_rest, cln_grpc): await lnd_rest.get_info() await cln_grpc.get_info() + @pytest.mark.asyncio async def test_create_invoice(lnd_rest, cln_grpc): invoice = dict({ @@ -42,6 +46,7 @@ async def test_create_invoice(lnd_rest, cln_grpc): await lnd_rest.create_invoice(invoice) await cln_grpc.create_invoice(invoice) + @pytest.mark.asyncio async def test_create_invoice_without_amount(lnd_rest, cln_grpc): invoice = dict({ @@ -52,3 +57,13 @@ async def test_create_invoice_without_amount(lnd_rest, cln_grpc): print(res) res = await cln_grpc.create_invoice(invoice) print(res) + + +@pytest.mark.asyncio +async def test_decode_invoice(lnd_rest, cln_grpc): + invoice = "lnbcrt2400u1p3n9jzupp5ydnyhjmsk3lxg93l9myr9qvxvff0hphvf44j59t4y9kmuq43r5hqdq62pshjmt9de6zqar0yp3kzun0dssp56fskehdusx96zn9mepzcxlpcwhyl0e2a2hl5zyqnsx29grp8g9yqmqz9gxqrrsscqp79q2sqqqqqysgq7rwfghezmteyaunm63efau5f2dufmlz4d5j8mkx4yxfa3dhfsnzreaykvc2dh4fqr6zrw40cxffy8r7rz68425f0e5pfv6nqj5s20mgqr94r57" + + res = await lnd_rest.decode_invoice(invoice) + print(res) + res = await cln_grpc.decode_invoice(invoice) + print(res) diff --git a/core/Cargo.toml b/core/Cargo.toml index e97e82b..2edd8b8 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -17,4 +17,5 @@ tonic = { version = "0.8.0", features = ["tls"] } prost = "0.11" cuid = "1.2.0" http = "0.2.8" +lightning-invoice = "0.18.0" regex = "1.6.0" diff --git a/core/src/backends/cln/grpc/node.rs b/core/src/backends/cln/grpc/node.rs index db3ee94..6ac470a 100644 --- a/core/src/backends/cln/grpc/node.rs +++ b/core/src/backends/cln/grpc/node.rs @@ -3,7 +3,8 @@ use tonic::transport::{Certificate, Channel, ClientTlsConfig, Endpoint, Identity use crate::error::Error; use crate::node::NodeMethods; use crate::types::{ - CreateInvoiceParams, CreateInvoiceResult, NodeInfo, PayInvoiceParams, PayInvoiceResult, + CreateInvoiceParams, CreateInvoiceResult, DecodeInvoiceResult, NodeInfo, PayInvoiceParams, + PayInvoiceResult, }; use super::config::ClnGrpcConfig; @@ -76,4 +77,8 @@ impl NodeMethods for ClnGrpc { Ok(response.into()) } + + async fn decode_invoice(&self, _invoice_str: String) -> Result { + Ok(_invoice_str.try_into()?) + } } diff --git a/core/src/backends/cln/grpc/pb.rs b/core/src/backends/cln/grpc/pb.rs index 907803d..43b5c3f 100644 --- a/core/src/backends/cln/grpc/pb.rs +++ b/core/src/backends/cln/grpc/pb.rs @@ -1,7 +1,10 @@ #![allow(clippy::from_over_into)] -use crate::{types::*, utils}; +use std::{convert::TryInto, time::UNIX_EPOCH}; + +use crate::{error::Error, types::*, utils}; use cuid; +use lightning_invoice::InvoiceDescription; include!(concat!(env!("PROTOBUFS_DIR"), "/cln.rs")); @@ -105,3 +108,39 @@ impl Into for PayResponse { } } } + +impl TryInto for String { + type Error = Error; + + fn try_into(self) -> Result { + // DecodeInvoice gRPC command is not implemented yet on CLN, so we need to use an external parser instead + let parsed_invoice = str::parse::(self.as_ref()) + .expect("provided invoice is not a valid bolt11 invoice"); + + let memo = match parsed_invoice.description() { + InvoiceDescription::Direct(direct) => Some(direct.to_string()), + InvoiceDescription::Hash(_hash) => { + unimplemented!("Hash transcription is not supported yet") + } + }; + + let invoice = DecodeInvoiceResult { + creation_date: parsed_invoice + .timestamp() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() as i64, + amount: utils::get_amount_sat(None, parsed_invoice.amount_milli_satoshis()), + amount_msat: parsed_invoice.amount_milli_satoshis(), + destination: None, + memo, + payment_hash: parsed_invoice.payment_hash().to_string(), + expiry: parsed_invoice.expiry_time().as_millis() as i32, + min_final_cltv_expiry: parsed_invoice.min_final_cltv_expiry() as u32, + features: None, + route_hints: Vec::new(), + }; + + Ok(invoice) + } +} diff --git a/core/src/backends/eclair/rest/node.rs b/core/src/backends/eclair/rest/node.rs index 3782c6b..b34bfb2 100644 --- a/core/src/backends/eclair/rest/node.rs +++ b/core/src/backends/eclair/rest/node.rs @@ -1,13 +1,15 @@ use crate::error::Error; use crate::node::NodeMethods; use crate::types::{ - CreateInvoiceParams, CreateInvoiceResult, NodeInfo, PayInvoiceParams, PayInvoiceResult, + CreateInvoiceParams, CreateInvoiceResult, DecodeInvoiceResult, NodeInfo, PayInvoiceParams, + PayInvoiceResult, }; use super::config::EclairRestConfig; use super::types::{ - ApiError, ChannelState, CreateInvoiceRequest, CreateInvoiceResponse, GetChannelsResponse, - GetInfoResponse, PayInvoiceRequest, PayInvoiceResponse, + ApiError, ChannelState, CreateInvoiceRequest, CreateInvoiceResponse, DecodeInvoiceRequest, + DecodeInvoiceResponse, GetChannelsResponse, GetInfoResponse, PayInvoiceRequest, + PayInvoiceResponse, }; pub struct EclairRest { @@ -119,4 +121,16 @@ impl NodeMethods for EclairRest { Ok(data.try_into()?) } + + async fn decode_invoice(&self, invoice: String) -> Result { + let url = format!("{}/parseinvoice", self.config.url); + + let request: DecodeInvoiceRequest = invoice.into(); + let mut response = self.client.post(&url).form(&request).send().await?; + + response = Self::on_response(response).await?; + let data: DecodeInvoiceResponse = response.json().await?; + + Ok(data.into()) + } } diff --git a/core/src/backends/eclair/rest/types.rs b/core/src/backends/eclair/rest/types.rs index 57f12ab..e4b291b 100644 --- a/core/src/backends/eclair/rest/types.rs +++ b/core/src/backends/eclair/rest/types.rs @@ -5,7 +5,6 @@ use serde_json::Value; use crate::error::Error; use crate::{types::*, utils}; - #[derive(Debug, Deserialize)] pub struct ApiError { #[serde(rename = "error")] @@ -268,3 +267,77 @@ impl TryInto for PayInvoiceResponse { Ok(result) } } + +#[derive(Debug, Serialize)] +pub struct DecodeInvoiceRequest { + invoice: String, +} + +impl From for DecodeInvoiceRequest { + fn from(invoice: String) -> DecodeInvoiceRequest { + return DecodeInvoiceRequest { invoice }; + } +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Activated { + pub var_onion_optin: Option, + pub payment_secret: Option, + pub basic_mpp: Option, + pub option_payment_metadata: Option, +} +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Features { + pub activated: Activated, + pub unknown: Vec, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DecodeInvoiceResponse { + pub prefix: String, + pub timestamp: i64, + pub node_id: String, + pub serialized: String, + pub description: String, + pub payment_hash: String, + pub expiry: i32, + pub min_final_cltv_expiry: u32, + pub amount: u64, + pub features: Features, + pub routing_info: Vec, +} + +fn extract_feature_status(feature_status_str: Option) -> FeatureActivationStatus { + match feature_status_str.unwrap_or(String::new()).as_ref() { + "optional" => FeatureActivationStatus::Optional, + "mandatory" => FeatureActivationStatus::Mandatory, + _ => FeatureActivationStatus::Unknown, + } +} + +impl Into for DecodeInvoiceResponse { + fn into(self) -> DecodeInvoiceResult { + let invoices_feature = InvoiceFeatures { + payment_secret: extract_feature_status(self.features.activated.payment_secret), + basic_mpp: extract_feature_status(self.features.activated.basic_mpp), + option_payment_metadata: extract_feature_status( + self.features.activated.option_payment_metadata, + ), + var_onion_optin: extract_feature_status(self.features.activated.var_onion_optin), + }; + + DecodeInvoiceResult { + creation_date: self.timestamp, + amount: utils::get_amount_sat(Some(self.amount), None), + amount_msat: Some(self.amount), + destination: Some(self.node_id), + memo: Some(self.description), + payment_hash: self.payment_hash, + expiry: self.expiry, + min_final_cltv_expiry: self.min_final_cltv_expiry, + features: Some(invoices_feature), + route_hints: Vec::new(), + } + } +} diff --git a/core/src/backends/lnd/rest/config.rs b/core/src/backends/lnd/rest/config.rs index 6783baf..ade4eb8 100644 --- a/core/src/backends/lnd/rest/config.rs +++ b/core/src/backends/lnd/rest/config.rs @@ -23,12 +23,12 @@ impl TryFrom for LndRestConfig { let tls_certificate = config .tls_certificate .ok_or_else(|| ConfigError::MissingField("tls_certificate".to_string()))?; - let config = LndRestConfig { url, macaroon, tls_certificate: hex::decode(&tls_certificate) - .map_err(|_| ConfigError::ParsingHexError("tls_certificate".to_string()))?, + .map_err(|_| ConfigError::ParsingHexError("tls_certificate".to_string())) + .unwrap(), }; Ok(config) diff --git a/core/src/backends/lnd/rest/node.rs b/core/src/backends/lnd/rest/node.rs index d972bad..4003882 100644 --- a/core/src/backends/lnd/rest/node.rs +++ b/core/src/backends/lnd/rest/node.rs @@ -1,13 +1,14 @@ use crate::error::Error; use crate::node::NodeMethods; use crate::types::{ - CreateInvoiceParams, CreateInvoiceResult, NodeInfo, PayInvoiceParams, PayInvoiceResult, + CreateInvoiceParams, CreateInvoiceResult, DecodeInvoiceResult, NodeInfo, PayInvoiceParams, + PayInvoiceResult, }; use super::config::LndRestConfig; use super::types::{ - ApiError, CreateInvoiceRequest, CreateInvoiceResponse, GetInfoResponse, SendPaymentSyncRequest, - SendPaymentSyncResponse, + ApiError, CreateInvoiceRequest, CreateInvoiceResponse, DecodeInvoiceResponse, GetInfoResponse, + SendPaymentSyncRequest, SendPaymentSyncResponse, }; pub struct LndRest { @@ -96,4 +97,16 @@ impl NodeMethods for LndRest { Ok(data.try_into()?) } + + async fn decode_invoice(&self, invoice: String) -> Result { + let url = format!("{}/v1/payreq/{}", self.config.url, invoice); + + let mut response = self.client.get(&url).send().await?; + + response = Self::on_response(response).await?; + + let data: DecodeInvoiceResponse = response.json().await?; + + Ok(data.into()) + } } diff --git a/core/src/backends/lnd/rest/types.rs b/core/src/backends/lnd/rest/types.rs index 7b6724e..eb7d69b 100644 --- a/core/src/backends/lnd/rest/types.rs +++ b/core/src/backends/lnd/rest/types.rs @@ -1,7 +1,9 @@ #![allow(clippy::from_over_into)] use serde::{Deserialize, Serialize}; +use serde_json::Value; use std::collections::HashMap; +use std::convert::TryInto; use crate::error::Error; use crate::{types::*, utils}; @@ -253,3 +255,65 @@ impl TryInto for SendPaymentSyncResponse { Ok(result) } } + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct DecodeInvoiceResponse { + pub destination: String, + pub payment_hash: String, + pub num_satoshis: String, + pub timestamp: String, + pub expiry: String, + pub description: String, + pub description_hash: String, + pub fallback_addr: String, + pub cltv_expiry: String, + pub route_hints: Vec, + pub payment_addr: String, + pub num_msat: String, + pub features: HashMap, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct DecodeInvoiceFeature { + pub name: String, + pub is_required: bool, + pub is_known: bool, +} + +fn extract_feature_status(feature: Option<&DecodeInvoiceFeature>) -> FeatureActivationStatus { + if feature.is_none() { + return FeatureActivationStatus::Unknown; + } + + let _feature = feature.unwrap(); + + match (_feature.is_known, _feature.is_required) { + (true, true) => FeatureActivationStatus::Mandatory, + (true, false) => FeatureActivationStatus::Optional, + _ => FeatureActivationStatus::Unknown, + } +} + +impl Into for DecodeInvoiceResponse { + fn into(self) -> DecodeInvoiceResult { + let invoice_features = InvoiceFeatures { + payment_secret: extract_feature_status(self.features.get(&14)), + basic_mpp: extract_feature_status(self.features.get(&17)), + option_payment_metadata: FeatureActivationStatus::Unknown, + var_onion_optin: extract_feature_status(self.features.get(&9)), + }; + + DecodeInvoiceResult { + creation_date: self.timestamp.parse().unwrap(), + amount: Some(self.num_satoshis.parse().unwrap()), + amount_msat: Some(self.num_msat.parse().unwrap()), + destination: Some(self.destination), + memo: Some(self.description), + payment_hash: self.payment_hash, + expiry: self.expiry.parse().unwrap(), + min_final_cltv_expiry: self.cltv_expiry.parse().unwrap(), + features: Some(invoice_features), + route_hints: Vec::new(), + } + } +} diff --git a/core/src/node.rs b/core/src/node.rs index fc5dab3..f8c6725 100644 --- a/core/src/node.rs +++ b/core/src/node.rs @@ -3,8 +3,8 @@ use crate::backends::eclair::rest::node::EclairRest; use crate::backends::lnd::rest::node::LndRest; use crate::error::Error; use crate::types::{ - Backend, CreateInvoiceParams, CreateInvoiceResult, NodeConfig, NodeInfo, PayInvoiceParams, - PayInvoiceResult, + Backend, CreateInvoiceParams, CreateInvoiceResult, DecodeInvoiceResult, NodeConfig, NodeInfo, + PayInvoiceParams, PayInvoiceResult, }; #[async_trait::async_trait] @@ -15,6 +15,7 @@ pub trait NodeMethods { ) -> Result; async fn get_info(&self) -> Result; async fn pay_invoice(&self, invoice: PayInvoiceParams) -> Result; + async fn decode_invoice(&self, invoice: String) -> Result; } pub struct Node { @@ -67,4 +68,8 @@ impl NodeMethods for Node { async fn pay_invoice(&self, invoice: PayInvoiceParams) -> Result { self.node.pay_invoice(invoice).await } + + async fn decode_invoice(&self, invoice: String) -> Result { + self.node.decode_invoice(invoice).await + } } diff --git a/core/src/types.rs b/core/src/types.rs index 49d0d86..14bb3bb 100644 --- a/core/src/types.rs +++ b/core/src/types.rs @@ -133,3 +133,41 @@ pub struct PayInvoiceResult { pub payment_preimage: String, pub fees_msat: Option, } + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub enum FeatureActivationStatus { + Mandatory, + Optional, + Unknown, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct InvoiceFeatures { + pub payment_secret: FeatureActivationStatus, + pub basic_mpp: FeatureActivationStatus, + pub option_payment_metadata: FeatureActivationStatus, + pub var_onion_optin: FeatureActivationStatus, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct RoutingHint { + pub node_id: String, + pub chan_id: u64, + pub fee_base_msat: u32, + pub fee_proportional_millionths: u32, + pub cltv_expiry_delta: u32, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct DecodeInvoiceResult { + pub creation_date: i64, + pub amount: Option, + pub amount_msat: Option, + pub destination: Option, + pub memo: Option, + pub payment_hash: String, + pub expiry: i32, + pub min_final_cltv_expiry: u32, + pub features: Option, + pub route_hints: Vec, +} diff --git a/schemas/src/main.rs b/schemas/src/main.rs index 2e04039..6b90610 100644 --- a/schemas/src/main.rs +++ b/schemas/src/main.rs @@ -3,7 +3,7 @@ use std::env; use una_core::types::{ Backend, ChannelStats, CreateInvoiceParams, CreateInvoiceResult, Network, NodeConfig, NodeInfo, - PayInvoiceParams, PayInvoiceResult, + PayInvoiceParams, PayInvoiceResult, DecodeInvoiceResult, }; fn write_schema(dir: &std::path::Path, name: &str, schema: &RootSchema) -> std::io::Result<()> { @@ -48,5 +48,8 @@ fn main() { let schema = schema_for!(PayInvoiceResult); write_schema(&dir, "pay_invoice_result", &schema).unwrap(); + let schema = schema_for!(DecodeInvoiceResult); + write_schema(&dir, "decode_invoice_result", &schema).unwrap(); + println!("Wrote schemas to {}", dir.to_string_lossy()); } From abe765115efcbdf1b46a3cf99c90fdd1f56f51ef Mon Sep 17 00:00:00 2001 From: acidbunny21 Date: Sun, 2 Oct 2022 22:01:00 +0200 Subject: [PATCH 2/7] chore: address comments --- core/src/backends/cln/grpc/config.rs | 2 ++ core/src/backends/cln/grpc/node.rs | 4 +-- core/src/backends/cln/grpc/pb.rs | 8 ++--- core/src/backends/eclair/rest/types.rs | 17 ++++++---- core/src/backends/lnd/rest/config.rs | 3 +- core/src/backends/lnd/rest/node.rs | 2 +- core/src/backends/lnd/rest/types.rs | 45 +++++++++++++------------- 7 files changed, 43 insertions(+), 38 deletions(-) diff --git a/core/src/backends/cln/grpc/config.rs b/core/src/backends/cln/grpc/config.rs index 2eb5e7f..269bc6f 100644 --- a/core/src/backends/cln/grpc/config.rs +++ b/core/src/backends/cln/grpc/config.rs @@ -33,6 +33,8 @@ impl TryFrom for ClnGrpcConfig { Uri::from_maybe_shared(url.clone()) .map_err(|_| ConfigError::InvalidField("url".to_string()))?; + dbg!(hex::decode(&tls_certificate)); + let config = ClnGrpcConfig { url, tls_certificate: hex::decode(&tls_certificate) diff --git a/core/src/backends/cln/grpc/node.rs b/core/src/backends/cln/grpc/node.rs index 6ac470a..84426de 100644 --- a/core/src/backends/cln/grpc/node.rs +++ b/core/src/backends/cln/grpc/node.rs @@ -78,7 +78,7 @@ impl NodeMethods for ClnGrpc { Ok(response.into()) } - async fn decode_invoice(&self, _invoice_str: String) -> Result { - Ok(_invoice_str.try_into()?) + async fn decode_invoice(&self, invoice_str: String) -> Result { + Ok(invoice_str.try_into()?) } } diff --git a/core/src/backends/cln/grpc/pb.rs b/core/src/backends/cln/grpc/pb.rs index 43b5c3f..e833570 100644 --- a/core/src/backends/cln/grpc/pb.rs +++ b/core/src/backends/cln/grpc/pb.rs @@ -4,7 +4,7 @@ use std::{convert::TryInto, time::UNIX_EPOCH}; use crate::{error::Error, types::*, utils}; use cuid; -use lightning_invoice::InvoiceDescription; +use lightning_invoice; include!(concat!(env!("PROTOBUFS_DIR"), "/cln.rs")); @@ -118,8 +118,8 @@ impl TryInto for String { .expect("provided invoice is not a valid bolt11 invoice"); let memo = match parsed_invoice.description() { - InvoiceDescription::Direct(direct) => Some(direct.to_string()), - InvoiceDescription::Hash(_hash) => { + lightning_invoice::InvoiceDescription::Direct(direct) => Some(direct.to_string()), + lightning_invoice::InvoiceDescription::Hash(_hash) => { unimplemented!("Hash transcription is not supported yet") } }; @@ -128,7 +128,7 @@ impl TryInto for String { creation_date: parsed_invoice .timestamp() .duration_since(UNIX_EPOCH) - .unwrap() + .map_err(|_| Error::ConversionError(String::from("could not convert error")))? .as_millis() as i64, amount: utils::get_amount_sat(None, parsed_invoice.amount_milli_satoshis()), amount_msat: parsed_invoice.amount_milli_satoshis(), diff --git a/core/src/backends/eclair/rest/types.rs b/core/src/backends/eclair/rest/types.rs index e4b291b..c6b3642 100644 --- a/core/src/backends/eclair/rest/types.rs +++ b/core/src/backends/eclair/rest/types.rs @@ -279,20 +279,20 @@ impl From for DecodeInvoiceRequest { } } -#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Deserialize)] pub struct Activated { pub var_onion_optin: Option, pub payment_secret: Option, pub basic_mpp: Option, pub option_payment_metadata: Option, } -#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Deserialize)] pub struct Features { pub activated: Activated, pub unknown: Vec, } -#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DecodeInvoiceResponse { pub prefix: String, @@ -309,10 +309,13 @@ pub struct DecodeInvoiceResponse { } fn extract_feature_status(feature_status_str: Option) -> FeatureActivationStatus { - match feature_status_str.unwrap_or(String::new()).as_ref() { - "optional" => FeatureActivationStatus::Optional, - "mandatory" => FeatureActivationStatus::Mandatory, - _ => FeatureActivationStatus::Unknown, + match feature_status_str { + None => FeatureActivationStatus::Unknown, + Some(f) => match f.as_str() { + "optional" => FeatureActivationStatus::Optional, + "mandatory" => FeatureActivationStatus::Mandatory, + _ => FeatureActivationStatus::Unknown, + }, } } diff --git a/core/src/backends/lnd/rest/config.rs b/core/src/backends/lnd/rest/config.rs index ade4eb8..e5bffdb 100644 --- a/core/src/backends/lnd/rest/config.rs +++ b/core/src/backends/lnd/rest/config.rs @@ -27,8 +27,7 @@ impl TryFrom for LndRestConfig { url, macaroon, tls_certificate: hex::decode(&tls_certificate) - .map_err(|_| ConfigError::ParsingHexError("tls_certificate".to_string())) - .unwrap(), + .map_err(|_| ConfigError::ParsingHexError("tls_certificate".to_string()))?, }; Ok(config) diff --git a/core/src/backends/lnd/rest/node.rs b/core/src/backends/lnd/rest/node.rs index 4003882..8a83bad 100644 --- a/core/src/backends/lnd/rest/node.rs +++ b/core/src/backends/lnd/rest/node.rs @@ -107,6 +107,6 @@ impl NodeMethods for LndRest { let data: DecodeInvoiceResponse = response.json().await?; - Ok(data.into()) + Ok(data.try_into()?) } } diff --git a/core/src/backends/lnd/rest/types.rs b/core/src/backends/lnd/rest/types.rs index eb7d69b..ffb5226 100644 --- a/core/src/backends/lnd/rest/types.rs +++ b/core/src/backends/lnd/rest/types.rs @@ -256,7 +256,7 @@ impl TryInto for SendPaymentSyncResponse { } } -#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Deserialize)] pub struct DecodeInvoiceResponse { pub destination: String, pub payment_hash: String, @@ -267,13 +267,13 @@ pub struct DecodeInvoiceResponse { pub description_hash: String, pub fallback_addr: String, pub cltv_expiry: String, - pub route_hints: Vec, pub payment_addr: String, pub num_msat: String, pub features: HashMap, + pub route_hints: Vec, } -#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Deserialize)] pub struct DecodeInvoiceFeature { pub name: String, pub is_required: bool, @@ -281,21 +281,20 @@ pub struct DecodeInvoiceFeature { } fn extract_feature_status(feature: Option<&DecodeInvoiceFeature>) -> FeatureActivationStatus { - if feature.is_none() { - return FeatureActivationStatus::Unknown; - } - - let _feature = feature.unwrap(); - - match (_feature.is_known, _feature.is_required) { - (true, true) => FeatureActivationStatus::Mandatory, - (true, false) => FeatureActivationStatus::Optional, - _ => FeatureActivationStatus::Unknown, + match feature { + None => FeatureActivationStatus::Unknown, + Some(f) => match (f.is_known, f.is_required) { + (true, true) => FeatureActivationStatus::Mandatory, + (true, false) => FeatureActivationStatus::Optional, + _ => FeatureActivationStatus::Unknown, + }, } } -impl Into for DecodeInvoiceResponse { - fn into(self) -> DecodeInvoiceResult { +impl TryInto for DecodeInvoiceResponse { + type Error = Error; + + fn try_into(self) -> Result { let invoice_features = InvoiceFeatures { payment_secret: extract_feature_status(self.features.get(&14)), basic_mpp: extract_feature_status(self.features.get(&17)), @@ -303,17 +302,19 @@ impl Into for DecodeInvoiceResponse { var_onion_optin: extract_feature_status(self.features.get(&9)), }; - DecodeInvoiceResult { - creation_date: self.timestamp.parse().unwrap(), - amount: Some(self.num_satoshis.parse().unwrap()), - amount_msat: Some(self.num_msat.parse().unwrap()), + let result = DecodeInvoiceResult { + creation_date: self.timestamp.parse()?, + amount: Some(self.num_satoshis.parse()?), + amount_msat: Some(self.num_msat.parse()?), destination: Some(self.destination), memo: Some(self.description), payment_hash: self.payment_hash, - expiry: self.expiry.parse().unwrap(), - min_final_cltv_expiry: self.cltv_expiry.parse().unwrap(), + expiry: self.expiry.parse()?, + min_final_cltv_expiry: self.cltv_expiry.parse()?, features: Some(invoice_features), route_hints: Vec::new(), - } + }; + + Ok(result) } } From c0102631e990c9f5506663b29ebcd7214392b6db Mon Sep 17 00:00:00 2001 From: acidbunny21 Date: Sun, 2 Oct 2022 22:26:19 +0200 Subject: [PATCH 3/7] feat(lnd-rest, cln-grpc): add route_hints support --- core/src/backends/cln/grpc/pb.rs | 18 ++++++++++++++- core/src/backends/lnd/rest/types.rs | 34 +++++++++++++++++++++++++++-- core/src/types.rs | 6 +++++ 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/core/src/backends/cln/grpc/pb.rs b/core/src/backends/cln/grpc/pb.rs index e833570..bd38a9b 100644 --- a/core/src/backends/cln/grpc/pb.rs +++ b/core/src/backends/cln/grpc/pb.rs @@ -138,7 +138,23 @@ impl TryInto for String { expiry: parsed_invoice.expiry_time().as_millis() as i32, min_final_cltv_expiry: parsed_invoice.min_final_cltv_expiry() as u32, features: None, - route_hints: Vec::new(), + route_hints: parsed_invoice + .route_hints() + .into_iter() + .map(|route_hint| RoutingHint { + hop_ints: route_hint + .0 + .into_iter() + .map(|hop_int| HopHint { + node_id: hop_int.src_node_id.to_string(), + chan_id: hop_int.short_channel_id, + fee_base_msat: hop_int.fees.base_msat, + fee_proportional_millionths: hop_int.fees.proportional_millionths, + cltv_expiry_delta: hop_int.cltv_expiry_delta as u32, + }) + .collect(), + }) + .collect(), }; Ok(invoice) diff --git a/core/src/backends/lnd/rest/types.rs b/core/src/backends/lnd/rest/types.rs index ffb5226..2f9373f 100644 --- a/core/src/backends/lnd/rest/types.rs +++ b/core/src/backends/lnd/rest/types.rs @@ -5,6 +5,7 @@ use serde_json::Value; use std::collections::HashMap; use std::convert::TryInto; +use crate::backends::eclair::rest::types; use crate::error::Error; use crate::{types::*, utils}; @@ -270,7 +271,7 @@ pub struct DecodeInvoiceResponse { pub payment_addr: String, pub num_msat: String, pub features: HashMap, - pub route_hints: Vec, + pub route_hints: Vec, } #[derive(Debug, Deserialize)] @@ -279,6 +280,19 @@ pub struct DecodeInvoiceFeature { pub is_required: bool, pub is_known: bool, } +#[derive(Debug, Deserialize)] +pub struct DecodeInvoiceRoutingHint { + pub hop_ints: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct DecodeInvoiceHopHint { + pub node_id: String, + pub chan_id: u64, + pub fee_base_msat: u32, + pub fee_proportional_millionths: u32, + pub cltv_expiry_delta: u32, +} fn extract_feature_status(feature: Option<&DecodeInvoiceFeature>) -> FeatureActivationStatus { match feature { @@ -312,7 +326,23 @@ impl TryInto for DecodeInvoiceResponse { expiry: self.expiry.parse()?, min_final_cltv_expiry: self.cltv_expiry.parse()?, features: Some(invoice_features), - route_hints: Vec::new(), + route_hints: self + .route_hints + .into_iter() + .map(|route_hint| RoutingHint { + hop_ints: route_hint + .hop_ints + .into_iter() + .map(|hop_hint| HopHint { + node_id: hop_hint.node_id, + chan_id: hop_hint.chan_id, + fee_base_msat: hop_hint.fee_base_msat, + fee_proportional_millionths: hop_hint.fee_proportional_millionths, + cltv_expiry_delta: hop_hint.cltv_expiry_delta, + }) + .collect(), + }) + .collect(), }; Ok(result) diff --git a/core/src/types.rs b/core/src/types.rs index 14bb3bb..dd82310 100644 --- a/core/src/types.rs +++ b/core/src/types.rs @@ -150,7 +150,13 @@ pub struct InvoiceFeatures { } #[derive(Debug, Deserialize, Serialize, JsonSchema)] + pub struct RoutingHint { + pub hop_ints: Vec, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct HopHint { pub node_id: String, pub chan_id: u64, pub fee_base_msat: u32, From 9cc45c3dbb13e51c0891e783271df1c78625f36d Mon Sep 17 00:00:00 2001 From: acidbunny21 Date: Tue, 18 Oct 2022 09:27:49 +0200 Subject: [PATCH 4/7] chore: address comments --- bindings/una-python/src/error.rs | 2 +- core/src/backends/cln/grpc/config.rs | 2 -- core/src/backends/cln/grpc/pb.rs | 24 +++++++++++++++--------- core/src/backends/eclair/rest/types.rs | 5 +++-- core/src/backends/lnd/rest/types.rs | 8 ++++---- core/src/error.rs | 4 ++-- core/src/types.rs | 4 ++-- 7 files changed, 27 insertions(+), 22 deletions(-) diff --git a/bindings/una-python/src/error.rs b/bindings/una-python/src/error.rs index 072a017..1700f2e 100644 --- a/bindings/una-python/src/error.rs +++ b/bindings/una-python/src/error.rs @@ -31,7 +31,7 @@ impl From for PyErr { } } UnaError::Unauthorized => PyPermissionError::new_err(message), - UnaError::NotImplemented => PyNotImplementedError::new_err(message), + UnaError::NotImplemented(message) => PyNotImplementedError::new_err(message), UnaError::ConnectionError(message) => { if message.contains("timeout") { PyTimeoutError::new_err(message) diff --git a/core/src/backends/cln/grpc/config.rs b/core/src/backends/cln/grpc/config.rs index 269bc6f..2eb5e7f 100644 --- a/core/src/backends/cln/grpc/config.rs +++ b/core/src/backends/cln/grpc/config.rs @@ -33,8 +33,6 @@ impl TryFrom for ClnGrpcConfig { Uri::from_maybe_shared(url.clone()) .map_err(|_| ConfigError::InvalidField("url".to_string()))?; - dbg!(hex::decode(&tls_certificate)); - let config = ClnGrpcConfig { url, tls_certificate: hex::decode(&tls_certificate) diff --git a/core/src/backends/cln/grpc/pb.rs b/core/src/backends/cln/grpc/pb.rs index bd38a9b..5349f48 100644 --- a/core/src/backends/cln/grpc/pb.rs +++ b/core/src/backends/cln/grpc/pb.rs @@ -114,21 +114,27 @@ impl TryInto for String { fn try_into(self) -> Result { // DecodeInvoice gRPC command is not implemented yet on CLN, so we need to use an external parser instead - let parsed_invoice = str::parse::(self.as_ref()) - .expect("provided invoice is not a valid bolt11 invoice"); + let parsed_invoice = + str::parse::(self.as_ref()).map_err(|_| { + Error::ConversionError(String::from( + "provided invoice is not a valid bolt11 invoice", + )) + })?; let memo = match parsed_invoice.description() { - lightning_invoice::InvoiceDescription::Direct(direct) => Some(direct.to_string()), - lightning_invoice::InvoiceDescription::Hash(_hash) => { - unimplemented!("Hash transcription is not supported yet") - } - }; + lightning_invoice::InvoiceDescription::Direct(direct) => Ok(direct.to_string()), + lightning_invoice::InvoiceDescription::Hash(_hash) => Err(Error::NotImplemented( + String::from("Hash transcription is not supported yet"), + )), + }?; let invoice = DecodeInvoiceResult { creation_date: parsed_invoice .timestamp() .duration_since(UNIX_EPOCH) - .map_err(|_| Error::ConversionError(String::from("could not convert error")))? + .map_err(|_| { + Error::ConversionError(String::from("could not convert creation_date")) + })? .as_millis() as i64, amount: utils::get_amount_sat(None, parsed_invoice.amount_milli_satoshis()), amount_msat: parsed_invoice.amount_milli_satoshis(), @@ -142,7 +148,7 @@ impl TryInto for String { .route_hints() .into_iter() .map(|route_hint| RoutingHint { - hop_ints: route_hint + hop_hints: route_hint .0 .into_iter() .map(|hop_int| HopHint { diff --git a/core/src/backends/eclair/rest/types.rs b/core/src/backends/eclair/rest/types.rs index c6b3642..a7d4ec4 100644 --- a/core/src/backends/eclair/rest/types.rs +++ b/core/src/backends/eclair/rest/types.rs @@ -301,6 +301,7 @@ pub struct DecodeInvoiceResponse { pub serialized: String, pub description: String, pub payment_hash: String, + pub payment_metadata: String, pub expiry: i32, pub min_final_cltv_expiry: u32, pub amount: u64, @@ -332,10 +333,10 @@ impl Into for DecodeInvoiceResponse { DecodeInvoiceResult { creation_date: self.timestamp, - amount: utils::get_amount_sat(Some(self.amount), None), + amount: Some(utils::msat_to_sat(self.amount)), amount_msat: Some(self.amount), destination: Some(self.node_id), - memo: Some(self.description), + memo: self.description, payment_hash: self.payment_hash, expiry: self.expiry, min_final_cltv_expiry: self.min_final_cltv_expiry, diff --git a/core/src/backends/lnd/rest/types.rs b/core/src/backends/lnd/rest/types.rs index 2f9373f..27b7576 100644 --- a/core/src/backends/lnd/rest/types.rs +++ b/core/src/backends/lnd/rest/types.rs @@ -282,7 +282,7 @@ pub struct DecodeInvoiceFeature { } #[derive(Debug, Deserialize)] pub struct DecodeInvoiceRoutingHint { - pub hop_ints: Vec, + pub hop_hints: Vec, } #[derive(Debug, Deserialize)] @@ -321,7 +321,7 @@ impl TryInto for DecodeInvoiceResponse { amount: Some(self.num_satoshis.parse()?), amount_msat: Some(self.num_msat.parse()?), destination: Some(self.destination), - memo: Some(self.description), + memo: self.description, payment_hash: self.payment_hash, expiry: self.expiry.parse()?, min_final_cltv_expiry: self.cltv_expiry.parse()?, @@ -330,8 +330,8 @@ impl TryInto for DecodeInvoiceResponse { .route_hints .into_iter() .map(|route_hint| RoutingHint { - hop_ints: route_hint - .hop_ints + hop_hints: route_hint + .hop_hints .into_iter() .map(|hop_hint| HopHint { node_id: hop_hint.node_id, diff --git a/core/src/error.rs b/core/src/error.rs index e85b0ac..46a8b5d 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -27,7 +27,7 @@ pub enum Error { MissingBackend, InvalidBackend, Unauthorized, - NotImplemented, + NotImplemented(String), ConfigError(ConfigError), ConnectionError(String), ApiError(String), @@ -41,7 +41,7 @@ impl Display for Error { Error::InvalidBackend => String::from("invalid backend"), Error::MissingBackend => String::from("missing backend"), Error::Unauthorized => String::from("unauthorized credentials"), - Error::NotImplemented => String::from("not implemented"), + Error::NotImplemented(err) => err.clone(), Error::ConfigError(err) => err.to_string(), Error::ConnectionError(err) => err.to_string(), Error::ApiError(err) => err.clone(), diff --git a/core/src/types.rs b/core/src/types.rs index dd82310..42d702d 100644 --- a/core/src/types.rs +++ b/core/src/types.rs @@ -152,7 +152,7 @@ pub struct InvoiceFeatures { #[derive(Debug, Deserialize, Serialize, JsonSchema)] pub struct RoutingHint { - pub hop_ints: Vec, + pub hop_hints: Vec, } #[derive(Debug, Deserialize, Serialize, JsonSchema)] @@ -170,7 +170,7 @@ pub struct DecodeInvoiceResult { pub amount: Option, pub amount_msat: Option, pub destination: Option, - pub memo: Option, + pub memo: String, pub payment_hash: String, pub expiry: i32, pub min_final_cltv_expiry: u32, From 0b3a2d0b09a85619e104ea49e7cb645a6a0378dc Mon Sep 17 00:00:00 2001 From: acidbunny21 Date: Fri, 11 Nov 2022 11:10:48 +0100 Subject: [PATCH 5/7] feat: add support for route_hints in eclair implementation --- core/src/backends/eclair/rest/node.rs | 2 +- core/src/backends/eclair/rest/types.rs | 54 +++++++++++++++++++++++--- core/src/backends/lnd/rest/types.rs | 2 - core/src/lib.rs | 2 + 4 files changed, 51 insertions(+), 9 deletions(-) diff --git a/core/src/backends/eclair/rest/node.rs b/core/src/backends/eclair/rest/node.rs index b34bfb2..d8c4b03 100644 --- a/core/src/backends/eclair/rest/node.rs +++ b/core/src/backends/eclair/rest/node.rs @@ -131,6 +131,6 @@ impl NodeMethods for EclairRest { response = Self::on_response(response).await?; let data: DecodeInvoiceResponse = response.json().await?; - Ok(data.into()) + data.try_into() } } diff --git a/core/src/backends/eclair/rest/types.rs b/core/src/backends/eclair/rest/types.rs index a7d4ec4..3b6a4b1 100644 --- a/core/src/backends/eclair/rest/types.rs +++ b/core/src/backends/eclair/rest/types.rs @@ -1,10 +1,13 @@ #![allow(clippy::from_over_into)] +use std::convert::TryInto; + use serde::{Deserialize, Serialize}; use serde_json::Value; use crate::error::Error; use crate::{types::*, utils}; + #[derive(Debug, Deserialize)] pub struct ApiError { #[serde(rename = "error")] @@ -292,6 +295,16 @@ pub struct Features { pub unknown: Vec, } +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RoutingInfo { + pub node_id: String, + pub short_channel_id: String, + pub fee_base: u32, + pub fee_proportional_millionths: u32, + pub cltv_expiry_delta: u32, +} + #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DecodeInvoiceResponse { @@ -306,7 +319,7 @@ pub struct DecodeInvoiceResponse { pub min_final_cltv_expiry: u32, pub amount: u64, pub features: Features, - pub routing_info: Vec, + pub routing_info: Vec>, } fn extract_feature_status(feature_status_str: Option) -> FeatureActivationStatus { @@ -320,8 +333,10 @@ fn extract_feature_status(feature_status_str: Option) -> FeatureActivati } } -impl Into for DecodeInvoiceResponse { - fn into(self) -> DecodeInvoiceResult { +impl TryInto for DecodeInvoiceResponse { + type Error = Error; + + fn try_into(self) -> Result { let invoices_feature = InvoiceFeatures { payment_secret: extract_feature_status(self.features.activated.payment_secret), basic_mpp: extract_feature_status(self.features.activated.basic_mpp), @@ -331,7 +346,32 @@ impl Into for DecodeInvoiceResponse { var_onion_optin: extract_feature_status(self.features.activated.var_onion_optin), }; - DecodeInvoiceResult { + let route_hints = self + .routing_info + .into_iter() + .map(|route_info: Vec| { + let info = RoutingHint { + hop_hints: route_info + .into_iter() + .map(|hop_int: RoutingInfo| { + let hop = HopHint { + node_id: hop_int.node_id.to_string(), + chan_id: hop_int.short_channel_id.parse()?, + fee_base_msat: hop_int.fee_base, + fee_proportional_millionths: hop_int.fee_proportional_millionths, + cltv_expiry_delta: hop_int.cltv_expiry_delta as u32, + }; + + Ok::(hop) + }) + .try_collect()?, + }; + + Ok::(info) + }) + .try_collect()?; + + let invoice = DecodeInvoiceResult { creation_date: self.timestamp, amount: Some(utils::msat_to_sat(self.amount)), amount_msat: Some(self.amount), @@ -341,7 +381,9 @@ impl Into for DecodeInvoiceResponse { expiry: self.expiry, min_final_cltv_expiry: self.min_final_cltv_expiry, features: Some(invoices_feature), - route_hints: Vec::new(), - } + route_hints, + }; + + Ok(invoice) } } diff --git a/core/src/backends/lnd/rest/types.rs b/core/src/backends/lnd/rest/types.rs index 27b7576..ad86e30 100644 --- a/core/src/backends/lnd/rest/types.rs +++ b/core/src/backends/lnd/rest/types.rs @@ -1,11 +1,9 @@ #![allow(clippy::from_over_into)] use serde::{Deserialize, Serialize}; -use serde_json::Value; use std::collections::HashMap; use std::convert::TryInto; -use crate::backends::eclair::rest::types; use crate::error::Error; use crate::{types::*, utils}; diff --git a/core/src/lib.rs b/core/src/lib.rs index bce2008..03d08ce 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,3 +1,5 @@ +#![feature(iterator_try_collect)] + pub mod backends; pub mod error; pub mod node; From ba10fb1f2aefe0091f782944af62b065eb1cca9a Mon Sep 17 00:00:00 2001 From: acidbunny21 Date: Sun, 13 Nov 2022 17:38:36 +0100 Subject: [PATCH 6/7] fix: fix typo + change memo type --- bindings/una-js/index.d.ts | 6 +++++- core/src/backends/cln/grpc/pb.rs | 4 ++-- core/src/backends/eclair/rest/types.rs | 28 +++++++++++++------------- core/src/backends/lnd/rest/types.rs | 6 +++--- core/src/types.rs | 4 ++-- 5 files changed, 26 insertions(+), 22 deletions(-) diff --git a/bindings/una-js/index.d.ts b/bindings/una-js/index.d.ts index 0cea36b..634fbea 100644 --- a/bindings/una-js/index.d.ts +++ b/bindings/una-js/index.d.ts @@ -61,7 +61,11 @@ export interface InvoiceFeatures { } export interface RoutingHint { - chan_id: number; + hop_hints: HopHint[]; +} + +export interface HopHint { + chan_id: string; cltv_expiry_delta: number; fee_base_msat: number; fee_proportional_millionths: number; diff --git a/core/src/backends/cln/grpc/pb.rs b/core/src/backends/cln/grpc/pb.rs index 5349f48..ff51370 100644 --- a/core/src/backends/cln/grpc/pb.rs +++ b/core/src/backends/cln/grpc/pb.rs @@ -122,7 +122,7 @@ impl TryInto for String { })?; let memo = match parsed_invoice.description() { - lightning_invoice::InvoiceDescription::Direct(direct) => Ok(direct.to_string()), + lightning_invoice::InvoiceDescription::Direct(direct) => Ok(Some(direct.to_string())), lightning_invoice::InvoiceDescription::Hash(_hash) => Err(Error::NotImplemented( String::from("Hash transcription is not supported yet"), )), @@ -153,7 +153,7 @@ impl TryInto for String { .into_iter() .map(|hop_int| HopHint { node_id: hop_int.src_node_id.to_string(), - chan_id: hop_int.short_channel_id, + chan_id: hop_int.short_channel_id.to_string(), fee_base_msat: hop_int.fees.base_msat, fee_proportional_millionths: hop_int.fees.proportional_millionths, cltv_expiry_delta: hop_int.cltv_expiry_delta as u32, diff --git a/core/src/backends/eclair/rest/types.rs b/core/src/backends/eclair/rest/types.rs index 3b6a4b1..303108b 100644 --- a/core/src/backends/eclair/rest/types.rs +++ b/core/src/backends/eclair/rest/types.rs @@ -297,7 +297,7 @@ pub struct Features { #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct RoutingInfo { +pub struct DecodeInvoiceRoutingInfo { pub node_id: String, pub short_channel_id: String, pub fee_base: u32, @@ -312,14 +312,14 @@ pub struct DecodeInvoiceResponse { pub timestamp: i64, pub node_id: String, pub serialized: String, - pub description: String, + pub description: Option, pub payment_hash: String, - pub payment_metadata: String, + pub payment_metadata: Option, pub expiry: i32, - pub min_final_cltv_expiry: u32, + pub min_final_cltv_expiry: Option, pub amount: u64, pub features: Features, - pub routing_info: Vec>, + pub routing_info: Vec>, } fn extract_feature_status(feature_status_str: Option) -> FeatureActivationStatus { @@ -346,20 +346,20 @@ impl TryInto for DecodeInvoiceResponse { var_onion_optin: extract_feature_status(self.features.activated.var_onion_optin), }; - let route_hints = self + let route_hints: Vec = self .routing_info .into_iter() - .map(|route_info: Vec| { + .map(|route_info| { let info = RoutingHint { hop_hints: route_info .into_iter() - .map(|hop_int: RoutingInfo| { + .map(|hop_hint| { let hop = HopHint { - node_id: hop_int.node_id.to_string(), - chan_id: hop_int.short_channel_id.parse()?, - fee_base_msat: hop_int.fee_base, - fee_proportional_millionths: hop_int.fee_proportional_millionths, - cltv_expiry_delta: hop_int.cltv_expiry_delta as u32, + node_id: hop_hint.node_id.to_string(), + chan_id: hop_hint.short_channel_id, + fee_base_msat: hop_hint.fee_base, + fee_proportional_millionths: hop_hint.fee_proportional_millionths, + cltv_expiry_delta: hop_hint.cltv_expiry_delta as u32, }; Ok::(hop) @@ -379,7 +379,7 @@ impl TryInto for DecodeInvoiceResponse { memo: self.description, payment_hash: self.payment_hash, expiry: self.expiry, - min_final_cltv_expiry: self.min_final_cltv_expiry, + min_final_cltv_expiry: 18, features: Some(invoices_feature), route_hints, }; diff --git a/core/src/backends/lnd/rest/types.rs b/core/src/backends/lnd/rest/types.rs index ad86e30..559d244 100644 --- a/core/src/backends/lnd/rest/types.rs +++ b/core/src/backends/lnd/rest/types.rs @@ -262,7 +262,7 @@ pub struct DecodeInvoiceResponse { pub num_satoshis: String, pub timestamp: String, pub expiry: String, - pub description: String, + pub description: Option, pub description_hash: String, pub fallback_addr: String, pub cltv_expiry: String, @@ -280,7 +280,7 @@ pub struct DecodeInvoiceFeature { } #[derive(Debug, Deserialize)] pub struct DecodeInvoiceRoutingHint { - pub hop_hints: Vec, + pub hop_hints: Vec, } #[derive(Debug, Deserialize)] @@ -333,7 +333,7 @@ impl TryInto for DecodeInvoiceResponse { .into_iter() .map(|hop_hint| HopHint { node_id: hop_hint.node_id, - chan_id: hop_hint.chan_id, + chan_id: hop_hint.chan_id.to_string(), fee_base_msat: hop_hint.fee_base_msat, fee_proportional_millionths: hop_hint.fee_proportional_millionths, cltv_expiry_delta: hop_hint.cltv_expiry_delta, diff --git a/core/src/types.rs b/core/src/types.rs index 42d702d..1289eec 100644 --- a/core/src/types.rs +++ b/core/src/types.rs @@ -158,7 +158,7 @@ pub struct RoutingHint { #[derive(Debug, Deserialize, Serialize, JsonSchema)] pub struct HopHint { pub node_id: String, - pub chan_id: u64, + pub chan_id: String, pub fee_base_msat: u32, pub fee_proportional_millionths: u32, pub cltv_expiry_delta: u32, @@ -170,7 +170,7 @@ pub struct DecodeInvoiceResult { pub amount: Option, pub amount_msat: Option, pub destination: Option, - pub memo: String, + pub memo: Option, pub payment_hash: String, pub expiry: i32, pub min_final_cltv_expiry: u32, From b2832a8f1c570324f6babbf2a18d09547060677f Mon Sep 17 00:00:00 2001 From: acidbunny21 Date: Fri, 13 Jan 2023 10:19:17 +0100 Subject: [PATCH 7/7] fix typo in cln grpc --- core/src/backends/cln/grpc/pb.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/src/backends/cln/grpc/pb.rs b/core/src/backends/cln/grpc/pb.rs index ff51370..1fa236f 100644 --- a/core/src/backends/cln/grpc/pb.rs +++ b/core/src/backends/cln/grpc/pb.rs @@ -151,12 +151,12 @@ impl TryInto for String { hop_hints: route_hint .0 .into_iter() - .map(|hop_int| HopHint { - node_id: hop_int.src_node_id.to_string(), - chan_id: hop_int.short_channel_id.to_string(), - fee_base_msat: hop_int.fees.base_msat, - fee_proportional_millionths: hop_int.fees.proportional_millionths, - cltv_expiry_delta: hop_int.cltv_expiry_delta as u32, + .map(|hop_hint| HopHint { + node_id: hop_hint.src_node_id.to_string(), + chan_id: hop_hint.short_channel_id.to_string(), + fee_base_msat: hop_hint.fees.base_msat, + fee_proportional_millionths: hop_hint.fees.proportional_millionths, + cltv_expiry_delta: hop_hint.cltv_expiry_delta as u32, }) .collect(), })