Skip to content

Commit 03b14d4

Browse files
committed
Add tests for currency-denominated Offer flows
Adds tests covering Offers whose amounts are denominated in fiat currencies. These tests verify that: * currency-denominated Offer amounts can be created * InvoiceRequests correctly resolve amounts using CurrencyConversion * Invoice construction validates and enforces the payable amount This ensures the full Offer → InvoiceRequest → Invoice flow works correctly when the original Offer amount is specified in currency.
1 parent b2d37e7 commit 03b14d4

2 files changed

Lines changed: 122 additions & 1 deletion

File tree

lightning/src/ln/offers_tests.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ use crate::blinded_path::message::OffersContext;
5252
use crate::events::{ClosureReason, Event, HTLCHandlingFailureType, PaidBolt12Invoice, PaymentFailureReason, PaymentPurpose};
5353
use crate::ln::channelmanager::{PaymentId, RecentPaymentDetails, self};
5454
use crate::ln::outbound_payment::{Bolt12PaymentError, RecipientOnionFields, Retry};
55+
use crate::offers::offer::{Amount, CurrencyCode};
5556
use crate::types::features::Bolt12InvoiceFeatures;
5657
use crate::ln::functional_test_utils::*;
5758
use crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler, Init, NodeAnnouncement, OnionMessage, OnionMessageHandler, RoutingMessageHandler, SocketAddress, UnsignedGossipMessage, UnsignedNodeAnnouncement};
@@ -916,6 +917,78 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() {
916917
expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id);
917918
}
918919

920+
/// Checks that an offer can be paid through a one-hop blinded path and that ephemeral pubkeys are
921+
/// used rather than exposing a node's pubkey. However, the node's pubkey is still used as the
922+
/// introduction node of the blinded path.
923+
#[test]
924+
fn creates_and_pays_for_offer_with_fiat_amount_using_one_hop_blinded_path() {
925+
let chanmon_cfgs = create_chanmon_cfgs(2);
926+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
927+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
928+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
929+
930+
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000);
931+
932+
let alice = &nodes[0];
933+
let alice_id = alice.node.get_our_node_id();
934+
let bob = &nodes[1];
935+
let bob_id = bob.node.get_our_node_id();
936+
937+
let amount = Amount::Currency {
938+
iso4217_code: CurrencyCode::new(*b"USD").unwrap(),
939+
amount: 1000,
940+
};
941+
942+
let offer = alice.node
943+
.create_offer_builder().unwrap()
944+
.amount(amount, &alice.node.flow.currency_conversion).unwrap()
945+
.build();
946+
assert_ne!(offer.issuer_signing_pubkey(), Some(alice_id));
947+
assert!(!offer.paths().is_empty());
948+
for path in offer.paths() {
949+
assert!(check_compact_path_introduction_node(&path, bob, alice_id));
950+
}
951+
952+
let payment_id = PaymentId([1; 32]);
953+
bob.node.pay_for_offer(&offer, None, payment_id, Default::default()).unwrap();
954+
expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id);
955+
956+
let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap();
957+
alice.onion_messenger.handle_onion_message(bob_id, &onion_message);
958+
959+
let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message);
960+
let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext {
961+
offer_id: offer.id(),
962+
invoice_request: InvoiceRequestFields {
963+
payer_signing_pubkey: invoice_request.payer_signing_pubkey(),
964+
quantity: None,
965+
payer_note_truncated: None,
966+
human_readable_name: None,
967+
},
968+
});
969+
assert_eq!(invoice_request.amount_msats(&alice.node.flow.currency_conversion), Ok(1_000_000));
970+
assert_ne!(invoice_request.payer_signing_pubkey(), bob_id);
971+
assert!(check_dummy_hopped_path_length(&reply_path, alice, bob_id, DUMMY_HOPS_PATH_LENGTH));
972+
973+
let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap();
974+
bob.onion_messenger.handle_onion_message(alice_id, &onion_message);
975+
976+
let (invoice, reply_path) = extract_invoice(bob, &onion_message);
977+
assert_eq!(invoice.amount_msats(), 1_000_000);
978+
assert_ne!(invoice.signing_pubkey(), alice_id);
979+
assert!(!invoice.payment_paths().is_empty());
980+
for path in invoice.payment_paths() {
981+
assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(alice_id));
982+
}
983+
assert!(check_dummy_hopped_path_length(&reply_path, bob, alice_id, DUMMY_HOPS_PATH_LENGTH));
984+
985+
route_bolt12_payment(bob, &[alice], &invoice);
986+
expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id);
987+
988+
claim_bolt12_payment(bob, &[alice], payment_context, &invoice);
989+
expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id);
990+
}
991+
919992
/// Checks that a refund can be paid through a one-hop blinded path and that ephemeral pubkeys are
920993
/// used rather than exposing a node's pubkey. However, the node's pubkey is still used as the
921994
/// introduction node of the blinded path.

lightning/src/offers/invoice.rs

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1897,7 +1897,7 @@ mod tests {
18971897
use crate::offers::merkle::{self, SignError, SignatureTlvStreamRef, TaggedHash, TlvStream};
18981898
use crate::offers::nonce::Nonce;
18991899
use crate::offers::offer::{
1900-
Amount, ExperimentalOfferTlvStreamRef, OfferTlvStreamRef, Quantity,
1900+
Amount, CurrencyCode, ExperimentalOfferTlvStreamRef, OfferTlvStreamRef, Quantity,
19011901
};
19021902
use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError};
19031903
use crate::offers::payer::PayerTlvStreamRef;
@@ -2099,6 +2099,54 @@ mod tests {
20992099
}
21002100
}
21012101

2102+
#[test]
2103+
fn builds_invoice_for_fiat_offer() {
2104+
let expanded_key = ExpandedKey::new([42; 32]);
2105+
let entropy = FixedEntropy {};
2106+
let nonce = Nonce::from_entropy_source(&entropy);
2107+
let secp_ctx = Secp256k1::new();
2108+
let payment_id = PaymentId([1; 32]);
2109+
let conversion = TestCurrencyConversion;
2110+
let currency_amount =
2111+
Amount::Currency { iso4217_code: CurrencyCode::new(*b"USD").unwrap(), amount: 10 };
2112+
let expected_amount_msats = 10_000;
2113+
2114+
let payment_paths = payment_paths();
2115+
let payment_hash = payment_hash();
2116+
let now = now();
2117+
let unsigned_invoice = OfferBuilder::new(recipient_pubkey())
2118+
.amount(currency_amount, &conversion)
2119+
.unwrap()
2120+
.build()
2121+
.request_invoice(&expanded_key, nonce, &secp_ctx, payment_id, &conversion)
2122+
.unwrap()
2123+
.build_and_sign()
2124+
.unwrap()
2125+
.respond_with_no_std(&conversion, payment_paths.clone(), payment_hash, now)
2126+
.unwrap()
2127+
.build()
2128+
.unwrap();
2129+
2130+
let (_, offer_tlv_stream, _, invoice_tlv_stream, _, _, _) =
2131+
unsigned_invoice.contents.as_tlv_stream();
2132+
assert_eq!(unsigned_invoice.amount(), Some(currency_amount));
2133+
assert_eq!(offer_tlv_stream.currency, Some(b"USD"));
2134+
assert_eq!(offer_tlv_stream.amount, Some(10));
2135+
assert_eq!(invoice_tlv_stream.amount, Some(expected_amount_msats));
2136+
assert_eq!(unsigned_invoice.amount_msats(), expected_amount_msats);
2137+
assert_eq!(unsigned_invoice.payment_paths(), payment_paths.as_slice());
2138+
2139+
#[cfg(c_bindings)]
2140+
let mut unsigned_invoice = unsigned_invoice;
2141+
let invoice = unsigned_invoice.sign(recipient_sign).unwrap();
2142+
let (_, offer_tlv_stream, _, invoice_tlv_stream, _, _, _, _) = invoice.as_tlv_stream();
2143+
assert_eq!(invoice.amount(), Some(currency_amount));
2144+
assert_eq!(offer_tlv_stream.currency, Some(b"USD"));
2145+
assert_eq!(offer_tlv_stream.amount, Some(10));
2146+
assert_eq!(invoice_tlv_stream.amount, Some(expected_amount_msats));
2147+
assert_eq!(invoice.amount_msats(), expected_amount_msats);
2148+
}
2149+
21022150
#[test]
21032151
fn builds_invoice_for_refund_with_defaults() {
21042152
let payment_paths = payment_paths();

0 commit comments

Comments
 (0)