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
3 changes: 3 additions & 0 deletions bindings/ldk_node.udl
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,7 @@ enum VssHeaderProviderError {

[Enum]
interface Event {
// TODO: Add bolt12_invoice field once we upgrade to UniFFI 0.29+. See #757.
PaymentSuccessful(PaymentId? payment_id, PaymentHash payment_hash, PaymentPreimage? payment_preimage, u64? fee_paid_msat);
PaymentFailed(PaymentId? payment_id, PaymentHash? payment_hash, PaymentFailureReason? reason);
PaymentReceived(PaymentId? payment_id, PaymentHash payment_hash, u64 amount_msat, sequence<CustomTlvRecord> custom_records);
Expand Down Expand Up @@ -860,6 +861,8 @@ interface Bolt12Invoice {
sequence<u8> encode();
};

// TODO: Add StaticInvoice and PaidBolt12Invoice once we upgrade to UniFFI 0.29+. See #757.

[Custom]
typedef string Txid;

Expand Down
118 changes: 118 additions & 0 deletions src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ use crate::payment::store::{
PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus,
};
use crate::runtime::Runtime;
#[cfg(not(feature = "uniffi"))]
use crate::types::PaidBolt12Invoice;
use crate::types::{CustomTlvRecord, DynStore, OnionMessenger, PaymentStore, Sweeper, Wallet};
use crate::{
hex_utils, BumpTransactionEventHandler, ChannelManager, Error, Graph, PeerInfo, PeerStore,
Expand Down Expand Up @@ -75,6 +77,22 @@ 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 [`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.
///
/// This field is only available in non-UniFFI builds. See the module documentation for
/// more information.
// TODO: Expose in bindings once we upgrade to UniFFI 0.29+. See #757.
#[cfg(not(feature = "uniffi"))]
bolt12_invoice: Option<PaidBolt12Invoice>,
},
/// A sent payment has failed.
PaymentFailed {
Expand Down Expand Up @@ -258,6 +276,90 @@ pub enum Event {
},
}

// TODO: These two macros are duplicated because the `Event::PaymentSuccessful` variant has a
// different set of fields depending on the `uniffi` feature flag. The `bolt12_invoice` field
// only exists in non-UniFFI builds, and the macro generates code that references struct fields
// by name, so we can't use a single macro for both. The duplication can be removed once we
// upgrade to UniFFI 0.29+, which supports Object types in enum variants.
// See https://github.com/lightningdevkit/ldk-node/issues/757
//
// Note: The serialization formats are compatible - TLV tag 7 (bolt12_invoice) written by
// non-UniFFI builds is silently skipped by UniFFI builds (unknown optional TLV), and UniFFI
// builds write without tag 7, which non-UniFFI builds read as `bolt12_invoice: None`.
#[cfg(not(feature = "uniffi"))]
impl_writeable_tlv_based_enum!(Event,
(0, PaymentSuccessful) => {
(0, payment_hash, required),
(1, fee_paid_msat, option),
(3, payment_id, option),
(5, payment_preimage, option),
(7, bolt12_invoice, option),
},
(1, PaymentFailed) => {
(0, payment_hash, option),
(1, reason, upgradable_option),
(3, payment_id, option),
},
(2, PaymentReceived) => {
(0, payment_hash, required),
(1, payment_id, option),
(2, amount_msat, required),
(3, custom_records, optional_vec),
},
(3, ChannelReady) => {
(0, channel_id, required),
(1, counterparty_node_id, option),
(2, user_channel_id, required),
(3, funding_txo, option),
},
(4, ChannelPending) => {
(0, channel_id, required),
(2, user_channel_id, required),
(4, former_temporary_channel_id, required),
(6, counterparty_node_id, required),
(8, funding_txo, required),
},
(5, ChannelClosed) => {
(0, channel_id, required),
(1, counterparty_node_id, option),
(2, user_channel_id, required),
(3, reason, upgradable_option),
},
(6, PaymentClaimable) => {
(0, payment_hash, required),
(2, payment_id, required),
(4, claimable_amount_msat, required),
(6, claim_deadline, option),
(7, custom_records, optional_vec),
},
(7, PaymentForwarded) => {
(0, prev_channel_id, required),
(1, prev_node_id, option),
(2, next_channel_id, required),
(3, next_node_id, option),
(4, prev_user_channel_id, option),
(6, next_user_channel_id, option),
(8, total_fee_earned_msat, option),
(10, skimmed_fee_msat, option),
(12, claim_from_onchain_tx, required),
(14, outbound_amount_forwarded_msat, option),
},
(8, SplicePending) => {
(1, channel_id, required),
(3, counterparty_node_id, required),
(5, user_channel_id, required),
(7, new_funding_txo, required),
},
(9, SpliceFailed) => {
(1, channel_id, required),
(3, counterparty_node_id, required),
(5, user_channel_id, required),
(7, abandoned_funding_txo, option),
},
);

// UniFFI version of the macro - see the comment above for why this duplication exists.
#[cfg(feature = "uniffi")]
impl_writeable_tlv_based_enum!(Event,
(0, PaymentSuccessful) => {
(0, payment_hash, required),
Expand Down Expand Up @@ -1022,6 +1124,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 +1165,26 @@ where
hex_utils::to_string(&payment_preimage.0)
);
});

#[cfg(not(feature = "uniffi"))]
let event = Event::PaymentSuccessful {
payment_id: Some(payment_id),
payment_hash,
payment_preimage: Some(payment_preimage),
fee_paid_msat,
bolt12_invoice,
};

#[cfg(feature = "uniffi")]
let event = {
// bolt12_invoice not exposed in uniffi builds until UniFFI 0.29+
let _ = bolt12_invoice;
Event::PaymentSuccessful {
payment_id: Some(payment_id),
payment_hash,
payment_preimage: Some(payment_preimage),
fee_paid_msat,
}
};

match self.event_queue.add_event(event).await {
Expand Down
3 changes: 3 additions & 0 deletions src/payment/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ pub use store::{
ConfirmationStatus, LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus,
};
pub use unified::{UnifiedPayment, UnifiedPaymentResult};

#[cfg(not(feature = "uniffi"))]
pub use crate::types::PaidBolt12Invoice;
6 changes: 6 additions & 0 deletions src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -621,3 +621,9 @@ impl From<&(u64, Vec<u8>)> for CustomTlvRecord {
CustomTlvRecord { type_num: tlv.0, value: tlv.1.clone() }
}
}

// PaidBolt12Invoice is only exposed for non-UniFFI builds because UniFFI v0.28
// doesn't support Object types in enum variants. We re-export LDK's type directly.
// TODO: Expose in bindings once we upgrade to UniFFI 0.29+. See #757.
#[cfg(not(feature = "uniffi"))]
pub use lightning::events::PaidBolt12Invoice;
110 changes: 110 additions & 0 deletions tests/integration_tests_rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ use common::{
use ldk_node::config::{AsyncPaymentsRole, EsploraSyncConfig};
use ldk_node::entropy::NodeEntropy;
use ldk_node::liquidity::LSPS2ServiceConfig;
#[cfg(not(feature = "uniffi"))]
use ldk_node::payment::PaidBolt12Invoice;
use ldk_node::payment::{
ConfirmationStatus, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus,
UnifiedPaymentResult,
Expand Down Expand Up @@ -1305,6 +1307,114 @@ async fn simple_bolt12_send_receive() {
assert_eq!(node_a_payments.first().unwrap().amount_msat, Some(overpaid_amount));
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn bolt12_proof_of_payment() {
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
let chain_source = TestChainSource::Esplora(&electrsd);
let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false);

let address_a = node_a.onchain_payment().new_address().unwrap();
let premine_amount_sat = 5_000_000;
premine_and_distribute_funds(
&bitcoind.client,
&electrsd.client,
vec![address_a],
Amount::from_sat(premine_amount_sat),
)
.await;

node_a.sync_wallets().unwrap();
open_channel(&node_a, &node_b, 4_000_000, true, &electrsd).await;

generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;

node_a.sync_wallets().unwrap();
node_b.sync_wallets().unwrap();

expect_channel_ready_event!(node_a, node_b.node_id());
expect_channel_ready_event!(node_b, node_a.node_id());

// Sleep until we broadcasted a node announcement.
while node_b.status().latest_node_announcement_broadcast_timestamp.is_none() {
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}

// Sleep one more sec to make sure the node announcement propagates.
tokio::time::sleep(std::time::Duration::from_secs(1)).await;

let expected_amount_msat = 100_000_000;
let offer = node_b
.bolt12_payment()
.receive(expected_amount_msat, "proof of payment test", None, Some(1))
.unwrap();
let payment_id =
node_a.bolt12_payment().send(&offer, Some(1), Some("Test".to_string()), None).unwrap();

// Wait for payment and verify proof of payment
#[cfg(not(feature = "uniffi"))]
match node_a.next_event_async().await {
Event::PaymentSuccessful {
payment_id: event_payment_id,
payment_hash,
payment_preimage,
fee_paid_msat: _,
bolt12_invoice,
} => {
assert_eq!(event_payment_id, Some(payment_id));

// Verify proof of payment: sha256(preimage) == payment_hash
let preimage = payment_preimage.expect("preimage should be present");
let computed_hash = Sha256Hash::hash(&preimage.0);
assert_eq!(PaymentHash(computed_hash.to_byte_array()), payment_hash);

// Verify the BOLT12 invoice is present and contains the correct payment hash
let paid_invoice =
bolt12_invoice.expect("bolt12_invoice should be present for BOLT12 payments");
match paid_invoice {
PaidBolt12Invoice::Bolt12Invoice(invoice) => {
assert_eq!(invoice.payment_hash(), payment_hash);
assert_eq!(invoice.amount_msats(), expected_amount_msat);
},
PaidBolt12Invoice::StaticInvoice(_) => {
panic!("Expected Bolt12Invoice, got StaticInvoice");
},
}

node_a.event_handled().unwrap();
},
ref e => {
panic!("Unexpected event: {:?}", e);
},
}

// For UniFFI builds, bolt12_invoice is not available until UniFFI 0.29+
#[cfg(feature = "uniffi")]
match node_a.next_event_async().await {
Event::PaymentSuccessful {
payment_id: event_payment_id,
payment_hash,
payment_preimage,
fee_paid_msat: _,
} => {
assert_eq!(event_payment_id, Some(payment_id));

// Verify proof of payment: sha256(preimage) == payment_hash
let preimage = payment_preimage.expect("preimage should be present");
let computed_hash = Sha256Hash::hash(&preimage.0);
assert_eq!(PaymentHash(computed_hash.to_byte_array()), payment_hash);

// Note: bolt12_invoice verification skipped in UniFFI builds until UniFFI 0.29+

node_a.event_handled().unwrap();
},
ref e => {
panic!("Unexpected event: {:?}", e);
},
}

expect_payment_received_event!(node_b, expected_amount_msat);
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn async_payment() {
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
Expand Down
Loading