diff --git a/libs/sdk-core/src/greenlight/error.rs b/libs/sdk-core/src/greenlight/error.rs index 9a1764a59..ecc0e93e6 100644 --- a/libs/sdk-core/src/greenlight/error.rs +++ b/libs/sdk-core/src/greenlight/error.rs @@ -186,7 +186,11 @@ impl From for NodeError { JsonRpcErrCode::InvoiceNoDescription => Self::InvoiceNoDescription(status.into()), JsonRpcErrCode::InvoicePreimageAlreadyExists => { Self::InvoicePreimageAlreadyExists(status.into()) - } + }, + JsonRpcErrCode::OfferExpired => Self::OfferExpired(status.into()), + JsonRpcErrCode::OfferBadInvreqReply => Self::OfferReplyError(status.into()), + JsonRpcErrCode::OfferRouteNotFound => Self::RouteNotFound(status.into()), + JsonRpcErrCode::OfferTimeout => Self::OfferTimeout(status.into()), _ => Self::Generic(status.into()), }, _ => Self::Generic(status.into()), diff --git a/libs/sdk-core/src/greenlight/node_api.rs b/libs/sdk-core/src/greenlight/node_api.rs index a27734fc0..3bcb53f83 100644 --- a/libs/sdk-core/src/greenlight/node_api.rs +++ b/libs/sdk-core/src/greenlight/node_api.rs @@ -38,7 +38,7 @@ use strum_macros::{Display, EnumString}; use tokio::sync::{mpsc, Mutex}; use tokio::time::sleep; use tokio_stream::{Stream, StreamExt}; -use tonic::{Code, Streaming}; +use tonic::Streaming; use crate::invoice::{parse_invoice, InvoiceError, RouteHintHop}; use crate::models::*; @@ -1313,43 +1313,38 @@ impl NodeAPI for Greenlight { #[allow(unused_variables)] async fn fetch_invoice( &self, - offer: String, - amount_msat: Option, - quantity: Option, - recurrence_counter: Option, - recurrence_start: Option, - recurrence_label: Option, - timeout: Option, - payer_note: Option, + req: FetchInvoiceRequest ) -> NodeResult { - // Get the required pubkeys - let mut client = self.get_node_client().await?; - // Parse the offer locally, to avoid any unnecessary calls to the recipient - if let Err(parse_error) = offer.parse::() { - return Err(NodeError::Generic(anyhow!("Invalid offer"))); + if let Err(parse_error) = req.offer.parse::() { + return Err(NodeError::InvalidOffer(parse_error)); } + let mut client = self.get_node_client().await?; let response = client - .fetch_invoice(cln::FetchinvoiceRequest { - offer, - amount_msat: amount_msat.map(|msat| cln::Amount { msat }), - quantity, - recurrence_counter, - recurrence_start, - recurrence_label, - timeout, - payer_note, - }) - .await; + .fetch_invoice(Into::::into(req)) + .await? + .into_inner(); - response - .map(|grpc_response| grpc_response.into()) - .map_err(|status| match status.code() { - Code::NotFound => NodeError::RouteNotFound(anyhow!(status.message().to_string())), - Code::Unimplemented => NodeError::NotSupported(), - _ => NodeError::Generic(anyhow!("Could not fetch invoice")), - }) + Ok( + FetchInvoiceResponse { + invoice: response.invoice, + changes: response.changes.map(|changes| FetchInvoiceChanges { + description: changes.description, + description_appended: changes.description_appended, + vendor: changes.vendor, + vendor_removed: changes.vendor_removed, + amount_msat: changes.amount_msat.map(|amount| amount.msat), + }), + next_period: response.next_period.map(|np| FetchInvoiceNextPeriod { + counter: np.counter, + start_time: np.starttime, + end_time: np.endtime, + paywindow_start: np.paywindow_start, + paywindow_end: np.paywindow_end, + }), + } + ) } } diff --git a/libs/sdk-core/src/input_parser.rs b/libs/sdk-core/src/input_parser.rs index 57d5f89cf..071d4e233 100644 --- a/libs/sdk-core/src/input_parser.rs +++ b/libs/sdk-core/src/input_parser.rs @@ -4,7 +4,6 @@ use anyhow::{anyhow, Result}; use bip21::Uri; use bitcoin::bech32; use bitcoin::bech32::FromBase32; -use lightning::offers::offer::Amount; use lightning::offers::offer::Offer; use serde::Deserialize; use serde::Serialize; @@ -179,15 +178,9 @@ pub async fn parse(input: &str) -> Result { return Ok(Bolt12Offer { offer: LNOffer { chains: offer.chains(), - amount_msats: offer.amount().map(|amount| { - match amount { - Amount::Currency { amount, .. } => amount, - Amount::Bitcoin { amount_msats } => amount_msats, - } - .clone() - }), + amount: offer.amount().map(|amount| amount.clone().into()), description: offer.description().to_string(), - absolute_expiry: offer.absolute_expiry(), + absolute_expiry: offer.absolute_expiry().map(|expiry| expiry.as_secs()), issuer: offer.issuer().map(|s| s.to_string()), supported_quantity: offer.supported_quantity().into(), signing_pubkey: offer.signing_pubkey(), diff --git a/libs/sdk-core/src/invoice.rs b/libs/sdk-core/src/invoice.rs index d8e4a9f37..e4a5120b5 100644 --- a/libs/sdk-core/src/invoice.rs +++ b/libs/sdk-core/src/invoice.rs @@ -9,7 +9,7 @@ use regex::Regex; use serde::{Deserialize, Serialize}; use std::num::NonZeroU64; use std::str::FromStr; -use std::time::{Duration, SystemTimeError, UNIX_EPOCH}; +use std::time::{SystemTimeError, UNIX_EPOCH}; pub type InvoiceResult = Result; @@ -75,12 +75,27 @@ impl From for Quantity { } } +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub enum Amount { + Bitcoin { amount_msats: u64 }, + Currency { iso4217_code: [u8; 3], amount: u64 }, +} + +impl From for Amount { + fn from(amount: lightning::offers::offer::Amount) -> Self { + match amount { + lightning::offers::offer::Amount::Bitcoin { amount_msats } => Amount::Bitcoin { amount_msats }, + lightning::offers::offer::Amount::Currency { iso4217_code, amount } => Amount::Currency { iso4217_code, amount }, + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct LNOffer { pub chains: Vec, - pub amount_msats: Option, + pub amount: Option, pub description: String, - pub absolute_expiry: Option, + pub absolute_expiry: Option, pub issuer: Option, pub supported_quantity: Quantity, pub signing_pubkey: PublicKey, diff --git a/libs/sdk-core/src/models.rs b/libs/sdk-core/src/models.rs index 3f356963c..3c1648e93 100644 --- a/libs/sdk-core/src/models.rs +++ b/libs/sdk-core/src/models.rs @@ -856,6 +856,34 @@ pub struct RefundResponse { pub refund_tx_id: String, } +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct FetchInvoiceRequest { + pub offer: String, + pub amount_msat: Option, + pub quantity: Option, + pub timeout: Option, + pub payer_note: Option, + // pub recurrence_counter: Option, + // pub recurrence_start: Option, + // pub recurrence_label: Option, +} + +impl Into for FetchInvoiceRequest { + fn into(self) -> gl_client::pb::cln::FetchinvoiceRequest { + gl_client::pb::cln::FetchinvoiceRequest { + offer: self.offer, + amount_msat: self.amount_msat.map(|msat| cln::Amount { msat }), + quantity: self.quantity, + timeout: self.timeout, + payer_note: self.payer_note, + // Not yet implemented + recurrence_counter: None, + recurrence_start: None, + recurrence_label: None, + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct FetchInvoiceChanges { pub description_appended: Option, @@ -868,8 +896,8 @@ pub struct FetchInvoiceChanges { #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct FetchInvoiceNextPeriod { pub counter: u64, - pub starttime: u64, - pub endtime: u64, + pub start_time: u64, + pub end_time: u64, pub paywindow_start: u64, pub paywindow_end: u64, } @@ -881,27 +909,8 @@ pub struct FetchInvoiceResponse { pub next_period: Option, } -impl From> for FetchInvoiceResponse { - fn from(response: tonic::Response) -> Self { - let response = response.into_inner(); - - FetchInvoiceResponse { - invoice: response.invoice, - changes: response.changes.map(|changes| FetchInvoiceChanges { - description: changes.description, - description_appended: changes.description_appended, - vendor: changes.vendor, - vendor_removed: changes.vendor_removed, - amount_msat: changes.amount_msat.map(|amount| amount.msat), - }), - next_period: response.next_period.map(|np| FetchInvoiceNextPeriod { - counter: np.counter, - starttime: np.starttime, - endtime: np.endtime, - paywindow_start: np.paywindow_start, - paywindow_end: np.paywindow_end, - }), - } +impl From for FetchInvoiceResponse { + fn from(response: cln::FetchinvoiceResponse) -> Self { } } diff --git a/libs/sdk-core/src/node_api.rs b/libs/sdk-core/src/node_api.rs index bc1fa745f..76907305b 100644 --- a/libs/sdk-core/src/node_api.rs +++ b/libs/sdk-core/src/node_api.rs @@ -1,10 +1,10 @@ use crate::{ - invoice::InvoiceError, persist::error::PersistError, CustomMessage, MaxChannelAmount, - NodeCredentials, PaymentResponse, Peer, PrepareSweepRequest, PrepareSweepResponse, - RouteHintHop, SyncResponse, + invoice::InvoiceError, persist::error::PersistError, CustomMessage, FetchInvoiceResponse, + PaymentResponse, Peer, PrepareSweepRequest, PrepareSweepResponse, SyncResponse, FetchInvoiceRequest, }; use anyhow::Result; use bitcoin::util::bip32::{ChildNumber, ExtendedPrivKey}; +use lightning::offers::parse::ParseError; use lightning_invoice::RawInvoice; use std::pin::Pin; use tokio::sync::mpsc; @@ -48,8 +48,17 @@ pub enum NodeError { #[error("Service connectivity: {0}")] ServiceConnectivity(anyhow::Error), - #[error("Feature not yet supported by Greenlight")] - NotSupported(), + #[error("Invalid offer: {0:?}")] + InvalidOffer(ParseError), + + #[error("Offer expired: {0}")] + OfferExpired(anyhow::Error), + + #[error("Offer reply error: {0}")] + OfferReplyError(anyhow::Error), + + #[error("Offer timeout: {0}")] + OfferTimeout(anyhow::Error), } /// Trait covering functions affecting the LN node @@ -119,14 +128,7 @@ pub trait NodeAPI: Send + Sync { ) -> NodeResult> + Send>>>; async fn fetch_invoice( &self, - offer: String, - amount_msat: Option, - quantity: Option, - recurrence_counter: Option, - recurrence_start: Option, - recurrence_label: Option, - timeout: Option, - payer_note: Option, + req: FetchInvoiceRequest, ) -> NodeResult; /// Gets the private key at the path specified