Skip to content

Commit fc2db89

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 bcad760 commit fc2db89

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
@@ -1894,7 +1894,7 @@ mod tests {
18941894
use crate::offers::merkle::{self, SignError, SignatureTlvStreamRef, TaggedHash, TlvStream};
18951895
use crate::offers::nonce::Nonce;
18961896
use crate::offers::offer::{
1897-
Amount, ExperimentalOfferTlvStreamRef, OfferTlvStreamRef, Quantity,
1897+
Amount, CurrencyCode, ExperimentalOfferTlvStreamRef, OfferTlvStreamRef, Quantity,
18981898
};
18991899
use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError};
19001900
use crate::offers::payer::PayerTlvStreamRef;
@@ -2096,6 +2096,54 @@ mod tests {
20962096
}
20972097
}
20982098

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

0 commit comments

Comments
 (0)