1use bitcoin::constants::ChainHash;
2pub use lightning::offers::invoice::Bolt12Invoice;
3pub use lightning_invoice::Bolt11Invoice;
4pub use lightning::offers::offer::{Amount as OfferAmount, Offer};
5
6use std::fmt;
7use std::borrow::Borrow;
8use std::str::FromStr;
9
10use bitcoin::{Amount, Network};
11use bitcoin::bech32::{encode_to_fmt, EncodeError, Hrp, NoChecksum, primitives::decode::CheckedHrpstring};
12use bitcoin::hashes::{sha256, Hash};
13use bitcoin::secp256k1::Message;
14use lightning::offers::parse::Bolt12ParseError;
15use lightning::util::ser::Writeable;
16
17use bitcoin_ext::{AmountExt, P2TR_DUST};
18
19use crate::SECP;
20
21const BECH32_BOLT12_INVOICE_HRP: &str = "lni";
22
23pub const HTLC_MIN_FEE: Amount = P2TR_DUST;
25
26pub const PREIMAGE_SIZE: usize = 32;
27pub const PAYMENT_HASH_SIZE: usize = 32;
28
29#[derive(Clone, Copy, PartialEq, Eq, Hash)]
31pub struct Preimage([u8; PREIMAGE_SIZE]);
32impl_byte_newtype!(Preimage, PREIMAGE_SIZE);
33
34impl Preimage {
35 pub fn random() -> Preimage {
37 Preimage(rand::random())
38 }
39
40 pub fn compute_payment_hash(&self) -> PaymentHash {
42 sha256::Hash::hash(self.as_ref()).into()
43 }
44}
45
46#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
48pub struct PaymentHash([u8; PAYMENT_HASH_SIZE]);
49impl_byte_newtype!(PaymentHash, PAYMENT_HASH_SIZE);
50
51impl From<sha256::Hash> for PaymentHash {
52 fn from(hash: sha256::Hash) -> Self {
53 PaymentHash(hash.to_byte_array())
54 }
55}
56
57impl From<Preimage> for PaymentHash {
58 fn from(preimage: Preimage) -> Self {
59 preimage.compute_payment_hash()
60 }
61}
62
63impl From<lightning::types::payment::PaymentHash> for PaymentHash {
64 fn from(hash: lightning::types::payment::PaymentHash) -> Self {
65 PaymentHash(hash.0)
66 }
67}
68
69impl<'a> From<&'a Bolt11Invoice> for PaymentHash {
70 fn from(i: &'a Bolt11Invoice) -> Self {
71 (*i.payment_hash()).into()
72 }
73}
74
75impl From<Bolt11Invoice> for PaymentHash {
76 fn from(i: Bolt11Invoice) -> Self {
77 (&i).into()
78 }
79}
80
81impl PaymentHash {
82 pub fn to_sha256_hash(&self) -> sha256::Hash {
84 sha256::Hash::from_byte_array(self.0)
85 }
86}
87
88pub trait AsPaymentHash {
90 fn as_payment_hash(&self) -> PaymentHash;
93}
94
95impl AsPaymentHash for PaymentHash {
96 fn as_payment_hash(&self) -> PaymentHash { *self }
97}
98
99impl AsPaymentHash for Preimage {
100 fn as_payment_hash(&self) -> PaymentHash { self.compute_payment_hash() }
101}
102
103impl AsPaymentHash for Bolt11Invoice {
104 fn as_payment_hash(&self) -> PaymentHash { PaymentHash::from(*self.payment_hash()) }
105}
106
107impl AsPaymentHash for Bolt12Invoice {
108 fn as_payment_hash(&self) -> PaymentHash { self.payment_hash().into() }
109}
110
111impl AsPaymentHash for Invoice {
112 fn as_payment_hash(&self) -> PaymentHash {
113 match self {
114 Invoice::Bolt11(i) => AsPaymentHash::as_payment_hash(i),
115 Invoice::Bolt12(i) => AsPaymentHash::as_payment_hash(i),
116 }
117 }
118}
119
120impl<'a, T: AsPaymentHash> AsPaymentHash for &'a T {
121 fn as_payment_hash(&self) -> PaymentHash {
122 AsPaymentHash::as_payment_hash(*self)
123 }
124}
125
126#[derive(Debug, Clone)]
127pub enum PaymentStatus {
128 Pending,
129 Success(Preimage),
130 Failed,
131}
132
133impl fmt::Display for PaymentStatus {
134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135 fmt::Debug::fmt(self, f)
136 }
137}
138
139#[derive(Debug, Clone, PartialEq, Eq, Hash)]
141pub enum Invoice {
142 Bolt11(Bolt11Invoice),
143 Bolt12(Bolt12Invoice),
144}
145
146#[derive(Debug, thiserror::Error)]
147#[error("cannot parse invoice")]
148pub struct InvoiceParseError;
149
150impl FromStr for Invoice {
151 type Err = InvoiceParseError;
152
153 fn from_str(s: &str) -> Result<Self, Self::Err> {
154 if let Ok(bolt11) = Bolt11Invoice::from_str(s) {
155 Ok(Invoice::Bolt11(bolt11))
156 } else if let Ok(bolt12) = Bolt12Invoice::from_str(s) {
157 Ok(Invoice::Bolt12(bolt12))
158 } else {
159 Err(InvoiceParseError)
160 }
161 }
162}
163
164impl From<Bolt11Invoice> for Invoice {
165 fn from(invoice: Bolt11Invoice) -> Self {
166 Invoice::Bolt11(invoice)
167 }
168}
169
170impl From<Bolt12Invoice> for Invoice {
171 fn from(invoice: Bolt12Invoice) -> Self {
172 Invoice::Bolt12(invoice)
173 }
174}
175
176impl<'a> TryFrom<&'a str> for Invoice {
177 type Error = <Invoice as FromStr>::Err;
178 fn try_from(invoice: &'a str) -> Result<Self, Self::Error> {
179 FromStr::from_str(invoice)
180 }
181}
182
183impl TryFrom<String> for Invoice {
184 type Error = <Invoice as FromStr>::Err;
185 fn try_from(invoice: String) -> Result<Self, Self::Error> {
186 FromStr::from_str(&invoice)
187 }
188}
189
190impl serde::Serialize for Invoice {
191 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
192 s.collect_str(self)
193 }
194}
195
196impl<'de> serde::Deserialize<'de> for Invoice {
197 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
198 struct Visitor;
199 impl<'de> serde::de::Visitor<'de> for Visitor {
200 type Value = Invoice;
201 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202 write!(f, "a lightning invoice")
203 }
204 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
205 Invoice::from_str(v).map_err(serde::de::Error::custom)
206 }
207 }
208 d.deserialize_str(Visitor)
209 }
210}
211
212
213#[derive(Debug, thiserror::Error)]
214#[error("invoice amount mismatch: invoice={invoice}, user={user}")]
215pub enum CheckAmountError {
216 #[error("invalid user amount: invoice={invoice}, user={user}")]
217 InvalidUserAmount { invoice: Amount, user: Amount },
218 #[error("offer currency is not supported: {amount:?}")]
219 UnsupportedCurrency { amount: OfferAmount },
220 #[error("user amount required")]
221 UserAmountRequired,
222}
223
224#[derive(Debug, thiserror::Error)]
225#[error("invalid invoice signature: {0}")]
226pub struct CheckSignatureError(pub String);
227
228#[derive(Debug, thiserror::Error)]
231pub enum ValidateIssuanceError {
232 #[error(transparent)]
233 Signature(#[from] CheckSignatureError),
234 #[error("invoice amount doesn't match the requested amount: \
235 invoice={invoice_msat} msat, requested={requested_msat} msat")]
236 AmountMismatch { invoice_msat: u64, requested_msat: u64 },
237}
238
239impl Invoice {
240 pub fn into_bolt11(self) -> Option<Bolt11Invoice> {
241 match self {
242 Invoice::Bolt11(invoice) => Some(invoice),
243 Invoice::Bolt12(_) => None
244 }
245 }
246
247 pub fn payment_hash(&self) -> PaymentHash {
248 match self {
249 Invoice::Bolt11(invoice) => PaymentHash::from(*invoice.payment_hash().as_byte_array()),
250 Invoice::Bolt12(invoice) => PaymentHash::from(invoice.payment_hash()),
251 }
252 }
253
254 pub fn network(&self) -> Network {
255 match self {
256 Invoice::Bolt11(invoice) => invoice.network(),
257 Invoice::Bolt12(invoice) => match invoice.chain() {
258 ChainHash::BITCOIN => Network::Bitcoin,
259 ChainHash::TESTNET3 => Network::Testnet,
260 ChainHash::TESTNET4 => Network::Testnet4,
261 ChainHash::SIGNET => Network::Signet,
262 ChainHash::REGTEST => Network::Regtest,
263 _ => panic!("unsupported network"),
264 },
265 }
266 }
267
268 pub fn get_payment_amount(
272 &self,
273 user_amount: Option<Amount>,
274 ) -> Result<Amount, CheckAmountError> {
275 match self {
276 Invoice::Bolt11(invoice) => invoice.get_payment_amount(user_amount),
277 Invoice::Bolt12(invoice) => invoice.get_payment_amount(user_amount),
278 }
279 }
280
281 pub fn amount_msat(&self) -> Option<u64> {
282 match self {
283 Invoice::Bolt11(invoice) => invoice.amount_milli_satoshis(),
284 Invoice::Bolt12(invoice) => Some(invoice.amount_msats()),
285 }
286 }
287
288 pub fn check_signature(&self) -> Result<(), CheckSignatureError> {
289 match self {
290 Invoice::Bolt11(invoice) => invoice
291 .check_signature()
292 .map_err(|e| CheckSignatureError(e.to_string())),
293 Invoice::Bolt12(invoice) => invoice.check_signature(),
294 }
295 }
296}
297
298impl fmt::Display for Invoice {
299 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
300 match self {
301 Invoice::Bolt11(invoice) => write!(f, "{}", invoice.to_string()),
302 Invoice::Bolt12(invoice) => encode_to_fmt::<NoChecksum, _>(
303 f,
304 Hrp::parse("lni").unwrap(),
305 &invoice.bytes(),
306 )
307 .map_err(|e| match e {
308 EncodeError::Fmt(e) => e,
309 _ => fmt::Error {},
310 }),
311 }
312 }
313}
314
315fn get_invoice_payment_amount(invoice_amount: Option<Amount>, user_amount: Option<Amount>) -> Result<Amount, CheckAmountError> {
319 match (invoice_amount, user_amount) {
320 (Some(invoice_amount), Some(user_amount)) => {
321 if user_amount >= invoice_amount && user_amount <= invoice_amount * 2 {
324 return Ok(user_amount);
325 }
326
327 return Err(CheckAmountError::InvalidUserAmount {
328 invoice: invoice_amount,
329 user: user_amount,
330 });
331 }
332 (Some(invoice_amount), None) => {
333 return Ok(invoice_amount);
334 }
335 (None, Some(user_amount)) => {
336 return Ok(user_amount);
337 }
338 (None, None) => {
339 return Err(CheckAmountError::UserAmountRequired);
340 }
341 }
342}
343
344pub fn offer_request_amount(
347 offer: &Offer,
348 user_amount: Option<Amount>,
349) -> Result<Amount, CheckAmountError> {
350 if let Some(user_amount) = user_amount {
351 return Ok(user_amount);
352 }
353
354 match offer.amount() {
355 Some(amount) => amount.to_bitcoin_amount()
358 .ok_or(CheckAmountError::UnsupportedCurrency { amount }),
359 None => Err(CheckAmountError::UserAmountRequired),
360 }
361}
362
363pub trait Bolt11InvoiceExt: Borrow<Bolt11Invoice> {
365 fn get_payment_amount(&self, user_amount: Option<Amount>) -> Result<Amount, CheckAmountError> {
369 let invoice_amount = self.borrow().amount_milli_satoshis()
370 .map(Amount::from_msat_ceil);
371
372 get_invoice_payment_amount(invoice_amount, user_amount)
373 }
374}
375
376impl Bolt11InvoiceExt for Bolt11Invoice {}
377
378pub trait Bolt12InvoiceExt: Borrow<Bolt12Invoice> {
380 fn payment_hash(&self) -> PaymentHash { PaymentHash::from(self.borrow().payment_hash()) }
381
382 fn get_payment_amount(&self, user_amount: Option<Amount>) -> Result<Amount, CheckAmountError> {
386 let invoice_amount = Amount::from_msat_ceil(self.borrow().amount_msats());
387 get_invoice_payment_amount(Some(invoice_amount), user_amount)
388 }
389
390 fn bytes(&self) -> Vec<u8> {
391 let mut bytes = Vec::new();
392 self.borrow().write(&mut bytes).expect("Writing into a Vec is infallible");
393 bytes
394 }
395
396 fn from_bytes(bytes: &[u8]) -> Result<Bolt12Invoice, Bolt12ParseError> {
397 Bolt12Invoice::try_from(bytes.to_vec())
398 }
399
400 fn from_str(s: &str) -> Result<Bolt12Invoice, Bolt12ParseError> {
401 let dec = CheckedHrpstring::new::<NoChecksum>(&s)?;
402 if dec.hrp().to_lowercase() != BECH32_BOLT12_INVOICE_HRP {
403 return Err(Bolt12ParseError::InvalidBech32Hrp);
404 }
405
406 let data = dec.byte_iter().collect::<Vec<_>>();
407 Bolt12Invoice::try_from(data)
408 }
409
410 fn check_signature(&self) -> Result<(), CheckSignatureError> {
415 let message = Message::from_digest(self.borrow().signable_hash());
416 let signature = self.borrow().signature();
417
418 let pubkey = self.borrow().signing_pubkey();
419 SECP.verify_schnorr(&signature, &message, &pubkey.into())
420 .map_err(|_| CheckSignatureError("invalid signature".to_string()))
421 }
422
423 fn validate_issuance(&self, offer: &Offer, requested_amount: Amount)
428 -> Result<(), ValidateIssuanceError>
429 {
430 self.validate_signing_key(offer)?;
431
432 let invoice_msat = self.borrow().amount_msats();
435 let requested_msat = requested_amount.to_msat();
436 if invoice_msat != requested_msat {
437 return Err(ValidateIssuanceError::AmountMismatch { invoice_msat, requested_msat });
438 }
439
440 Ok(())
441 }
442
443 fn validate_signing_key(&self, offer: &Offer) -> Result<(), CheckSignatureError> {
448 if let Some(issuer_signing_pubkey) = offer.issuer_signing_pubkey() {
449 if issuer_signing_pubkey != self.borrow().signing_pubkey() {
450 return Err(CheckSignatureError("public keys mismatch".to_string()));
451 }
452
453 self.check_signature()
454 } else {
455 for offer_path in offer.paths() {
456 let final_hop_pk = offer_path.blinded_hops().last()
457 .map(|hop| hop.blinded_node_id);
458
459 match final_hop_pk {
460 Some(final_hop_pk) if final_hop_pk == self.borrow().signing_pubkey() => {
461 return self.check_signature();
462 }
463 _ => {}
464 }
465 }
466
467 Err(CheckSignatureError("public keys mismatch".to_string()))
468 }
469 }
470}
471
472impl Bolt12InvoiceExt for Bolt12Invoice {}
473
474pub trait OfferAmountExt: Borrow<OfferAmount> {
475 fn to_bitcoin_amount(&self) -> Option<Amount> {
476 match self.borrow() {
477 OfferAmount::Bitcoin { amount_msats } => Some(Amount::from_msat_ceil(*amount_msats)),
478 OfferAmount::Currency { .. } => None,
479 }
480 }
481}
482
483impl OfferAmountExt for OfferAmount {}
484
485#[cfg(test)]
486mod test {
487 use super::*;
488
489 use hex_conservative::FromHex;
490 use bitcoin::secp256k1::{Keypair, Secp256k1, SecretKey};
491 use lightning::blinded_path::BlindedHop;
492 use lightning::blinded_path::payment::{BlindedPayInfo, BlindedPaymentPath};
493 use lightning::ln::channelmanager::PaymentId;
494 use lightning::ln::inbound_payment::ExpandedKey;
495 use lightning::offers::nonce::Nonce;
496 use lightning::offers::invoice_request::InvoiceRequest;
497 use lightning::offers::offer::{CurrencyCode, OfferBuilder};
498 use lightning::sign::EntropySource;
499 use lightning::types::features::BlindedHopFeatures;
500
501 struct FixedEntropy;
502
503 impl EntropySource for FixedEntropy {
504 fn get_secure_random_bytes(&self) -> [u8; 32] { [42; 32] }
505 }
506
507 fn pubkey(byte: u8) -> bitcoin::secp256k1::PublicKey {
508 let secp = Secp256k1::new();
509 bitcoin::secp256k1::PublicKey::from_secret_key(
510 &secp,
511 &SecretKey::from_slice(&[byte; 32]).unwrap(),
512 )
513 }
514
515 fn payment_paths() -> Vec<BlindedPaymentPath> {
516 vec![BlindedPaymentPath::from_blinded_path_and_payinfo(
517 pubkey(40),
518 pubkey(41),
519 vec![
520 BlindedHop { blinded_node_id: pubkey(43), encrypted_payload: vec![0; 43] },
521 BlindedHop { blinded_node_id: pubkey(44), encrypted_payload: vec![0; 44] },
522 ],
523 BlindedPayInfo {
524 fee_base_msat: 1,
525 fee_proportional_millionths: 1_000,
526 cltv_expiry_delta: 42,
527 htlc_minimum_msat: 100,
528 htlc_maximum_msat: 1_000_000_000_000,
529 features: BlindedHopFeatures::empty(),
530 },
531 )]
532 }
533
534 #[test]
535 fn offer_with_signing_pubkey_validate_invoice() {
536 let secp = Secp256k1::new();
537 let expanded_key = ExpandedKey::new([42; 32]);
538 let entropy = FixedEntropy;
539 let nonce = Nonce::from_entropy_source(&entropy);
540
541 let recipient_keys = Keypair::from_secret_key(
543 &secp,
544 &SecretKey::from_slice(&[43; 32]).unwrap(),
545 );
546
547 let offer = OfferBuilder::new(recipient_keys.public_key())
549 .amount_msats(1_000_000)
550 .build()
551 .unwrap();
552
553 assert_eq!(offer.amount(), Some(OfferAmount::Bitcoin { amount_msats: 1_000_000 }));
554 assert_eq!(offer.issuer_signing_pubkey(), Some(recipient_keys.public_key()));
555
556 let payment_id = PaymentId([1; 32]);
558 let invoice_request = offer
559 .request_invoice(&expanded_key, nonce, &secp, payment_id)
560 .unwrap()
561 .build_and_sign()
562 .unwrap();
563
564 assert_eq!(invoice_request.issuer_signing_pubkey(), Some(recipient_keys.public_key()));
565
566 let payment_hash = lightning::types::payment::PaymentHash([99; 32]);
568 let unsigned_invoice = invoice_request
569 .respond_with(payment_paths(), payment_hash)
570 .unwrap()
571 .build()
572 .unwrap();
573
574 let invoice = unsigned_invoice
575 .sign(|msg: &lightning::offers::invoice::UnsignedBolt12Invoice| {
576 Ok(secp.sign_schnorr_no_aux_rand(msg.as_ref().as_digest(), &recipient_keys))
577 })
578 .unwrap();
579
580 assert_eq!(invoice.payment_hash(), payment_hash);
582 assert_eq!(invoice.amount_msats(), 1_000_000);
583 assert_eq!(
584 invoice.issuer_signing_pubkey(),
585 Some(recipient_keys.public_key()),
586 );
587
588 let amount = invoice.get_payment_amount(None).unwrap();
589 assert_eq!(amount, Amount::from_sat(1_000));
590
591 invoice.check_signature().unwrap();
592 invoice.validate_signing_key(&offer).unwrap();
593 invoice.validate_issuance(&offer, Amount::from_sat(1_000)).unwrap();
594
595 let err = invoice.validate_issuance(&offer, Amount::from_sat(999)).unwrap_err();
598 assert!(matches!(err, ValidateIssuanceError::AmountMismatch {
599 invoice_msat: 1_000_000, requested_msat: 999_000,
600 }), "{:?}", err);
601 }
602
603 #[test]
604 fn offer_no_signing_pubkey_validate_invoice() {
605 let offer_str = "lno1pqpzwyq2qe3k7enxv4j3pjgrrwzv24nmzfjypx2a8m264ws9vht3uxp5vpypnluuzl67n4waq78syn2tdngnvypje2da9t4emyq25n29m84dszkfggehf3z35uj56pmxqgp5vfme44926w23gc282xn3pp0j7y8pc7je8e8qxrhmtwrjrnj4kzcqyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqjnrlnqdqf52q7jwgcnxgnuseav37nvs0zn06dyfs79hk7uk8lrxuqzqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq";
607 let invoice_request_hex = "00208f483020855be2127df9a1b25963afbb633c183d06d3223cf31942a059fb861b080227100a06636f6666656510c9031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d07660203462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b0020000000000000000000000000000000000000000000000000000000000000000002531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe3370020000000000000000000000000000000000000000000000000000000000000000052022710582103d084805e2f4c2bcf5188e40e7baec8b8680a1554da028b3d4c25e8969869fbe5f0401404f55082e4499ad85ac9cef739909f61243800ba31e2718bd5e40f08b05be22181ec91a4ccdf8c2cbb1feae62a62cda13ea069ca0134add34b215e6019bc33";
609 let invoice_hex = "00208f483020855be2127df9a1b25963afbb633c183d06d3223cf31942a059fb861b080227100a06636f6666656510c9031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d07660203462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b0020000000000000000000000000000000000000000000000000000000000000000002531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe3370020000000000000000000000000000000000000000000000000000000000000000052022710582103d084805e2f4c2bcf5188e40e7baec8b8680a1554da028b3d4c25e8969869fbe5a0c9031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d07660203462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b0020000000000000000000000000000000000000000000000000000000000000000002531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe33700200000000000000000000000000000000000000000000000000000000000000000a21c00000001000003e8002a0000000000000064000000e8d4a510000000a40469d570dfa820aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa022710b02102531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337f0408f1efd3aafd2b200bb740b16b2311487312da67520b9d2977335074e27db8c8cdba9ea1c45f89f1c345ace60c48c1cd8cc149c184851cbc58d8221be4794db7b";
611 let other_offer_invoice_hex = "00202aa648ba07b96455928d4908f851c9e7f4bc1c4b44896ffa952bd116db27873e080213880a0374656110c90362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f703f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a0203f991f944d1e1954a7fc8b9bf62e0d78f015f4c07762d505e20e6c45260a3661b0020000000000000000000000000000000000000000000000000000000000000000002989c0b76cb563971fdc9bef31ec06c3560f3249d6ee9e5d83c57625596e05f6f0020000000000000000000000000000000000000000000000000000000000000000052021388582103d9f3787be32e810bc0a72ebb252160b76da09c9adba45ee62aa94f048c8f12e1a0c90362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f703f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a0203f991f944d1e1954a7fc8b9bf62e0d78f015f4c07762d505e20e6c45260a3661b0020000000000000000000000000000000000000000000000000000000000000000002989c0b76cb563971fdc9bef31ec06c3560f3249d6ee9e5d83c57625596e05f6f00200000000000000000000000000000000000000000000000000000000000000000a21c00000001000003e8002a0000000000000064000000e8d4a510000000a40469d5f147a820bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaa021388b02102989c0b76cb563971fdc9bef31ec06c3560f3249d6ee9e5d83c57625596e05f6ff0407e645ece0602cddf966b5fcb99cf0829ca3952f5a2401d2c063be0e7895944d61409774afdea389c7486e5eadc74666347307f251444f9ea34f6eb2538848e65";
613 let extra_path_invoice_hex = "00208f483020855be2127df9a1b25963afbb633c183d06d3223cf31942a059fb861b080227100a06636f6666656510c9031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d07660203462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b0020000000000000000000000000000000000000000000000000000000000000000002531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe3370020000000000000000000000000000000000000000000000000000000000000000052022710582103d084805e2f4c2bcf5188e40e7baec8b8680a1554da028b3d4c25e8969869fbe5a0fd0192031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d07660203462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b0020000000000000000000000000000000000000000000000000000000000000000002531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe3370020000000000000000000000000000000000000000000000000000000000000000003ff8adab52623bcb2717fc71d7edc6f55e98396e6c234dff01f307a12b2af1c9903d793631af7aa0e709439dd47fc001acd0b0727670b6670ea528ac83cb0127f4a0202a8397a935f0dfceba6ba9618f6451ef4d80637abf4e6af2669fbc9de6a8fd2ac002000000000000000000000000000000000000000000000000000000000000000000257eb3638f51f4dc5c8d5a7324b47df99e816cfcc5b5eb1245bc8c98029f9e67400200000000000000000000000000000000000000000000000000000000000000000a23800000001000003e8002a0000000000000064000000e8d4a51000000000000001000003e8002a0000000000000064000000e8d4a510000000a40469d5f704a820aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa022710b02102531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337f04050faf92c0ac2aebd997ca25cee63f5b3186ef8a7e4dc977289f77130acb37894cc5fd48fb9875335f445b30359892f04954fe2b6623507aca7d403ebcc4ec938";
615
616 let offer = offer_str.parse::<Offer>().unwrap();
618 assert_eq!(offer.issuer_signing_pubkey(), None);
619 assert_eq!(offer.paths().len(), 1, "offer should have blinded paths");
620
621 let secp = Secp256k1::new();
623 let recipient_secret = SecretKey::from_slice(&[0x03; 32]).unwrap();
624 let recipient_keys = Keypair::from_secret_key(&secp, &recipient_secret);
625 assert_eq!(
626 recipient_keys.public_key().to_string(),
627 "02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337",
628 );
629
630 let invoice_request_bytes = Vec::from_hex(invoice_request_hex).unwrap();
632 let invoice_request = InvoiceRequest::try_from(invoice_request_bytes).unwrap();
633 assert_eq!(invoice_request.issuer_signing_pubkey(), None);
634 assert_eq!(invoice_request.paths().len(), 1, "offer should have blinded paths");
635
636 let invoice_bytes = Vec::from_hex(invoice_hex).unwrap();
638 let invoice = Bolt12Invoice::try_from(invoice_bytes).unwrap();
639
640 assert_eq!(invoice.amount_msats(), 10_000);
641 assert_eq!(invoice.payment_hash(), lightning::types::payment::PaymentHash([0xaa; 32]));
642
643 invoice.validate_issuance(&offer, Amount::from_sat(10)).unwrap();
645 invoice.check_signature().unwrap();
646
647 let invoice_bytes = Vec::from_hex(other_offer_invoice_hex).unwrap();
649 let invoice = Bolt12Invoice::try_from(invoice_bytes).unwrap();
650
651 let err = invoice.validate_issuance(&offer, Amount::from_sat(10)).unwrap_err();
652 assert!(err.to_string().contains("public keys mismatch"), "{:?}", err);
653
654 let invoice_bytes = Vec::from_hex(extra_path_invoice_hex).unwrap();
656 let invoice = Bolt12Invoice::try_from(invoice_bytes).unwrap();
657
658 invoice.validate_issuance(&offer, Amount::from_sat(10)).unwrap();
660 invoice.check_signature().unwrap();
661 }
662
663 #[test]
664 fn offer_request_amount_sources() {
665 let offer_str = "lno1pqpzwyq2qe3k7enxv4j3pjgrrwzv24nmzfjypx2a8m264ws9vht3uxp5vpypnluuzl67n4waq78syn2tdngnvypje2da9t4emyq25n29m84dszkfggehf3z35uj56pmxqgp5vfme44926w23gc282xn3pp0j7y8pc7je8e8qxrhmtwrjrnj4kzcqyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqjnrlnqdqf52q7jwgcnxgnuseav37nvs0zn06dyfs79hk7uk8lrxuqzqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq";
666 let offer = offer_str.parse::<Offer>().unwrap();
667 assert_eq!(offer.amount(), Some(OfferAmount::Bitcoin { amount_msats: 10_000 }));
668
669 assert_eq!(
671 offer_request_amount(&offer, Some(Amount::from_sat(42))).unwrap(),
672 Amount::from_sat(42),
673 );
674
675 assert_eq!(offer_request_amount(&offer, None).unwrap(), Amount::from_sat(10));
677
678 let amountless = OfferBuilder::new(pubkey(43)).build().unwrap();
680 assert!(matches!(
681 offer_request_amount(&amountless, None),
682 Err(CheckAmountError::UserAmountRequired),
683 ));
684 assert_eq!(
685 offer_request_amount(&amountless, Some(Amount::from_sat(42))).unwrap(),
686 Amount::from_sat(42),
687 );
688 }
689
690 #[test]
691 fn offer_amount_ext_to_bitcoin_amount() {
692 let btc = OfferAmount::Bitcoin { amount_msats: 1_500 };
694 assert_eq!(btc.to_bitcoin_amount(), Some(Amount::from_sat(2)));
695
696 let btc_exact = OfferAmount::Bitcoin { amount_msats: 2_000 };
697 assert_eq!(btc_exact.to_bitcoin_amount(), Some(Amount::from_sat(2)));
698
699 let usd = CurrencyCode::from_str("USD").unwrap();
701 let currency = OfferAmount::Currency { iso4217_code: usd, amount: 100 };
702 assert_eq!(currency.to_bitcoin_amount(), None);
703 }
704}