Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion bindings/ldk_node.udl
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ enum VssHeaderProviderError {

[Enum]
interface Event {
PaymentSuccessful(PaymentId? payment_id, PaymentHash payment_hash, PaymentPreimage? payment_preimage, u64? fee_paid_msat);
PaymentSuccessful(PaymentId? payment_id, PaymentHash payment_hash, PaymentPreimage? payment_preimage, u64? fee_paid_msat, PaidBolt12Invoice? bolt12_invoice);
PaymentFailed(PaymentId? payment_id, PaymentHash? payment_hash, PaymentFailureReason? reason);
PaymentReceived(PaymentId? payment_id, PaymentHash payment_hash, u64 amount_msat, sequence<CustomTlvRecord> custom_records);
PaymentClaimable(PaymentId payment_id, PaymentHash payment_hash, u64 claimable_amount_msat, u32? claim_deadline, sequence<CustomTlvRecord> custom_records);
Expand Down Expand Up @@ -850,6 +850,33 @@ interface Bolt12Invoice {
sequence<u8> encode();
};

interface StaticInvoice {
[Throws=NodeError, Name=from_str]
constructor([ByRef] string invoice_str);
OfferId offer_id();
boolean is_offer_expired();
PublicKey signing_pubkey();
PublicKey? issuer_signing_pubkey();
string? invoice_description();
string? issuer();
OfferAmount? amount();
sequence<u8> chain();
sequence<u8>? metadata();
u64? absolute_expiry_seconds();
sequence<u8> encode();
};

enum PaidBolt12InvoiceKind {
"Bolt12Invoice",
"StaticInvoice",
};

dictionary PaidBolt12Invoice {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this an

[Enum]
interface PaidBolt12Invoice {

instead?

PaidBolt12InvoiceKind kind;
Bolt12Invoice? bolt12_invoice;
StaticInvoice? static_invoice;
};

[Custom]
typedef string Txid;

Expand Down
22 changes: 21 additions & 1 deletion src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ use crate::payment::store::{
PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus,
};
use crate::runtime::Runtime;
use crate::types::{CustomTlvRecord, DynStore, OnionMessenger, PaymentStore, Sweeper, Wallet};
use crate::types::{
CustomTlvRecord, DynStore, OnionMessenger, PaidBolt12Invoice, PaymentStore, Sweeper, Wallet,
};
use crate::{
hex_utils, BumpTransactionEventHandler, ChannelManager, Error, Graph, PeerInfo, PeerStore,
UserChannelId,
Expand All @@ -75,6 +77,17 @@ pub enum Event {
payment_preimage: Option<PaymentPreimage>,
/// The total fee which was spent at intermediate hops in this payment.
fee_paid_msat: Option<u64>,
/// 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 [`crate::payment::PaidBolt12InvoiceKind::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<PaidBolt12Invoice>,
},
/// A sent payment has failed.
PaymentFailed {
Expand Down Expand Up @@ -264,6 +277,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),
Expand Down Expand Up @@ -1022,6 +1036,7 @@ where
payment_preimage,
payment_hash,
fee_paid_msat,
bolt12_invoice,
..
} => {
let payment_id = if let Some(id) = payment_id {
Expand Down Expand Up @@ -1062,11 +1077,16 @@ where
hex_utils::to_string(&payment_preimage.0)
);
});

// Convert LDK's PaidBolt12Invoice to our wrapped type.
let bolt12_invoice_wrapped = bolt12_invoice.map(PaidBolt12Invoice::from);

let event = Event::PaymentSuccessful {
payment_id: Some(payment_id),
payment_hash,
payment_preimage: Some(payment_preimage),
fee_paid_msat,
bolt12_invoice: bolt12_invoice_wrapped,
};

match self.event_queue.add_event(event).await {
Expand Down
204 changes: 203 additions & 1 deletion src/ffi/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,19 @@ 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;
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;
Expand Down Expand Up @@ -619,6 +622,205 @@ impl AsRef<LdkBolt12Invoice> for Bolt12Invoice {
}
}

/// A `StaticInvoice` is used for async payments where the recipient may be offline.
///
/// Unlike [`Bolt12Invoice`], a `StaticInvoice` does not support proof of payment
/// because the payment hash is not derived from a preimage known only to the recipient.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StaticInvoice {
pub(crate) inner: LdkStaticInvoice,
}

impl StaticInvoice {
pub fn from_str(invoice_str: &str) -> Result<Self, Error> {
invoice_str.parse()
}

/// Returns the [`OfferId`] of the underlying [`Offer`] this invoice corresponds to.
///
/// [`Offer`]: lightning::offers::offer::Offer
pub fn offer_id(&self) -> OfferId {
OfferId(self.inner.offer_id().0)
}

/// Whether the offer this invoice corresponds to has expired.
pub fn is_offer_expired(&self) -> bool {
self.inner.is_offer_expired()
}

/// A typically transient public key corresponding to the key used to sign the invoice.
pub fn signing_pubkey(&self) -> PublicKey {
self.inner.signing_pubkey()
}

/// The public key used by the recipient to sign invoices.
pub fn issuer_signing_pubkey(&self) -> Option<PublicKey> {
self.inner.issuer_signing_pubkey()
}

/// A complete description of the purpose of the originating offer.
pub fn invoice_description(&self) -> Option<String> {
self.inner.description().map(|printable| printable.to_string())
}

/// The issuer of the offer.
pub fn issuer(&self) -> Option<String> {
self.inner.issuer().map(|printable| printable.to_string())
}

/// The minimum amount required for a successful payment of a single item.
pub fn amount(&self) -> Option<OfferAmount> {
self.inner.amount().map(|amount| amount.into())
}

/// The chain that must be used when paying the invoice.
pub fn chain(&self) -> Vec<u8> {
self.inner.chain().to_bytes().to_vec()
}

/// Opaque bytes set by the originating [`Offer`].
///
/// [`Offer`]: lightning::offers::offer::Offer
pub fn metadata(&self) -> Option<Vec<u8>> {
self.inner.metadata().cloned()
}

/// Seconds since the Unix epoch when an invoice should no longer be requested.
///
/// If `None`, the offer does not expire.
pub fn absolute_expiry_seconds(&self) -> Option<u64> {
self.inner.absolute_expiry().map(|duration| duration.as_secs())
}

/// Writes `self` out to a `Vec<u8>`.
pub fn encode(&self) -> Vec<u8> {
self.inner.encode()
}
}

impl std::str::FromStr for StaticInvoice {
type Err = Error;

fn from_str(invoice_str: &str) -> Result<Self, Self::Err> {
if let Some(bytes_vec) = hex_utils::to_vec(invoice_str) {
if let Ok(invoice) = LdkStaticInvoice::try_from(bytes_vec) {
return Ok(StaticInvoice { inner: invoice });
}
}
Err(Error::InvalidInvoice)
}
}

impl From<LdkStaticInvoice> 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<LdkStaticInvoice> for StaticInvoice {
fn as_ref(&self) -> &LdkStaticInvoice {
self.deref()
}
}

/// The kind of [`PaidBolt12Invoice`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PaidBolt12InvoiceKind {
/// A standard BOLT12 invoice.
Bolt12Invoice,
/// A static invoice for async payments.
StaticInvoice,
}

/// Represents a BOLT12 invoice that was paid.
///
/// This is used in [`Event::PaymentSuccessful`] to provide proof of payment for BOLT12 payments.
///
/// [`Event::PaymentSuccessful`]: crate::Event::PaymentSuccessful
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PaidBolt12Invoice {
/// The kind of invoice that was paid.
pub kind: PaidBolt12InvoiceKind,
/// The paid BOLT12 invoice, if this is a regular BOLT12 invoice.
pub bolt12_invoice: Option<Arc<Bolt12Invoice>>,
/// The paid static invoice, if this is a static invoice (async payment).
pub static_invoice: Option<Arc<StaticInvoice>>,
}

impl From<LdkPaidBolt12Invoice> for PaidBolt12Invoice {
fn from(ldk_invoice: LdkPaidBolt12Invoice) -> Self {
match ldk_invoice {
LdkPaidBolt12Invoice::Bolt12Invoice(invoice) => PaidBolt12Invoice {
kind: PaidBolt12InvoiceKind::Bolt12Invoice,
bolt12_invoice: Some(Arc::new(Bolt12Invoice { inner: invoice })),
static_invoice: None,
},
LdkPaidBolt12Invoice::StaticInvoice(invoice) => PaidBolt12Invoice {
kind: PaidBolt12InvoiceKind::StaticInvoice,
bolt12_invoice: None,
static_invoice: Some(Arc::new(StaticInvoice { inner: invoice })),
},
}
}
}

impl Writeable for PaidBolt12Invoice {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), lightning::io::Error> {
match self.kind {
PaidBolt12InvoiceKind::Bolt12Invoice => {
0u8.write(writer)?;
if let Some(invoice) = &self.bolt12_invoice {
let bytes = invoice.inner.encode();
bytes.write(writer)?;
}
},
PaidBolt12InvoiceKind::StaticInvoice => {
1u8.write(writer)?;
if let Some(invoice) = &self.static_invoice {
let bytes = invoice.inner.encode();
bytes.write(writer)?;
}
},
}
Ok(())
}
}

impl Readable for PaidBolt12Invoice {
fn read<R: lightning::io::Read>(reader: &mut R) -> Result<Self, DecodeError> {
let tag: u8 = Readable::read(reader)?;
let bytes: Vec<u8> = Readable::read(reader)?;
match tag {
0 => {
let invoice =
LdkBolt12Invoice::try_from(bytes).map_err(|_| DecodeError::InvalidValue)?;
Ok(PaidBolt12Invoice {
kind: PaidBolt12InvoiceKind::Bolt12Invoice,
bolt12_invoice: Some(Arc::new(Bolt12Invoice { inner: invoice })),
static_invoice: None,
})
},
1 => {
let invoice =
LdkStaticInvoice::try_from(bytes).map_err(|_| DecodeError::InvalidValue)?;
Ok(PaidBolt12Invoice {
kind: PaidBolt12InvoiceKind::StaticInvoice,
bolt12_invoice: None,
static_invoice: Some(Arc::new(StaticInvoice { inner: invoice })),
})
},
_ => Err(DecodeError::InvalidValue),
}
}
}

impl UniffiCustomTypeConverter for OfferId {
type Builtin = String;

Expand Down
2 changes: 2 additions & 0 deletions src/payment/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ pub use store::{
ConfirmationStatus, LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus,
};
pub use unified_qr::{QrPaymentResult, UnifiedQrPayment};

pub use crate::types::{Bolt12Invoice, PaidBolt12Invoice, PaidBolt12InvoiceKind, StaticInvoice};
Loading