diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 1a56a7d4b..c2f0166c8 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -698,6 +698,44 @@ dictionary NodeAnnouncementInfo { sequence addresses; }; +enum Currency { + "Bitcoin", + "BitcoinTestnet", + "Regtest", + "Simnet", + "Signet", +}; + +dictionary RouteHintHop { + PublicKey src_node_id; + u64 short_channel_id; + u16 cltv_expiry_delta; + u64? htlc_minimum_msat; + u64? htlc_maximum_msat; + RoutingFees fees; +}; + +interface Bolt11Invoice { + [Throws=NodeError, Name=from_str] + constructor([ByRef] string invoice_str); + sequence signable_hash(); + PaymentHash payment_hash(); + PaymentSecret payment_secret(); + u64? amount_milli_satoshis(); + u64 expiry_time_seconds(); + u64 seconds_since_epoch(); + u64 seconds_until_expiry(); + boolean is_expired(); + boolean would_expire(u64 at_time_seconds); + Bolt11InvoiceDescription description(); + u64 min_final_cltv_expiry_delta(); + Network network(); + Currency currency(); + sequence
fallback_addresses(); + sequence> route_hints(); + PublicKey recover_payee_pub_key(); +}; + [Custom] typedef string Txid; @@ -716,9 +754,6 @@ typedef string NodeId; [Custom] typedef string Address; -[Custom] -typedef string Bolt11Invoice; - [Custom] typedef string Offer; diff --git a/src/lib.rs b/src/lib.rs index 93393585d..7859a092e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,6 +23,8 @@ //! controlled via commands such as [`start`], [`stop`], [`open_channel`], [`send`], etc.: //! //! ```no_run +//! # #[cfg(not(feature = "uniffi"))] +//! # { //! use ldk_node::Builder; //! use ldk_node::lightning_invoice::Bolt11Invoice; //! use ldk_node::lightning::ln::msgs::SocketAddress; @@ -57,6 +59,7 @@ //! //! node.stop().unwrap(); //! } +//! # } //! ``` //! //! [`build`]: Builder::build diff --git a/src/liquidity.rs b/src/liquidity.rs index a47c23c81..47f3dcce4 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -1308,7 +1308,7 @@ type PaymentInfo = lightning_liquidity::lsps1::msgs::PaymentInfo; #[derive(Clone, Debug, PartialEq, Eq)] pub struct PaymentInfo { /// A Lightning payment using BOLT 11. - pub bolt11: Option, + pub bolt11: Option, /// An onchain payment. pub onchain: Option, } @@ -1316,7 +1316,10 @@ pub struct PaymentInfo { #[cfg(feature = "uniffi")] impl From for PaymentInfo { fn from(value: lightning_liquidity::lsps1::msgs::PaymentInfo) -> Self { - PaymentInfo { bolt11: value.bolt11, onchain: value.onchain.map(|o| o.into()) } + PaymentInfo { + bolt11: value.bolt11.map(|b| b.into()), + onchain: value.onchain.map(|o| o.into()), + } } } diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index ee0f24f05..75fc24887 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -30,7 +30,7 @@ use lightning::routing::router::{PaymentParameters, RouteParameters}; use lightning_types::payment::{PaymentHash, PaymentPreimage}; -use lightning_invoice::Bolt11Invoice; +use lightning_invoice::Bolt11Invoice as LdkBolt11Invoice; use lightning_invoice::Bolt11InvoiceDescription as LdkBolt11InvoiceDescription; use bitcoin::hashes::sha256::Hash as Sha256; @@ -38,6 +38,29 @@ use bitcoin::hashes::Hash; use std::sync::{Arc, RwLock}; +#[cfg(not(feature = "uniffi"))] +type Bolt11Invoice = LdkBolt11Invoice; +#[cfg(feature = "uniffi")] +type Bolt11Invoice = Arc; + +#[cfg(not(feature = "uniffi"))] +pub(crate) fn maybe_wrap_invoice(invoice: LdkBolt11Invoice) -> Bolt11Invoice { + invoice +} +#[cfg(feature = "uniffi")] +pub fn maybe_wrap_invoice(invoice: LdkBolt11Invoice) -> Bolt11Invoice { + Arc::new(invoice.into()) +} + +#[cfg(not(feature = "uniffi"))] +pub fn maybe_convert_invoice(invoice: &Bolt11Invoice) -> &LdkBolt11Invoice { + invoice +} +#[cfg(feature = "uniffi")] +pub fn maybe_convert_invoice(invoice: &Bolt11Invoice) -> &LdkBolt11Invoice { + &invoice.inner +} + #[cfg(not(feature = "uniffi"))] type Bolt11InvoiceDescription = LdkBolt11InvoiceDescription; #[cfg(feature = "uniffi")] @@ -101,6 +124,7 @@ impl Bolt11Payment { pub fn send( &self, invoice: &Bolt11Invoice, sending_parameters: Option, ) -> Result { + let invoice = maybe_convert_invoice(invoice); let rt_lock = self.runtime.read().unwrap(); if rt_lock.is_none() { return Err(Error::NotRunning); @@ -209,6 +233,7 @@ impl Bolt11Payment { &self, invoice: &Bolt11Invoice, amount_msat: u64, sending_parameters: Option, ) -> Result { + let invoice = maybe_convert_invoice(invoice); let rt_lock = self.runtime.read().unwrap(); if rt_lock.is_none() { return Err(Error::NotRunning); @@ -441,7 +466,8 @@ impl Bolt11Payment { &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, ) -> Result { let description = maybe_convert_description!(description); - self.receive_inner(Some(amount_msat), description, expiry_secs, None) + let invoice = self.receive_inner(Some(amount_msat), description, expiry_secs, None)?; + Ok(maybe_wrap_invoice(invoice)) } /// Returns a payable invoice that can be used to request a payment of the amount @@ -463,7 +489,9 @@ impl Bolt11Payment { payment_hash: PaymentHash, ) -> Result { let description = maybe_convert_description!(description); - self.receive_inner(Some(amount_msat), description, expiry_secs, Some(payment_hash)) + let invoice = + self.receive_inner(Some(amount_msat), description, expiry_secs, Some(payment_hash))?; + Ok(maybe_wrap_invoice(invoice)) } /// Returns a payable invoice that can be used to request and receive a payment for which the @@ -474,7 +502,8 @@ impl Bolt11Payment { &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, ) -> Result { let description = maybe_convert_description!(description); - self.receive_inner(None, description, expiry_secs, None) + let invoice = self.receive_inner(None, description, expiry_secs, None)?; + Ok(maybe_wrap_invoice(invoice)) } /// Returns a payable invoice that can be used to request a payment for the given payment hash @@ -495,13 +524,14 @@ impl Bolt11Payment { &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, payment_hash: PaymentHash, ) -> Result { let description = maybe_convert_description!(description); - self.receive_inner(None, description, expiry_secs, Some(payment_hash)) + let invoice = self.receive_inner(None, description, expiry_secs, Some(payment_hash))?; + Ok(maybe_wrap_invoice(invoice)) } pub(crate) fn receive_inner( &self, amount_msat: Option, invoice_description: &LdkBolt11InvoiceDescription, expiry_secs: u32, manual_claim_payment_hash: Option, - ) -> Result { + ) -> Result { let invoice = { let invoice_params = Bolt11InvoiceParameters { amount_msats: amount_msat, @@ -571,13 +601,14 @@ impl Bolt11Payment { max_total_lsp_fee_limit_msat: Option, ) -> Result { let description = maybe_convert_description!(description); - self.receive_via_jit_channel_inner( + let invoice = self.receive_via_jit_channel_inner( Some(amount_msat), description, expiry_secs, max_total_lsp_fee_limit_msat, None, - ) + )?; + Ok(maybe_wrap_invoice(invoice)) } /// Returns a payable invoice that can be used to request a variable amount payment (also known @@ -596,20 +627,21 @@ impl Bolt11Payment { max_proportional_lsp_fee_limit_ppm_msat: Option, ) -> Result { let description = maybe_convert_description!(description); - self.receive_via_jit_channel_inner( + let invoice = self.receive_via_jit_channel_inner( None, description, expiry_secs, None, max_proportional_lsp_fee_limit_ppm_msat, - ) + )?; + Ok(maybe_wrap_invoice(invoice)) } fn receive_via_jit_channel_inner( &self, amount_msat: Option, description: &LdkBolt11InvoiceDescription, expiry_secs: u32, max_total_lsp_fee_limit_msat: Option, max_proportional_lsp_fee_limit_ppm_msat: Option, - ) -> Result { + ) -> Result { let liquidity_source = self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; @@ -709,6 +741,7 @@ impl Bolt11Payment { /// amount times [`Config::probing_liquidity_limit_multiplier`] won't be used to send /// pre-flight probes. pub fn send_probes(&self, invoice: &Bolt11Invoice) -> Result<(), Error> { + let invoice = maybe_convert_invoice(invoice); let rt_lock = self.runtime.read().unwrap(); if rt_lock.is_none() { return Err(Error::NotRunning); @@ -741,6 +774,7 @@ impl Bolt11Payment { pub fn send_probes_using_amount( &self, invoice: &Bolt11Invoice, amount_msat: u64, ) -> Result<(), Error> { + let invoice = maybe_convert_invoice(invoice); let rt_lock = self.runtime.read().unwrap(); if rt_lock.is_none() { return Err(Error::NotRunning); diff --git a/src/payment/unified_qr.rs b/src/payment/unified_qr.rs index 92c405056..ec37931a0 100644 --- a/src/payment/unified_qr.rs +++ b/src/payment/unified_qr.rs @@ -13,7 +13,7 @@ //! [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md use crate::error::Error; use crate::logger::{log_error, LdkLogger, Logger}; -use crate::payment::{Bolt11Payment, Bolt12Payment, OnchainPayment}; +use crate::payment::{bolt11::maybe_wrap_invoice, Bolt11Payment, Bolt12Payment, OnchainPayment}; use crate::Config; use lightning::ln::channelmanager::PaymentId; @@ -149,6 +149,7 @@ impl UnifiedQrPayment { } if let Some(invoice) = uri_network_checked.extras.bolt11_invoice { + let invoice = maybe_wrap_invoice(invoice); match self.bolt11_invoice.send(&invoice, None) { Ok(payment_id) => return Ok(QrPaymentResult::Bolt11 { payment_id }), Err(e) => log_error!(self.logger, "Failed to send BOLT11 invoice: {:?}. This is part of a unified QR code payment. Falling back to the on-chain transaction.", e), diff --git a/src/uniffi_types.rs b/src/uniffi_types.rs index acdee3a94..77f9348cc 100644 --- a/src/uniffi_types.rs +++ b/src/uniffi_types.rs @@ -33,12 +33,10 @@ pub use lightning::util::string::UntrustedString; pub use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; -pub use lightning_invoice::{Bolt11Invoice, Description}; +pub use lightning_invoice::{Description, SignedRawBolt11Invoice}; pub use lightning_liquidity::lsps1::msgs::ChannelInfo as ChannelOrderInfo; -pub use lightning_liquidity::lsps1::msgs::{ - Bolt11PaymentInfo, OrderId, OrderParameters, PaymentState, -}; +pub use lightning_liquidity::lsps1::msgs::{OrderId, OrderParameters, PaymentState}; pub use bitcoin::{Address, BlockHash, FeeRate, Network, OutPoint, Txid}; @@ -60,10 +58,12 @@ use bitcoin::hashes::Hash; use bitcoin::secp256k1::PublicKey; use lightning::ln::channelmanager::PaymentId; use lightning::util::ser::Writeable; -use lightning_invoice::SignedRawBolt11Invoice; +use lightning_invoice::{Bolt11Invoice as LdkBolt11Invoice, Bolt11InvoiceDescriptionRef}; use std::convert::TryInto; use std::str::FromStr; +use std::sync::Arc; +use std::time::Duration; impl UniffiCustomTypeConverter for PublicKey { type Builtin = String; @@ -113,24 +113,6 @@ impl UniffiCustomTypeConverter for Address { } } -impl UniffiCustomTypeConverter for Bolt11Invoice { - type Builtin = String; - - fn into_custom(val: Self::Builtin) -> uniffi::Result { - if let Ok(signed) = val.parse::() { - if let Ok(invoice) = Bolt11Invoice::from_signed(signed) { - return Ok(invoice); - } - } - - Err(Error::InvalidInvoice.into()) - } - - fn from_custom(obj: Self) -> Self::Builtin { - obj.to_string() - } -} - impl UniffiCustomTypeConverter for Offer { type Builtin = String; @@ -405,6 +387,251 @@ impl From for Bolt11InvoiceDescript } } +impl<'a> From> for Bolt11InvoiceDescription { + fn from(value: Bolt11InvoiceDescriptionRef<'a>) -> Self { + match value { + lightning_invoice::Bolt11InvoiceDescriptionRef::Direct(description) => { + Bolt11InvoiceDescription::Direct { description: description.to_string() } + }, + lightning_invoice::Bolt11InvoiceDescriptionRef::Hash(hash) => { + Bolt11InvoiceDescription::Hash { hash: hex_utils::to_string(hash.0.as_ref()) } + }, + } + } +} + +/// Enum representing the crypto currencies (or networks) supported by this library +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum Currency { + /// Bitcoin mainnet + Bitcoin, + + /// Bitcoin testnet + BitcoinTestnet, + + /// Bitcoin regtest + Regtest, + + /// Bitcoin simnet + Simnet, + + /// Bitcoin signet + Signet, +} + +impl From for Currency { + fn from(currency: lightning_invoice::Currency) -> Self { + match currency { + lightning_invoice::Currency::Bitcoin => Currency::Bitcoin, + lightning_invoice::Currency::BitcoinTestnet => Currency::BitcoinTestnet, + lightning_invoice::Currency::Regtest => Currency::Regtest, + lightning_invoice::Currency::Simnet => Currency::Simnet, + lightning_invoice::Currency::Signet => Currency::Signet, + } + } +} + +/// A channel descriptor for a hop along a payment path. +/// +/// While this generally comes from BOLT 11's `r` field, this struct includes more fields than are +/// available in BOLT 11. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RouteHintHop { + /// The node_id of the non-target end of the route + pub src_node_id: PublicKey, + /// The short_channel_id of this channel + pub short_channel_id: u64, + /// The fees which must be paid to use this channel + pub fees: RoutingFees, + /// The difference in CLTV values between this node and the next node. + pub cltv_expiry_delta: u16, + /// The minimum value, in msat, which must be relayed to the next hop. + pub htlc_minimum_msat: Option, + /// The maximum value in msat available for routing with a single HTLC. + pub htlc_maximum_msat: Option, +} + +impl From for RouteHintHop { + fn from(hop: lightning::routing::router::RouteHintHop) -> Self { + Self { + src_node_id: hop.src_node_id, + short_channel_id: hop.short_channel_id, + cltv_expiry_delta: hop.cltv_expiry_delta, + htlc_minimum_msat: hop.htlc_minimum_msat, + htlc_maximum_msat: hop.htlc_maximum_msat, + fees: hop.fees, + } + } +} + +/// Represents a syntactically and semantically correct lightning BOLT11 invoice. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Bolt11Invoice { + pub inner: LdkBolt11Invoice, +} + +impl Bolt11Invoice { + pub fn from_str(invoice_str: &str) -> Result { + invoice_str.parse() + } + + /// Returns the underlying invoice [`LdkBolt11Invoice`] + pub fn into_inner(self) -> LdkBolt11Invoice { + self.inner + } + + /// The hash of the [`RawBolt11Invoice`] that was signed. + /// + /// [`RawBolt11Invoice`]: lightning_invoice::RawBolt11Invoice + pub fn signable_hash(&self) -> Vec { + self.inner.signable_hash().to_vec() + } + + /// Returns the hash to which we will receive the preimage on completion of the payment + pub fn payment_hash(&self) -> PaymentHash { + PaymentHash(self.inner.payment_hash().to_byte_array()) + } + + /// Get the payment secret if one was included in the invoice + pub fn payment_secret(&self) -> PaymentSecret { + PaymentSecret(self.inner.payment_secret().0) + } + + /// Returns the amount if specified in the invoice as millisatoshis. + pub fn amount_milli_satoshis(&self) -> Option { + self.inner.amount_milli_satoshis() + } + + /// Returns the invoice's expiry time (in seconds), if present, otherwise [`DEFAULT_EXPIRY_TIME`]. + /// + /// [`DEFAULT_EXPIRY_TIME`]: lightning_invoice::DEFAULT_EXPIRY_TIME + pub fn expiry_time_seconds(&self) -> u64 { + self.inner.expiry_time().as_secs() + } + + /// Returns the `Bolt11Invoice`'s timestamp as seconds since the Unix epoch + pub fn seconds_since_epoch(&self) -> u64 { + self.inner.duration_since_epoch().as_secs() + } + + /// Returns the seconds remaining until the invoice expires. + pub fn seconds_until_expiry(&self) -> u64 { + self.inner.duration_until_expiry().as_secs() + } + + /// Returns whether the invoice has expired. + pub fn is_expired(&self) -> bool { + self.inner.is_expired() + } + + /// Returns whether the expiry time would pass at the given point in time. + /// `at_time_seconds` is the timestamp as seconds since the Unix epoch. + pub fn would_expire(&self, at_time_seconds: u64) -> bool { + self.inner.would_expire(Duration::from_secs(at_time_seconds)) + } + + /// Return the description or a hash of it for longer ones + pub fn description(&self) -> Bolt11InvoiceDescription { + self.inner.description().into() + } + + /// Returns the invoice's `min_final_cltv_expiry_delta` time, if present, otherwise + /// [`DEFAULT_MIN_FINAL_CLTV_EXPIRY_DELTA`]. + /// + /// [`DEFAULT_MIN_FINAL_CLTV_EXPIRY_DELTA`]: lightning_invoice::DEFAULT_MIN_FINAL_CLTV_EXPIRY_DELTA + pub fn min_final_cltv_expiry_delta(&self) -> u64 { + self.inner.min_final_cltv_expiry_delta() + } + + /// Returns the network for which the invoice was issued + pub fn network(&self) -> Network { + self.inner.network() + } + + /// Returns the currency for which the invoice was issued + pub fn currency(&self) -> Currency { + self.inner.currency().into() + } + + /// Returns a list of all fallback addresses as [`Address`]es + pub fn fallback_addresses(&self) -> Vec
{ + self.inner.fallback_addresses() + } + + /// Returns a list of all routes included in the invoice as the underlying hints + pub fn route_hints(&self) -> Vec> { + self.inner + .route_hints() + .iter() + .map(|route| route.0.iter().map(|hop| RouteHintHop::from(hop.clone())).collect()) + .collect() + } + + /// Recover the payee's public key (only to be used if none was included in the invoice) + pub fn recover_payee_pub_key(&self) -> PublicKey { + self.inner.recover_payee_pub_key() + } +} + +impl std::str::FromStr for Bolt11Invoice { + type Err = Error; + + fn from_str(invoice_str: &str) -> Result { + match invoice_str.parse::() { + Ok(signed) => match LdkBolt11Invoice::from_signed(signed) { + Ok(invoice) => Ok(Bolt11Invoice { inner: invoice }), + Err(_) => Err(Error::InvalidInvoice), + }, + Err(_) => Err(Error::InvalidInvoice), + } + } +} + +impl From for Bolt11Invoice { + fn from(invoice: LdkBolt11Invoice) -> Self { + Bolt11Invoice { inner: invoice } + } +} + +impl From for LdkBolt11Invoice { + fn from(wrapper: Bolt11Invoice) -> Self { + wrapper.into_inner() + } +} + +impl std::fmt::Display for Bolt11Invoice { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.inner) + } +} + +/// A Lightning payment using BOLT 11. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Bolt11PaymentInfo { + /// Indicates the current state of the payment. + pub state: PaymentState, + /// The datetime when the payment option expires. + pub expires_at: chrono::DateTime, + /// The total fee the LSP will charge to open this channel in satoshi. + pub fee_total_sat: u64, + /// The amount the client needs to pay to have the requested channel openend. + pub order_total_sat: u64, + /// A BOLT11 invoice the client can pay to have to channel opened. + pub invoice: Arc, +} + +impl From for Bolt11PaymentInfo { + fn from(info: lightning_liquidity::lsps1::msgs::Bolt11PaymentInfo) -> Self { + Self { + state: info.state, + expires_at: info.expires_at, + fee_total_sat: info.fee_total_sat, + order_total_sat: info.order_total_sat, + invoice: Arc::new(info.invoice.into()), + } + } +} + impl UniffiCustomTypeConverter for OrderId { type Builtin = String; @@ -432,6 +659,14 @@ impl UniffiCustomTypeConverter for DateTime { #[cfg(test)] mod tests { use super::*; + + fn create_test_invoice() -> (LdkBolt11Invoice, Bolt11Invoice) { + let invoice_string = "lnbc1pn8g249pp5f6ytj32ty90jhvw69enf30hwfgdhyymjewywcmfjevflg6s4z86qdqqcqzzgxqyz5vqrzjqwnvuc0u4txn35cafc7w94gxvq5p3cu9dd95f7hlrh0fvs46wpvhdfjjzh2j9f7ye5qqqqryqqqqthqqpysp5mm832athgcal3m7h35sc29j63lmgzvwc5smfjh2es65elc2ns7dq9qrsgqu2xcje2gsnjp0wn97aknyd3h58an7sjj6nhcrm40846jxphv47958c6th76whmec8ttr2wmg6sxwchvxmsc00kqrzqcga6lvsf9jtqgqy5yexa"; + let ldk_invoice: LdkBolt11Invoice = invoice_string.parse().unwrap(); + let wrapped_invoice = Bolt11Invoice::from(ldk_invoice.clone()); + (ldk_invoice, wrapped_invoice) + } + #[test] fn test_invoice_description_conversion() { let hash = "09d08d4865e8af9266f6cc7c0ae23a1d6bf868207cf8f7c5979b9f6ed850dfb0".to_string(); @@ -441,4 +676,104 @@ mod tests { let reconverted_description: Bolt11InvoiceDescription = converted_description.into(); assert_eq!(description, reconverted_description); } + + #[test] + fn test_bolt11_invoice_basic_properties() { + let (ldk_invoice, wrapped_invoice) = create_test_invoice(); + + assert_eq!( + ldk_invoice.payment_hash().to_string(), + wrapped_invoice.payment_hash().to_string() + ); + assert_eq!(ldk_invoice.amount_milli_satoshis(), wrapped_invoice.amount_milli_satoshis()); + + assert_eq!( + ldk_invoice.min_final_cltv_expiry_delta(), + wrapped_invoice.min_final_cltv_expiry_delta() + ); + assert_eq!( + ldk_invoice.payment_secret().0.to_vec(), + wrapped_invoice.payment_secret().0.to_vec() + ); + + assert_eq!(ldk_invoice.network(), wrapped_invoice.network()); + assert_eq!( + format!("{:?}", ldk_invoice.currency()), + format!("{:?}", wrapped_invoice.currency()) + ); + } + + #[test] + fn test_bolt11_invoice_time_related_fields() { + let (ldk_invoice, wrapped_invoice) = create_test_invoice(); + + assert_eq!(ldk_invoice.expiry_time().as_secs(), wrapped_invoice.expiry_time_seconds()); + assert_eq!( + ldk_invoice.duration_until_expiry().as_secs(), + wrapped_invoice.seconds_until_expiry() + ); + assert_eq!( + ldk_invoice.duration_since_epoch().as_secs(), + wrapped_invoice.seconds_since_epoch() + ); + + let future_time = Duration::from_secs(wrapped_invoice.seconds_since_epoch() + 10000); + assert!(!ldk_invoice.would_expire(future_time)); + assert!(!wrapped_invoice.would_expire(future_time.as_secs())); + } + + #[test] + fn test_bolt11_invoice_description() { + let (ldk_invoice, wrapped_invoice) = create_test_invoice(); + + let ldk_description = ldk_invoice.description(); + let wrapped_description = wrapped_invoice.description(); + + match (ldk_description, &wrapped_description) { + ( + lightning_invoice::Bolt11InvoiceDescriptionRef::Direct(ldk_description), + Bolt11InvoiceDescription::Direct { description }, + ) => { + assert_eq!(ldk_description.to_string(), *description) + }, + ( + lightning_invoice::Bolt11InvoiceDescriptionRef::Hash(ldk_hash), + Bolt11InvoiceDescription::Hash { hash }, + ) => { + assert_eq!(hex_utils::to_string(ldk_hash.0.as_ref()), *hash) + }, + _ => panic!("Description types don't match"), + } + } + + #[test] + fn test_bolt11_invoice_route_hints() { + let (ldk_invoice, wrapped_invoice) = create_test_invoice(); + + let wrapped_route_hints = wrapped_invoice.route_hints(); + let ldk_route_hints = ldk_invoice.route_hints(); + assert_eq!(ldk_route_hints.len(), wrapped_route_hints.len()); + + let ldk_hop = &ldk_route_hints[0].0[0]; + let wrapped_hop = &wrapped_route_hints[0][0]; + assert_eq!(ldk_hop.src_node_id, wrapped_hop.src_node_id); + assert_eq!(ldk_hop.short_channel_id, wrapped_hop.short_channel_id); + assert_eq!(ldk_hop.cltv_expiry_delta, wrapped_hop.cltv_expiry_delta); + assert_eq!(ldk_hop.htlc_minimum_msat, wrapped_hop.htlc_minimum_msat); + assert_eq!(ldk_hop.htlc_maximum_msat, wrapped_hop.htlc_maximum_msat); + assert_eq!(ldk_hop.fees.base_msat, wrapped_hop.fees.base_msat); + assert_eq!(ldk_hop.fees.proportional_millionths, wrapped_hop.fees.proportional_millionths); + } + + #[test] + fn test_bolt11_invoice_roundtrip() { + let (ldk_invoice, wrapped_invoice) = create_test_invoice(); + + let invoice_str = wrapped_invoice.to_string(); + let parsed_invoice: LdkBolt11Invoice = invoice_str.parse().unwrap(); + assert_eq!( + ldk_invoice.payment_hash().to_byte_array().to_vec(), + parsed_invoice.payment_hash().to_byte_array().to_vec() + ); + } }