diff --git a/bindings/una-js/index.d.ts b/bindings/una-js/index.d.ts index fcb1902..634fbea 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,40 @@ 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 { + hop_hints: HopHint[]; +} + +export interface HopHint { + chan_id: string; + 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/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/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..84426de 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..1fa236f 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; include!(concat!(env!("PROTOBUFS_DIR"), "/cln.rs")); @@ -105,3 +108,61 @@ 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()).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) => Ok(Some(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 creation_date")) + })? + .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: parsed_invoice + .route_hints() + .into_iter() + .map(|route_hint| RoutingHint { + hop_hints: route_hint + .0 + .into_iter() + .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(), + }) + .collect(), + }; + + Ok(invoice) + } +} diff --git a/core/src/backends/eclair/rest/node.rs b/core/src/backends/eclair/rest/node.rs index 3782c6b..d8c4b03 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?; + + data.try_into() + } } diff --git a/core/src/backends/eclair/rest/types.rs b/core/src/backends/eclair/rest/types.rs index 57f12ab..303108b 100644 --- a/core/src/backends/eclair/rest/types.rs +++ b/core/src/backends/eclair/rest/types.rs @@ -1,5 +1,7 @@ #![allow(clippy::from_over_into)] +use std::convert::TryInto; + use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -268,3 +270,120 @@ 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(Debug, Deserialize)] +pub struct Activated { + pub var_onion_optin: Option, + pub payment_secret: Option, + pub basic_mpp: Option, + pub option_payment_metadata: Option, +} +#[derive(Debug, Deserialize)] +pub struct Features { + pub activated: Activated, + pub unknown: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DecodeInvoiceRoutingInfo { + 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 { + pub prefix: String, + pub timestamp: i64, + pub node_id: String, + pub serialized: String, + pub description: Option, + pub payment_hash: String, + pub payment_metadata: Option, + pub expiry: i32, + pub min_final_cltv_expiry: Option, + pub amount: u64, + pub features: Features, + pub routing_info: Vec>, +} + +fn extract_feature_status(feature_status_str: Option) -> FeatureActivationStatus { + match feature_status_str { + None => FeatureActivationStatus::Unknown, + Some(f) => match f.as_str() { + "optional" => FeatureActivationStatus::Optional, + "mandatory" => FeatureActivationStatus::Mandatory, + _ => FeatureActivationStatus::Unknown, + }, + } +} + +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), + option_payment_metadata: extract_feature_status( + self.features.activated.option_payment_metadata, + ), + var_onion_optin: extract_feature_status(self.features.activated.var_onion_optin), + }; + + let route_hints: Vec = self + .routing_info + .into_iter() + .map(|route_info| { + let info = RoutingHint { + hop_hints: route_info + .into_iter() + .map(|hop_hint| { + let hop = HopHint { + 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) + }) + .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), + destination: Some(self.node_id), + memo: self.description, + payment_hash: self.payment_hash, + expiry: self.expiry, + min_final_cltv_expiry: 18, + features: Some(invoices_feature), + route_hints, + }; + + Ok(invoice) + } +} diff --git a/core/src/backends/lnd/rest/config.rs b/core/src/backends/lnd/rest/config.rs index 6783baf..e5bffdb 100644 --- a/core/src/backends/lnd/rest/config.rs +++ b/core/src/backends/lnd/rest/config.rs @@ -23,7 +23,6 @@ impl TryFrom for LndRestConfig { let tls_certificate = config .tls_certificate .ok_or_else(|| ConfigError::MissingField("tls_certificate".to_string()))?; - let config = LndRestConfig { url, macaroon, diff --git a/core/src/backends/lnd/rest/node.rs b/core/src/backends/lnd/rest/node.rs index d972bad..8a83bad 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.try_into()?) + } } diff --git a/core/src/backends/lnd/rest/types.rs b/core/src/backends/lnd/rest/types.rs index 7b6724e..559d244 100644 --- a/core/src/backends/lnd/rest/types.rs +++ b/core/src/backends/lnd/rest/types.rs @@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use std::convert::TryInto; use crate::error::Error; use crate::{types::*, utils}; @@ -253,3 +254,95 @@ impl TryInto for SendPaymentSyncResponse { Ok(result) } } + +#[derive(Debug, Deserialize)] +pub struct DecodeInvoiceResponse { + pub destination: String, + pub payment_hash: String, + pub num_satoshis: String, + pub timestamp: String, + pub expiry: String, + pub description: Option, + pub description_hash: String, + pub fallback_addr: String, + pub cltv_expiry: String, + pub payment_addr: String, + pub num_msat: String, + pub features: HashMap, + pub route_hints: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct DecodeInvoiceFeature { + pub name: String, + pub is_required: bool, + pub is_known: bool, +} +#[derive(Debug, Deserialize)] +pub struct DecodeInvoiceRoutingHint { + pub hop_hints: 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 { + None => FeatureActivationStatus::Unknown, + Some(f) => match (f.is_known, f.is_required) { + (true, true) => FeatureActivationStatus::Mandatory, + (true, false) => FeatureActivationStatus::Optional, + _ => FeatureActivationStatus::Unknown, + }, + } +} + +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)), + option_payment_metadata: FeatureActivationStatus::Unknown, + var_onion_optin: extract_feature_status(self.features.get(&9)), + }; + + 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: self.description, + payment_hash: self.payment_hash, + expiry: self.expiry.parse()?, + min_final_cltv_expiry: self.cltv_expiry.parse()?, + features: Some(invoice_features), + route_hints: self + .route_hints + .into_iter() + .map(|route_hint| RoutingHint { + hop_hints: route_hint + .hop_hints + .into_iter() + .map(|hop_hint| HopHint { + node_id: hop_hint.node_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, + }) + .collect(), + }) + .collect(), + }; + + Ok(result) + } +} 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/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; 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..1289eec 100644 --- a/core/src/types.rs +++ b/core/src/types.rs @@ -133,3 +133,47 @@ 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 hop_hints: Vec, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct HopHint { + pub node_id: String, + pub chan_id: String, + 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()); }