diff --git a/src/event.rs b/src/event.rs index 6a5ceb025..c4949a5ac 100644 --- a/src/event.rs +++ b/src/event.rs @@ -15,6 +15,8 @@ use bitcoin::blockdata::locktime::absolute::LockTime; use bitcoin::secp256k1::PublicKey; use bitcoin::{Amount, OutPoint}; use lightning::events::bump_transaction::BumpTransactionEvent; +#[cfg(not(feature = "uniffi"))] +use lightning::events::PaidBolt12Invoice; use lightning::events::{ ClosureReason, Event as LdkEvent, FundingInfo, PaymentFailureReason, PaymentPurpose, ReplayEvent, @@ -37,6 +39,8 @@ use crate::config::{may_announce_channel, Config}; use crate::connection::ConnectionManager; use crate::data_store::DataStoreUpdateResult; use crate::fee_estimator::ConfirmationTarget; +#[cfg(feature = "uniffi")] +use crate::ffi::PaidBolt12Invoice; use crate::io::{ EVENT_QUEUE_PERSISTENCE_KEY, EVENT_QUEUE_PERSISTENCE_PRIMARY_NAMESPACE, EVENT_QUEUE_PERSISTENCE_SECONDARY_NAMESPACE, @@ -79,6 +83,17 @@ pub enum Event { payment_preimage: Option, /// The total fee which was spent at intermediate hops in this payment. fee_paid_msat: Option, + /// The BOLT12 invoice that was paid. + /// + /// This is useful for proof of payment. A third party can verify that the payment was made + /// by checking that the `payment_hash` in the invoice matches `sha256(payment_preimage)`. + /// + /// Will be `None` for non-BOLT12 payments. + /// + /// Note that static invoices (indicated by [`PaidBolt12Invoice::StaticInvoice`], used for + /// async payments) do not support proof of payment as the payment hash is not derived + /// from a preimage known only to the recipient. + bolt12_invoice: Option, }, /// A sent payment has failed. PaymentFailed { @@ -268,6 +283,7 @@ impl_writeable_tlv_based_enum!(Event, (1, fee_paid_msat, option), (3, payment_id, option), (5, payment_preimage, option), + (7, bolt12_invoice, option), }, (1, PaymentFailed) => { (0, payment_hash, option), @@ -1028,6 +1044,7 @@ where payment_preimage, payment_hash, fee_paid_msat, + bolt12_invoice, .. } => { let payment_id = if let Some(id) = payment_id { @@ -1073,6 +1090,7 @@ where payment_hash, payment_preimage: Some(payment_preimage), fee_paid_msat, + bolt12_invoice: bolt12_invoice.map(Into::into), }; match self.event_queue.add_event(event).await { diff --git a/src/ffi/types.rs b/src/ffi/types.rs index 8fe387078..cc7298cfa 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -10,6 +10,7 @@ // // Make sure to add any re-exported items that need to be used in uniffi below. +use std::collections::HashMap; use std::convert::TryInto; use std::ops::Deref; use std::str::FromStr; @@ -22,17 +23,20 @@ use bitcoin::hashes::Hash; use bitcoin::secp256k1::PublicKey; pub use bitcoin::{Address, BlockHash, FeeRate, 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::channelmanager::PaymentId; +use lightning::ln::msgs::DecodeError; 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::refund::Refund as LdkRefund; +use lightning::offers::static_invoice::StaticInvoice as LdkStaticInvoice; use lightning::onion_message::dns_resolution::HumanReadableName as LdkHumanReadableName; pub use lightning::routing::gossip::{NodeAlias, NodeId, RoutingFees}; pub use lightning::routing::router::RouteParametersConfig; -use lightning::util::ser::Writeable; +use lightning::util::ser::{Readable, Writeable, Writer}; use lightning_invoice::{Bolt11Invoice as LdkBolt11Invoice, Bolt11InvoiceDescriptionRef}; pub use lightning_invoice::{Description, SignedRawBolt11Invoice}; pub use lightning_liquidity::lsps0::ser::LSPSDateTime; @@ -41,10 +45,10 @@ pub use lightning_liquidity::lsps1::msgs::{ }; pub use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; pub use lightning_types::string::UntrustedString; -use std::collections::HashMap; - -use vss_client::headers::VssHeaderProvider as VssClientHeaderProvider; -use vss_client::headers::VssHeaderProviderError as VssClientHeaderProviderError; +use vss_client::headers::{ + VssHeaderProvider as VssClientHeaderProvider, + VssHeaderProviderError as VssClientHeaderProviderError, +}; /// Errors around providing headers for each VSS request. #[derive(Debug, uniffi::Error)] @@ -775,6 +779,95 @@ impl AsRef for Bolt12Invoice { } } +/// A static invoice used for async payments. +/// +/// Static invoices are a special type of BOLT12 invoice where proof of payment is not possible, +/// as the payment hash is not derived from a preimage known only to the recipient. +#[derive(Debug, Clone, PartialEq, Eq, uniffi::Object)] +pub struct StaticInvoice { + pub(crate) inner: LdkStaticInvoice, +} + +#[uniffi::export] +impl StaticInvoice { + /// The amount for a successful payment of the invoice, if specified. + pub fn amount(&self) -> Option { + self.inner.amount().map(|amount| amount.into()) + } +} + +impl From for StaticInvoice { + fn from(invoice: LdkStaticInvoice) -> Self { + StaticInvoice { inner: invoice } + } +} + +impl Deref for StaticInvoice { + type Target = LdkStaticInvoice; + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl AsRef for StaticInvoice { + fn as_ref(&self) -> &LdkStaticInvoice { + self.deref() + } +} + +/// The BOLT12 invoice that was paid, surfaced in [`Event::PaymentSuccessful`]. +/// +/// [`Event::PaymentSuccessful`]: crate::Event::PaymentSuccessful +#[derive(Debug, Clone, PartialEq, Eq, uniffi::Enum)] +pub enum PaidBolt12Invoice { + /// The BOLT12 invoice, allowing the user to perform proof of payment. + Bolt12(Arc), + /// The static invoice, used in async payments, where the user cannot perform proof of + /// payment. + Static(Arc), +} + +impl From for PaidBolt12Invoice { + fn from(ldk: LdkPaidBolt12Invoice) -> Self { + match ldk { + LdkPaidBolt12Invoice::Bolt12Invoice(invoice) => { + PaidBolt12Invoice::Bolt12(Arc::new(Bolt12Invoice::from(invoice))) + }, + LdkPaidBolt12Invoice::StaticInvoice(invoice) => { + PaidBolt12Invoice::Static(Arc::new(StaticInvoice::from(invoice))) + }, + } + } +} + +impl From for LdkPaidBolt12Invoice { + fn from(wrapper: PaidBolt12Invoice) -> Self { + match wrapper { + PaidBolt12Invoice::Bolt12(invoice) => { + LdkPaidBolt12Invoice::Bolt12Invoice(invoice.inner.clone()) + }, + PaidBolt12Invoice::Static(invoice) => { + LdkPaidBolt12Invoice::StaticInvoice(invoice.inner.clone()) + }, + } + } +} + +impl Writeable for PaidBolt12Invoice { + fn write(&self, w: &mut W) -> Result<(), lightning::io::Error> { + // TODO: Find way to avoid cloning invoice data. + let ldk_type: LdkPaidBolt12Invoice = self.clone().into(); + ldk_type.write(w) + } +} + +impl Readable for PaidBolt12Invoice { + fn read(r: &mut R) -> Result { + let ldk_type = LdkPaidBolt12Invoice::read(r)?; + Ok(ldk_type.into()) + } +} + uniffi::custom_type!(OfferId, String, { remote, try_lift: |val| { diff --git a/tests/common/mod.rs b/tests/common/mod.rs index ee9d267fe..7854a77f2 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -953,7 +953,17 @@ pub(crate) async fn do_channel_full_cycle( }); assert_eq!(inbound_payments_b.len(), 1); - expect_event!(node_a, PaymentSuccessful); + // Verify bolt12_invoice is None for BOLT11 payments + match node_a.next_event_async().await { + ref e @ Event::PaymentSuccessful { ref bolt12_invoice, .. } => { + println!("{} got event {:?}", node_a.node_id(), e); + assert!(bolt12_invoice.is_none(), "bolt12_invoice should be None for BOLT11 payments"); + node_a.event_handled().unwrap(); + }, + ref e => { + panic!("{} got unexpected event!: {:?}", std::stringify!(node_a), e); + }, + } expect_event!(node_b, PaymentReceived); assert_eq!(node_a.payment(&payment_id).unwrap().status, PaymentStatus::Succeeded); assert_eq!(node_a.payment(&payment_id).unwrap().direction, PaymentDirection::Outbound); diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index eea97efab..3fde52dc4 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -1089,7 +1089,19 @@ async fn simple_bolt12_send_receive() { .send(&offer, expected_quantity, expected_payer_note.clone(), None) .unwrap(); - expect_payment_successful_event!(node_a, Some(payment_id), None); + let event = node_a.next_event_async().await; + match event { + ref e @ Event::PaymentSuccessful { payment_id: ref evt_id, ref bolt12_invoice, .. } => { + println!("{} got event {:?}", node_a.node_id(), e); + assert_eq!(*evt_id, Some(payment_id)); + assert!( + bolt12_invoice.is_some(), + "bolt12_invoice should be present for BOLT12 payments" + ); + node_a.event_handled().unwrap(); + }, + ref e => panic!("{} got unexpected event!: {:?}", "node_a", e), + } let node_a_payments = node_a.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Bolt12Offer { .. })); assert_eq!(node_a_payments.len(), 1);