Skip to content
Closed
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions bindings/ldk_node.udl
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ enum NodeError {
"FeerateEstimationUpdateTimeout",
"WalletOperationFailed",
"WalletOperationTimeout",
"PayerProofCreationFailed",
"PayerProofUnavailable",
"OnchainTxSigningFailed",
"TxSyncFailed",
"TxSyncTimeout",
Expand Down Expand Up @@ -247,6 +249,7 @@ enum NodeError {
"LnurlAuthTimeout",
"InvalidLnurl",
"ChainSourceNotSupported",
"InvalidPayerProof",
};

typedef dictionary NodeStatus;
Expand Down
11 changes: 11 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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."),
Expand Down Expand Up @@ -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."),
}
}
}
Expand Down
10 changes: 9 additions & 1 deletion src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1129,6 +1129,7 @@ where
offer_id,
payer_note,
quantity,
bolt12_invoice: None,
};

let payment = PaymentDetails::new(
Expand Down Expand Up @@ -1178,6 +1179,7 @@ where
secret: Some(payment_secret),
payer_note: None,
quantity: None,
bolt12_invoice: None,
};

let payment = PaymentDetails::new(
Expand Down Expand Up @@ -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),
Expand All @@ -1352,6 +1355,7 @@ where
secret: Some(payment_secret),
payer_note: None,
quantity: None,
bolt12_invoice: None,
};
let update = PaymentDetailsUpdate {
preimage: Some(payment_preimage),
Expand Down Expand Up @@ -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)
};

Expand Down Expand Up @@ -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 {
Expand Down
111 changes: 110 additions & 1 deletion src/ffi/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<u8>) -> Result<Self, Error> {
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<u8> {
self.inner.invoice_signature().as_ref().to_vec()
}

/// The proof signature bytes.
pub fn proof_signature(&self) -> Vec<u8> {
self.inner.proof_signature().as_ref().to_vec()
}

/// The offer description, if it was disclosed in the proof.
pub fn offer_description(&self) -> Option<String> {
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<String> {
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<u64> {
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<u64> {
self.inner.invoice_created_at().map(|value| value.as_secs())
}

/// The optional note attached to the proof.
pub fn proof_note(&self) -> Option<String> {
self.inner.proof_note().map(|value| value.to_string())
}

/// The Merkle root committed to by the proof.
pub fn merkle_root(&self) -> Vec<u8> {
self.inner.merkle_root().to_byte_array().to_vec()
}

/// The raw TLV bytes of the proof.
pub fn bytes(&self) -> Vec<u8> {
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<LdkPayerProof> 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| {
Expand Down
Loading
Loading