lightning_signer/
invoice.rs1use core::time::Duration;
2
3use bitcoin::hashes::Hash;
4use bitcoin::secp256k1::PublicKey;
5
6use lightning::types::payment::PaymentHash;
7
8pub use lightning::offers::invoice as bolt12;
9pub use lightning_invoice as bolt11;
10
11use crate::prelude::*;
12
13pub trait InvoiceAttributes {
15 fn invoice_hash(&self) -> [u8; 32];
17 fn payment_hash(&self) -> PaymentHash;
19 fn amount_milli_satoshis(&self) -> u64;
21 fn description(&self) -> Option<String>;
23 fn payee_pub_key(&self) -> PublicKey;
25 fn duration_since_epoch(&self) -> Duration;
27 fn expiry_duration(&self) -> Duration;
29}
30
31#[derive(Clone, Debug)]
33pub enum Invoice {
34 Bolt11(bolt11::Bolt11Invoice),
36 Bolt12(bolt12::Bolt12Invoice),
38}
39
40impl InvoiceAttributes for Invoice {
41 fn invoice_hash(&self) -> [u8; 32] {
42 match self {
43 Invoice::Bolt11(bolt11) => bolt11.signable_hash(),
44 Invoice::Bolt12(bolt12) => bolt12.signable_hash(),
45 }
46 }
47
48 fn payment_hash(&self) -> PaymentHash {
49 match self {
50 Invoice::Bolt11(bolt11) => PaymentHash(bolt11.payment_hash().to_byte_array()),
51 Invoice::Bolt12(bolt12) => bolt12.payment_hash(),
52 }
53 }
54
55 fn amount_milli_satoshis(&self) -> u64 {
56 match self {
57 Invoice::Bolt11(bolt11) => bolt11.amount_milli_satoshis().unwrap_or(0),
58 Invoice::Bolt12(bolt12) => bolt12.amount_msats(),
59 }
60 }
61
62 fn description(&self) -> Option<String> {
63 match self {
64 Invoice::Bolt11(bolt11) => match bolt11.description() {
65 bolt11::Bolt11InvoiceDescriptionRef::Direct(d) => Some(d.to_string()),
66 bolt11::Bolt11InvoiceDescriptionRef::Hash(h) => Some(format!("hash: {:?}", h)),
67 },
68 Invoice::Bolt12(bolt12) => bolt12.description().map(|d| d.to_string()),
69 }
70 }
71
72 fn payee_pub_key(&self) -> PublicKey {
73 match self {
74 Invoice::Bolt11(bolt11) => bolt11
75 .payee_pub_key()
76 .map(|p| p.clone())
77 .unwrap_or_else(|| bolt11.recover_payee_pub_key()),
78 Invoice::Bolt12(bolt12) => bolt12.signing_pubkey(),
79 }
80 }
81
82 fn duration_since_epoch(&self) -> Duration {
83 match self {
84 Invoice::Bolt11(bolt11) => bolt11.duration_since_epoch(),
85 Invoice::Bolt12(bolt12) => bolt12.created_at(),
86 }
87 }
88
89 fn expiry_duration(&self) -> Duration {
90 match self {
91 Invoice::Bolt11(bolt11) => bolt11.expiry_time(),
92 Invoice::Bolt12(bolt12) => bolt12.relative_expiry(),
93 }
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use core::str::FromStr;
100
101 use crate::invoice::{Invoice, InvoiceAttributes};
102 use crate::util::status::Code;
103
104 #[test]
105 fn test_bolt11_encoded() {
106 let invoice = Invoice::from_str("lnbc1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq9qrsgq357wnc5r2ueh7ck6q93dj32dlqnls087fxdwk8qakdyafkq3yap9us6v52vjjsrvywa6rt52cm9r9zqt8r2t7mlcwspyetp5h2tztugp9lfyql").expect("invoice");
112 assert_eq!(invoice.amount_milli_satoshis(), 0);
113 assert_eq!(
114 hex::encode(invoice.payment_hash().0),
115 "0001020304050607080900010203040506070809000102030405060708090102"
116 );
117 assert_eq!(
118 invoice.payee_pub_key().to_string(),
119 "03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad"
120 );
121 }
122
123 #[test]
124 fn test_bolt12_encoded() {
125 let invoice = Invoice::from_str("lni1qqgf8ene6trt4n9mmrejx50c6v30cq3qqc3xu3s3rg94nj40zfsy866mhu5vxne6tcej5878k2mneuvgjy8ssqgzpg9hx6tdwpkx2gr5v4ehg93pqdwjkyvjm7apxnssu4qgwhfkd67ghs6n6k48v6uqczgt88p6tky965pqqc3xu3s3rg94nj40zfsy866mhu5vxne6tcej5878k2mneuvgjy84sggravpsmwr0rxjdwzvj3ltcg95eklxftgfw8njx2dd3v9eat2k8q8g6pxqrt543ryklhgf5uy89gzr46dnwhj9ux5744fmxhqxqjzeecwja3pwsxz392f64zmwkh5p9hygu8gvt3lpfrn7ehs53d6ylasgcyppwdr6pqypde4glecqn4h2ydg7e56xq3n0p0jxzpw9v89qw7n9encppxqt037qqx2s4d5007pqgecutjv9x6gr793gqsc2svc9a2k3l62klfcny8ca8z60eptrhahvy9aypymralep23vvvkw3pcqqqqqqqqqqqqqqq2qqqqqqqqqqqqqwjfvkl43fqqqqqqzjqgepvjh02sg8u5wx8nat9vgux9cvr8fe9c337706k08xrnl03dmwaglxr46yglz4qzq4syyp462c3jt0m5y6wzrj5pp6axehtez7r20265antsrqfpvuu8fwcsh0sgzm7pttfeuz5snjhmks67afze5klpew503kn98x4zt24dcsurm9wch699ucgw9sh5ww85gu2fy598hdne0gp5msx0shu4kqqc9z6hhk7").expect("invoice");
128 assert_eq!(invoice.amount_milli_satoshis(), 2);
129 assert_eq!(
130 hex::encode(invoice.payment_hash().0),
131 "fca38c79f565623862e1833a725c463ef3f5679cc39fdf16eddd47cc3ae888f8"
132 );
133 assert_eq!(
134 invoice.payee_pub_key().to_string(),
135 "035d2b1192dfba134e10e540875d366ebc8bc353d5aa766b80c090b39c3a5d885d"
136 );
137 }
138
139 #[test]
140 fn test_bolt12_recurrence() {
142 assert_invalid_argument_err!(
145 Invoice::from_str("lni1qqg239qhp9zd4tnv4exlh74gmlq6yq3qqc3xu3s3rg94nj40zfsy866mhu5vxne6tcej5878k2mneuvgjy8ssqgppg88yetrw4e8y6twvus8getnwstzzq3dygmzpg6e53ll0aavg37gt3rvjg762vufygdqq4xprs0regcatydqyqpu2qsqvgnwgcg35z6ee2h3yczraddm72xrfua9uve2rlrm9deu7xyfzr6cyypc97ywxgyjmc72gxh466uf8lyr7akfmvtn4ye4efqpscxx8g5vzj26qzsfsq3dygmzpg6e53ll0aavg37gt3rvjg762vufygdqq4xprs0regcatypt8spm8wcafuwyh24nfkctvcxmyruamsljh638ec306na7327zutcpqt0vv5neq5504nacs6cy7c39atn2ldrtecldj36tjw8nq0z69e3jkqpj9n43mjrkctxjqg07amjelrlq0zyth3gv28cmju6eumg3pqqyqr2klccnuv2h6xnkymss284z2sy0sm7w5gwqqqqqqqqqqqqqqqzsqqqqqqqqqqqqr5jt9hav2gqqqqqq5szxgt8ndznqzwagyrehhzcerrnk2p5evccgrct2cdjhk5tyz02wa9lleaf879hlhcx6a2spqxczzq3dygmzpg6e53ll0aavg37gt3rvjg762vufygdqq4xprs0regcatxeqgepv7d50qsxyts2muqhwhyphg6z096dzvkj80am0f3rhm65fsycx0890807t53cmzwqppu00p25vrua6fctshty9a6hjt4sfzpqp8v4crq4pvqe75"),
146 "invoice not bolt12: Decode(UnknownRequiredFeature) \
147 and not bolt11: Bech32Error(Checksum(InvalidResidue))");
148 }
149}