lightning_signer/
invoice.rs

1use 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
13/// Generic invoice methods for both BOLT-11 and BOLT-12 invoices.
14pub trait InvoiceAttributes {
15    /// The hash of the invoice, as a unique ID
16    fn invoice_hash(&self) -> [u8; 32];
17    /// The payment hash of the invoice
18    fn payment_hash(&self) -> PaymentHash;
19    /// Invoiced amount
20    fn amount_milli_satoshis(&self) -> u64;
21    /// Description
22    fn description(&self) -> Option<String>;
23    /// Payee's public key
24    fn payee_pub_key(&self) -> PublicKey;
25    /// Timestamp of the payment, as duration since the UNIX epoch
26    fn duration_since_epoch(&self) -> Duration;
27    /// Expiry, as duration since the timestamp
28    fn expiry_duration(&self) -> Duration;
29}
30
31/// A BOLT11 or BOLT12 invoice
32#[derive(Clone, Debug)]
33pub enum Invoice {
34    /// A BOLT11 Invoice and its raw invoice hash
35    Bolt11(bolt11::Bolt11Invoice),
36    /// A BOLT12 Invoice
37    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        // from https://github.com/lightning/bolts/blob/master/11-payment-encoding.md#examples
107        //
108        // Please make a donation of any amount using payment_hash
109        // 0001020304050607080900010203040506070809000102030405060708090102 to me
110        // @03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad
111        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        // captured from CLN "test_pay.py::test_fetchinvoice"
126        //
127        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    // BOLT-12 recurrence is not supported yet
141    fn test_bolt12_recurrence() {
142        // captured from CLN "test_pay.py::test_fetchinvoice"
143        //
144        assert_invalid_argument_err!(
145            Invoice::from_str("lni1qqg239qhp9zd4tnv4exlh74gmlq6yq3qqc3xu3s3rg94nj40zfsy866mhu5vxne6tcej5878k2mneuvgjy8ssqgppg88yetrw4e8y6twvus8getnwstzzq3dygmzpg6e53ll0aavg37gt3rvjg762vufygdqq4xprs0regcatydqyqpu2qsqvgnwgcg35z6ee2h3yczraddm72xrfua9uve2rlrm9deu7xyfzr6cyypc97ywxgyjmc72gxh466uf8lyr7akfmvtn4ye4efqpscxx8g5vzj26qzsfsq3dygmzpg6e53ll0aavg37gt3rvjg762vufygdqq4xprs0regcatypt8spm8wcafuwyh24nfkctvcxmyruamsljh638ec306na7327zutcpqt0vv5neq5504nacs6cy7c39atn2ldrtecldj36tjw8nq0z69e3jkqpj9n43mjrkctxjqg07amjelrlq0zyth3gv28cmju6eumg3pqqyqr2klccnuv2h6xnkymss284z2sy0sm7w5gwqqqqqqqqqqqqqqqzsqqqqqqqqqqqqr5jt9hav2gqqqqqq5szxgt8ndznqzwagyrehhzcerrnk2p5evccgrct2cdjhk5tyz02wa9lleaf879hlhcx6a2spqxczzq3dygmzpg6e53ll0aavg37gt3rvjg762vufygdqq4xprs0regcatxeqgepv7d50qsxyts2muqhwhyphg6z096dzvkj80am0f3rhm65fsycx0890807t53cmzwqppu00p25vrua6fctshty9a6hjt4sfzpqp8v4crq4pvqe75"),
146            "invoice not bolt12: Decode(UnknownRequiredFeature) \
147             and not bolt11: Bech32Error(Checksum(InvalidResidue))");
148    }
149}