diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e9236489..876124b94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,11 @@ `ChannelTypeFeatures`. - `Config::anchor_channels_config` is no longer optional, hence anchor channels can no longer be disabled. We still negotiate legacy channels if the peer does not support anchor channels. +- The paid BOLT 12 invoice is now persisted on `PaymentKind::Bolt12Offer` and + `PaymentKind::Bolt12Refund`, and `Bolt12Payment::create_payer_proof` allows building a BOLT 12 + payer proof for a previously succeeded outbound BOLT 12 payment. `PayerProofOptions` controls + which optional invoice fields are selectively disclosed. Payments that completed via a static + invoice, i.e., async payments, do not support payer proofs. (#845) ## Bug Fixes and Improvements - Building a fresh node against a Bitcoin Core RPC or REST chain source that fails to return the diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index c1a926f2f..32bd4bac7 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -206,6 +206,8 @@ enum NodeError { "FeerateEstimationUpdateTimeout", "WalletOperationFailed", "WalletOperationTimeout", + "PayerProofCreationFailed", + "PayerProofUnavailable", "OnchainTxSigningFailed", "TxSyncFailed", "TxSyncTimeout", @@ -247,6 +249,7 @@ enum NodeError { "LnurlAuthTimeout", "InvalidLnurl", "ChainSourceNotSupported", + "InvalidPayerProof", }; typedef dictionary NodeStatus; diff --git a/src/error.rs b/src/error.rs index 8546af0dd..df25d47fe 100644 --- a/src/error.rs +++ b/src/error.rs @@ -57,6 +57,10 @@ pub enum Error { WalletOperationFailed, /// A wallet operation timed out. WalletOperationTimeout, + /// Creating a payer proof failed. + PayerProofCreationFailed, + /// A payer proof is unavailable for the requested payment. + PayerProofUnavailable, /// A signing operation for transaction failed. OnchainTxSigningFailed, /// A transaction sync operation failed. @@ -139,6 +143,8 @@ pub enum Error { InvalidLnurl, /// The configured chain source is not supported. ChainSourceNotSupported, + /// The provided payer proof is invalid. + InvalidPayerProof, } impl fmt::Display for Error { @@ -170,6 +176,10 @@ impl fmt::Display for Error { }, Self::WalletOperationFailed => write!(f, "Failed to conduct wallet operation."), Self::WalletOperationTimeout => write!(f, "A wallet operation timed out."), + Self::PayerProofCreationFailed => write!(f, "Failed to create payer proof."), + Self::PayerProofUnavailable => { + write!(f, "A payer proof is unavailable for the requested payment.") + }, Self::OnchainTxSigningFailed => write!(f, "Failed to sign given transaction."), Self::TxSyncFailed => write!(f, "Failed to sync transactions."), Self::TxSyncTimeout => write!(f, "Syncing transactions timed out."), @@ -227,6 +237,7 @@ impl fmt::Display for Error { Self::ChainSourceNotSupported => { write!(f, "The configured chain source is not supported.") }, + Self::InvalidPayerProof => write!(f, "The provided payer proof is invalid."), } } } diff --git a/src/event.rs b/src/event.rs index be54969c7..fb154545b 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1129,6 +1129,7 @@ where offer_id, payer_note, quantity, + bolt12_invoice: None, }; let payment = PaymentDetails::new( @@ -1178,6 +1179,7 @@ where secret: Some(payment_secret), payer_note: None, quantity: None, + bolt12_invoice: None, }; let payment = PaymentDetails::new( @@ -1331,6 +1333,7 @@ where offer_id: payment_context.offer_id, payer_note: payment_context.invoice_request.payer_note_truncated, quantity: payment_context.invoice_request.quantity, + bolt12_invoice: None, }; let update = PaymentDetailsUpdate { preimage: Some(payment_preimage), @@ -1352,6 +1355,7 @@ where secret: Some(payment_secret), payer_note: None, quantity: None, + bolt12_invoice: None, }; let update = PaymentDetailsUpdate { preimage: Some(payment_preimage), @@ -1442,12 +1446,16 @@ where debug_assert!(false, "payment_id should always be set."); return Ok(()); }; + let bolt12_invoice = bolt12_invoice.map(Into::into); + // Only set the field if the event actually carried an invoice, to avoid + // overriding any previously-stored invoice with `None`. let update = PaymentDetailsUpdate { hash: Some(Some(payment_hash)), preimage: Some(Some(payment_preimage)), fee_paid_msat: Some(fee_paid_msat), status: Some(PaymentStatus::Succeeded), + bolt12_invoice: bolt12_invoice.clone().map(Some), ..PaymentDetailsUpdate::new(payment_id) }; @@ -1480,7 +1488,7 @@ where payment_hash, payment_preimage: Some(payment_preimage), fee_paid_msat, - bolt12_invoice: bolt12_invoice.map(Into::into), + bolt12_invoice, }; match self.event_queue.add_event(event).await { diff --git a/src/ffi/types.rs b/src/ffi/types.rs index 0dc79758d..25465ced0 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -23,7 +23,6 @@ use bitcoin::hashes::Hash; use bitcoin::secp256k1::PublicKey; pub use bitcoin::{Address, BlockHash, Network, OutPoint, ScriptBuf, Txid}; pub use lightning::chain::channelmonitor::BalanceSource; -use lightning::events::PaidBolt12Invoice as LdkPaidBolt12Invoice; pub use lightning::events::{ClosureReason, PaymentFailureReason}; use lightning::ln::channel_state::{ChannelShutdownState, CounterpartyForwardingInfo}; use lightning::ln::channelmanager::PaymentId; @@ -32,6 +31,9 @@ pub use lightning::ln::types::ChannelId; use lightning::offers::invoice::Bolt12Invoice as LdkBolt12Invoice; pub use lightning::offers::offer::OfferId; use lightning::offers::offer::{Amount as LdkAmount, Offer as LdkOffer}; +use lightning::offers::payer_proof::{ + PaidBolt12Invoice as LdkPaidBolt12Invoice, PayerProof as LdkPayerProof, +}; use lightning::offers::refund::Refund as LdkRefund; use lightning::offers::static_invoice::StaticInvoice as LdkStaticInvoice; use lightning::onion_message::dns_resolution::HumanReadableName as LdkHumanReadableName; @@ -881,6 +883,113 @@ impl Readable for PaidBolt12Invoice { } } +/// A cryptographic proof that a BOLT12 invoice was paid by this node. +#[derive(Debug, Clone, uniffi::Object)] +#[uniffi::export(Debug, Display)] +pub struct PayerProof { + pub(crate) inner: LdkPayerProof, +} + +#[uniffi::export] +impl PayerProof { + #[uniffi::constructor] + pub fn from_bytes(proof_bytes: Vec) -> Result { + let inner = LdkPayerProof::try_from(proof_bytes).map_err(|_| Error::InvalidPayerProof)?; + Ok(Self { inner }) + } + + /// The payment preimage proving the payment completed. + pub fn payment_preimage(&self) -> PaymentPreimage { + self.inner.payment_preimage() + } + + /// The payment hash committed to by the invoice and proven by the preimage. + pub fn payment_hash(&self) -> PaymentHash { + self.inner.payment_hash() + } + + /// The public key of the payer that authorized the payment. + pub fn payer_signing_pubkey(&self) -> PublicKey { + self.inner.payer_signing_pubkey() + } + + /// The issuer signing public key committed to by the invoice. + pub fn issuer_signing_pubkey(&self) -> PublicKey { + self.inner.issuer_signing_pubkey() + } + + /// The invoice signature bytes. + pub fn invoice_signature(&self) -> Vec { + self.inner.invoice_signature().as_ref().to_vec() + } + + /// The proof signature bytes. + pub fn proof_signature(&self) -> Vec { + self.inner.proof_signature().as_ref().to_vec() + } + + /// The offer description, if it was disclosed in the proof. + pub fn offer_description(&self) -> Option { + self.inner.offer_description().map(|value| value.to_string()) + } + + /// The offer issuer, if it was disclosed in the proof. + pub fn offer_issuer(&self) -> Option { + self.inner.offer_issuer().map(|value| value.to_string()) + } + + /// The invoice amount in millisatoshis, if it was disclosed in the proof. + pub fn invoice_amount_msats(&self) -> Option { + self.inner.invoice_amount_msats() + } + + /// The invoice creation time, in seconds since the UNIX epoch, if it was disclosed in the + /// proof. + pub fn invoice_created_at(&self) -> Option { + self.inner.invoice_created_at().map(|value| value.as_secs()) + } + + /// The optional note attached to the proof. + pub fn proof_note(&self) -> Option { + self.inner.proof_note().map(|value| value.to_string()) + } + + /// The Merkle root committed to by the proof. + pub fn merkle_root(&self) -> Vec { + self.inner.merkle_root().to_byte_array().to_vec() + } + + /// The raw TLV bytes of the proof. + pub fn bytes(&self) -> Vec { + self.inner.bytes().to_vec() + } + + /// The bech32-encoded string form of the proof. + pub fn as_string(&self) -> String { + self.inner.to_string() + } +} + +impl From for PayerProof { + fn from(inner: LdkPayerProof) -> Self { + Self { inner } + } +} + +impl Deref for PayerProof { + type Target = LdkPayerProof; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl std::fmt::Display for PayerProof { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.inner) + } +} + uniffi::custom_type!(OfferId, String, { remote, try_lift: |val| { diff --git a/src/payment/bolt12.rs b/src/payment/bolt12.rs index 15ab251f0..149715aaf 100644 --- a/src/payment/bolt12.rs +++ b/src/payment/bolt12.rs @@ -18,10 +18,14 @@ use lightning::ln::channelmanager::{OptionalOfferPaymentParams, PaymentId}; use lightning::ln::outbound_payment::Retry; use lightning::offers::offer::{Amount, Offer as LdkOffer, OfferFromHrn, Quantity}; use lightning::offers::parse::Bolt12SemanticError; +use lightning::offers::payer_proof::PaidBolt12Invoice as LdkPaidBolt12Invoice; +#[cfg(not(feature = "uniffi"))] +use lightning::offers::payer_proof::PayerProof as LdkPayerProof; use lightning::routing::router::RouteParametersConfig; -use lightning::sign::EntropySource; +use lightning::sign::{EntropySource, NodeSigner}; #[cfg(feature = "uniffi")] use lightning::util::ser::{Readable, Writeable}; +use lightning_types::payment::PaymentPreimage; use lightning_types::string::UntrustedString; use crate::config::{AsyncPaymentsRole, Config, LDK_PAYMENT_RETRY_TIMEOUT}; @@ -52,6 +56,11 @@ type HumanReadableName = lightning::onion_message::dns_resolution::HumanReadable #[cfg(feature = "uniffi")] type HumanReadableName = Arc; +#[cfg(not(feature = "uniffi"))] +type PayerProof = LdkPayerProof; +#[cfg(feature = "uniffi")] +type PayerProof = Arc; + /// A payment handler allowing to create and pay [BOLT 12] offers and refunds. /// /// Should be retrieved by calling [`Node::bolt12_payment`]. @@ -70,6 +79,24 @@ pub struct Bolt12Payment { async_payments_role: Option, } +/// Options controlling which optional fields are disclosed in a BOLT12 payer proof. +#[derive(Clone, Debug, PartialEq, Eq, Default)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +pub struct PayerProofOptions { + /// An optional note attached to the payer proof itself. + pub note: Option, + /// Whether to include the offer description in the proof. + pub include_offer_description: bool, + /// Whether to include the offer issuer in the proof. + pub include_offer_issuer: bool, + /// Whether to include the invoice amount in the proof. + pub include_invoice_amount: bool, + /// Whether to include the invoice creation timestamp in the proof. + pub include_invoice_created_at: bool, + /// Additional TLV types to include in the selective disclosure set. + pub extra_tlv_types: Vec, +} + impl Bolt12Payment { pub(crate) fn new( runtime: Arc, channel_manager: Arc, @@ -158,6 +185,7 @@ impl Bolt12Payment { offer_id: offer.id(), payer_note: payer_note.map(UntrustedString), quantity, + bolt12_invoice: None, }; let payment = PaymentDetails::new( payment_id, @@ -183,6 +211,7 @@ impl Bolt12Payment { offer_id: offer.id(), payer_note: payer_note.map(UntrustedString), quantity, + bolt12_invoice: None, }; let payment = PaymentDetails::new( payment_id, @@ -251,6 +280,33 @@ impl Bolt12Payment { .blinded_paths_for_async_recipient(recipient_id, None) .or(Err(Error::InvalidBlindedPaths)) } + + /// Retrieves the persisted payer proof context, i.e., the paid BOLT 12 invoice and the payment + /// preimage, for a successful outbound BOLT 12 payment. + fn payer_proof_context( + &self, payment_id: &PaymentId, + ) -> Result<(LdkPaidBolt12Invoice, PaymentPreimage), Error> { + let payment = self.payment_store.get(payment_id).ok_or(Error::PayerProofUnavailable)?; + if payment.direction != PaymentDirection::Outbound + || payment.status != PaymentStatus::Succeeded + { + return Err(Error::PayerProofUnavailable); + } + + match payment.kind { + PaymentKind::Bolt12Offer { + preimage: Some(preimage), + bolt12_invoice: Some(invoice), + .. + } + | PaymentKind::Bolt12Refund { + preimage: Some(preimage), + bolt12_invoice: Some(invoice), + .. + } => Ok((invoice.into(), preimage)), + _ => Err(Error::PayerProofUnavailable), + } + } } #[cfg_attr(feature = "uniffi", uniffi::export)] @@ -320,6 +376,7 @@ impl Bolt12Payment { offer_id: offer.id(), payer_note: payer_note.map(UntrustedString), quantity, + bolt12_invoice: None, }; let payment = PaymentDetails::new( payment_id, @@ -345,6 +402,7 @@ impl Bolt12Payment { offer_id: offer.id(), payer_note: payer_note.map(UntrustedString), quantity, + bolt12_invoice: None, }; let payment = PaymentDetails::new( payment_id, @@ -389,6 +447,70 @@ impl Bolt12Payment { Ok(payment_id) } + /// Create a payer proof for a previously succeeded outbound BOLT 12 payment. + /// + /// This requires a standard BOLT 12 invoice response, which we persist alongside the payment + /// in the payment store. Payments that completed via a static invoice, i.e., async payments, + /// do not support payer proofs. + pub fn create_payer_proof( + &self, payment_id: &PaymentId, options: Option, + ) -> Result { + let (paid_invoice, preimage) = self.payer_proof_context(payment_id)?; + + let options = options.unwrap_or_default(); + let expanded_key = self.keys_manager.get_expanded_key(); + let secp_ctx = bitcoin::secp256k1::Secp256k1::new(); + + let mut builder = paid_invoice + .prove_payer_derived(preimage, &expanded_key, *payment_id, &secp_ctx) + .map_err(|e| { + log_error!( + self.logger, + "Failed to initialize payer proof builder for {}: {:?}", + payment_id, + e + ); + Error::PayerProofCreationFailed + })?; + + for tlv_type in options.extra_tlv_types { + builder = builder.include_type(tlv_type).map_err(|e| { + log_error!( + self.logger, + "Failed to include TLV {} in payer proof for {}: {:?}", + tlv_type, + payment_id, + e + ); + Error::PayerProofCreationFailed + })?; + } + + if options.include_offer_description { + builder = builder.include_offer_description(); + } + if options.include_offer_issuer { + builder = builder.include_offer_issuer(); + } + if options.include_invoice_amount { + builder = builder.include_invoice_amount(); + } + if options.include_invoice_created_at { + builder = builder.include_invoice_created_at(); + } + + if let Some(note) = options.note { + builder = builder.with_proof_note(note); + } + + let proof = builder.build_and_sign().map_err(|e| { + log_error!(self.logger, "Failed to build payer proof for {}: {:?}", payment_id, e); + Error::PayerProofCreationFailed + })?; + + Ok(maybe_wrap(proof)) + } + /// Returns a payable offer that can be used to request and receive a payment of the amount /// given. pub fn receive( @@ -498,6 +620,7 @@ impl Bolt12Payment { secret: None, payer_note: payer_note.map(|note| UntrustedString(note)), quantity, + bolt12_invoice: None, }; let payment = PaymentDetails::new( payment_id, diff --git a/src/payment/mod.rs b/src/payment/mod.rs index fd75322ce..1ac6103be 100644 --- a/src/payment/mod.rs +++ b/src/payment/mod.rs @@ -18,7 +18,7 @@ mod unified; pub use bolt11::Bolt11Payment; pub(crate) use bolt11::PaymentMetadata; -pub use bolt12::Bolt12Payment; +pub use bolt12::{Bolt12Payment, PayerProofOptions}; pub use onchain::OnchainPayment; pub(crate) use pending_payment_store::{FundingTxCandidate, PendingPaymentDetails}; pub use spontaneous::SpontaneousPayment; diff --git a/src/payment/store.rs b/src/payment/store.rs index 0b2940205..0544bfe5e 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -10,6 +10,8 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH}; use bitcoin::secp256k1::PublicKey; use bitcoin::{BlockHash, Txid}; use lightning::chain::chaininterface::TransactionType as LdkTransactionType; +#[cfg(not(feature = "uniffi"))] +use lightning::events::PaidBolt12Invoice; use lightning::ln::channelmanager::PaymentId; use lightning::ln::msgs::DecodeError; use lightning::ln::types::ChannelId; @@ -23,6 +25,8 @@ use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; use lightning_types::string::UntrustedString; use crate::data_store::{StorableObject, StorableObjectId, StorableObjectUpdate}; +#[cfg(feature = "uniffi")] +use crate::ffi::PaidBolt12Invoice; use crate::hex_utils; /// Represents a payment. @@ -251,6 +255,18 @@ impl StorableObject for PaymentDetails { update_if_necessary!(self.fee_paid_msat, fee_paid_msat_opt); } + if let Some(ref bolt12_invoice_opt) = update.bolt12_invoice { + match self.kind { + PaymentKind::Bolt12Offer { ref mut bolt12_invoice, .. } => { + update_if_necessary!(*bolt12_invoice, bolt12_invoice_opt.clone()); + }, + PaymentKind::Bolt12Refund { ref mut bolt12_invoice, .. } => { + update_if_necessary!(*bolt12_invoice, bolt12_invoice_opt.clone()); + }, + _ => {}, + } + } + if let Some(skimmed_fee_msat) = update.counterparty_skimmed_fee_msat { match self.kind { PaymentKind::Bolt11 { ref mut counterparty_skimmed_fee_msat, .. } => { @@ -559,6 +575,15 @@ pub enum PaymentKind { /// /// This will always be `None` for payments serialized with version `v0.3.0`. quantity: Option, + /// The BOLT 12 invoice that was paid, set once the payment succeeded. + /// + /// Only set for successful outbound payments, allowing to build a payer proof for the + /// payment via [`Bolt12Payment::create_payer_proof`]. + /// + /// This will always be `None` for payments serialized with version `v0.7.0` or prior. + /// + /// [`Bolt12Payment::create_payer_proof`]: crate::payment::Bolt12Payment::create_payer_proof + bolt12_invoice: Option, }, /// A [BOLT 12] 'refund' payment, i.e., a payment for a [`Refund`]. /// @@ -586,6 +611,15 @@ pub enum PaymentKind { /// /// This will always be `None` for payments serialized with version `v0.3.0`. quantity: Option, + /// The BOLT 12 invoice that was paid, set once the payment succeeded. + /// + /// Only set for successful outbound payments, allowing to build a payer proof for the + /// payment via [`Bolt12Payment::create_payer_proof`]. + /// + /// This will always be `None` for payments serialized with version `v0.7.0` or prior. + /// + /// [`Bolt12Payment::create_payer_proof`]: crate::payment::Bolt12Payment::create_payer_proof + bolt12_invoice: Option, }, /// A spontaneous ("keysend") payment. Spontaneous { @@ -625,6 +659,7 @@ impl_writeable_tlv_based_enum!(PaymentKind, (3, quantity, option), (4, secret, option), (6, offer_id, required), + (8, bolt12_invoice, option), }, (8, Spontaneous) => { (0, hash, required), @@ -636,6 +671,7 @@ impl_writeable_tlv_based_enum!(PaymentKind, (2, preimage, option), (3, quantity, option), (4, secret, option), + (6, bolt12_invoice, option), } ); @@ -698,6 +734,7 @@ pub(crate) struct PaymentDetailsUpdate { pub direction: Option, pub status: Option, pub confirmation_status: Option, + pub bolt12_invoice: Option>, pub txid: Option, pub tx_type: Option>, } @@ -715,6 +752,7 @@ impl PaymentDetailsUpdate { direction: None, status: None, confirmation_status: None, + bolt12_invoice: None, txid: None, tx_type: None, } @@ -723,12 +761,18 @@ impl PaymentDetailsUpdate { impl From<&PaymentDetails> for PaymentDetailsUpdate { fn from(value: &PaymentDetails) -> Self { - let (hash, preimage, secret) = match value.kind { - PaymentKind::Bolt11 { hash, preimage, secret, .. } => (Some(hash), preimage, secret), - PaymentKind::Bolt12Offer { hash, preimage, secret, .. } => (hash, preimage, secret), - PaymentKind::Bolt12Refund { hash, preimage, secret, .. } => (hash, preimage, secret), - PaymentKind::Spontaneous { hash, preimage, .. } => (Some(hash), preimage, None), - _ => (None, None, None), + let (hash, preimage, secret, bolt12_invoice) = match &value.kind { + PaymentKind::Bolt11 { hash, preimage, secret, .. } => { + (Some(*hash), *preimage, *secret, None) + }, + PaymentKind::Bolt12Offer { hash, preimage, secret, bolt12_invoice, .. } => { + (*hash, *preimage, *secret, Some(bolt12_invoice.clone())) + }, + PaymentKind::Bolt12Refund { hash, preimage, secret, bolt12_invoice, .. } => { + (*hash, *preimage, *secret, Some(bolt12_invoice.clone())) + }, + PaymentKind::Spontaneous { hash, preimage, .. } => (Some(*hash), *preimage, None, None), + _ => (None, None, None, None), }; let (confirmation_status, txid, tx_type) = match &value.kind { @@ -738,9 +782,9 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate { _ => (None, None, None), }; - let counterparty_skimmed_fee_msat = match value.kind { + let counterparty_skimmed_fee_msat = match &value.kind { PaymentKind::Bolt11 { counterparty_skimmed_fee_msat, .. } => { - Some(counterparty_skimmed_fee_msat) + Some(*counterparty_skimmed_fee_msat) }, _ => None, }; @@ -756,6 +800,7 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate { direction: Some(value.direction), status: Some(value.status), confirmation_status, + bolt12_invoice, txid, tx_type, } diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 7fe26509a..af8a5cc9f 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -38,8 +38,8 @@ use ldk_node::config::{ use ldk_node::entropy::NodeEntropy; use ldk_node::liquidity::LSPS2ServiceConfig; use ldk_node::payment::{ - ConfirmationStatus, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, - TransactionType, UnifiedPaymentResult, + ConfirmationStatus, PayerProofOptions, PaymentDetails, PaymentDirection, PaymentKind, + PaymentStatus, TransactionType, UnifiedPaymentResult, }; use ldk_node::{BuildError, Builder, Event, Node, NodeError, ReserveType}; use lightning::ln::channelmanager::PaymentId; @@ -2615,12 +2615,14 @@ async fn simple_bolt12_send_receive() { offer_id, quantity: ref qty, payer_note: ref note, + bolt12_invoice: ref invoice, } => { assert!(hash.is_some()); assert!(preimage.is_some()); assert_eq!(offer_id, offer.id()); assert_eq!(&expected_quantity, qty); assert_eq!(expected_payer_note.unwrap(), note.clone().unwrap().0); + assert!(invoice.is_some()); // TODO: We should eventually set and assert the secret sender-side, too, but the BOLT12 // API currently doesn't allow to do that. }, @@ -2630,6 +2632,27 @@ async fn simple_bolt12_send_receive() { } assert_eq!(node_a_payments.first().unwrap().amount_msat, Some(expected_amount_msat)); + // As we persist the paid invoice alongside the payment, the payer is able to build a payer + // proof for it after the fact. + let expected_proof_note = "Paid in full".to_string(); + let payer_proof_options = PayerProofOptions { + note: Some(expected_proof_note.clone()), + include_offer_description: true, + include_invoice_amount: true, + ..Default::default() + }; + let payer_proof = + node_a.bolt12_payment().create_payer_proof(&payment_id, Some(payer_proof_options)).unwrap(); + let expected_hash = match node_a_payments.first().unwrap().kind { + PaymentKind::Bolt12Offer { hash, .. } => hash.unwrap(), + _ => panic!("Unexpected payment kind"), + }; + assert_eq!(payer_proof.payment_hash(), expected_hash); + assert_eq!(payer_proof.invoice_amount_msats(), Some(expected_amount_msat)); + assert_eq!(payer_proof.proof_note().map(|n| n.to_string()), Some(expected_proof_note)); + assert!(payer_proof.offer_description().is_some()); + assert!(payer_proof.offer_issuer().is_none()); + expect_payment_received_event!(node_b, expected_amount_msat); let node_b_payments = node_b.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Bolt12Offer { .. })); @@ -2682,12 +2705,14 @@ async fn simple_bolt12_send_receive() { offer_id, quantity: ref qty, payer_note: ref note, + bolt12_invoice: ref invoice, } => { assert!(hash.is_some()); assert!(preimage.is_some()); assert_eq!(offer_id, offer.id()); assert_eq!(&expected_quantity, qty); assert_eq!(expected_payer_note.unwrap(), note.clone().unwrap().0); + assert!(invoice.is_some()); // TODO: We should eventually set and assert the secret sender-side, too, but the BOLT12 // API currently doesn't allow to do that. }, @@ -2753,11 +2778,13 @@ async fn simple_bolt12_send_receive() { secret: _, quantity: ref qty, payer_note: ref note, + bolt12_invoice: ref invoice, } => { assert!(hash.is_some()); assert!(preimage.is_some()); assert_eq!(&expected_quantity, qty); - assert_eq!(expected_payer_note.unwrap(), note.clone().unwrap().0) + assert_eq!(expected_payer_note.unwrap(), note.clone().unwrap().0); + assert!(invoice.is_some()); // TODO: We should eventually set and assert the secret sender-side, too, but the BOLT12 // API currently doesn't allow to do that. },