@@ -17,9 +17,11 @@ use crate::offers::invoice::{
17
17
construct_payment_paths, filter_fallbacks, BlindedPathIter , BlindedPayInfo , BlindedPayInfoIter ,
18
18
FallbackAddress , SIGNATURE_TAG ,
19
19
} ;
20
- use crate :: offers:: invoice_macros:: invoice_accessors_common;
21
- use crate :: offers:: merkle:: { self , SignatureTlvStream , TaggedHash } ;
22
- use crate :: offers:: offer:: { Amount , OfferContents , OfferTlvStream , Quantity } ;
20
+ use crate :: offers:: invoice_macros:: { invoice_accessors_common, invoice_builder_methods_common} ;
21
+ use crate :: offers:: merkle:: {
22
+ self , SignError , SignFn , SignatureTlvStream , SignatureTlvStreamRef , TaggedHash ,
23
+ } ;
24
+ use crate :: offers:: offer:: { Amount , Offer , OfferContents , OfferTlvStream , Quantity } ;
23
25
use crate :: offers:: parse:: { Bolt12ParseError , Bolt12SemanticError , ParsedMessage } ;
24
26
use crate :: util:: ser:: {
25
27
HighZeroBytesDroppedBigSize , Iterable , SeekReadable , WithoutLength , Writeable , Writer ,
@@ -28,7 +30,7 @@ use crate::util::string::PrintableString;
28
30
use bitcoin:: address:: Address ;
29
31
use bitcoin:: blockdata:: constants:: ChainHash ;
30
32
use bitcoin:: secp256k1:: schnorr:: Signature ;
31
- use bitcoin:: secp256k1:: PublicKey ;
33
+ use bitcoin:: secp256k1:: { self , KeyPair , PublicKey , Secp256k1 } ;
32
34
use core:: time:: Duration ;
33
35
34
36
#[ cfg( feature = "std" ) ]
@@ -40,36 +42,74 @@ use crate::prelude::*;
40
42
/// Static invoices default to expiring after 24 hours.
41
43
const DEFAULT_RELATIVE_EXPIRY : Duration = Duration :: from_secs ( 3600 * 24 ) ;
42
44
43
- /// A `StaticInvoice` is a reusable payment request corresponding to an [`Offer`].
45
+ /// Builds a [ `StaticInvoice`] from an [`Offer`].
44
46
///
45
- /// A static invoice may be sent in response to an [`InvoiceRequest`] and includes all the
46
- /// information needed to pay the recipient. However, unlike [`Bolt12Invoice`]s, static invoices do
47
- /// not provide proof-of-payment. Therefore, [`Bolt12Invoice`]s should be preferred when the
48
- /// recipient is online to provide one.
47
+ /// See [module-level documentation] for usage.
49
48
///
50
- /// [`Offer`]: crate::offers::offer::Offer
51
- /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
52
- /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice
53
- pub struct StaticInvoice {
49
+ /// This is not exported to bindings users as builder patterns don't map outside of move semantics.
50
+ pub struct StaticInvoiceBuilder < ' a > {
51
+ offer_bytes : & ' a Vec < u8 > ,
52
+ invoice : InvoiceContents ,
53
+ keys : KeyPair ,
54
+ }
55
+
56
+ impl < ' a > StaticInvoiceBuilder < ' a > {
57
+ /// Initialize a [`StaticInvoiceBuilder`] from the given [`Offer`].
58
+ ///
59
+ /// Unless [`StaticInvoiceBuilder::relative_expiry`] is set, the invoice will expire 24 hours
60
+ /// after `created_at`.
61
+ pub fn for_offer_using_keys (
62
+ offer : & ' a Offer , payment_paths : Vec < ( BlindedPayInfo , BlindedPath ) > , created_at : Duration ,
63
+ keys : KeyPair ,
64
+ ) -> Result < Self , Bolt12SemanticError > {
65
+ let invoice = InvoiceContents :: new ( offer, payment_paths, created_at, keys. public_key ( ) ) ;
66
+ if invoice. payment_paths . is_empty ( ) {
67
+ return Err ( Bolt12SemanticError :: MissingPaths ) ;
68
+ }
69
+ if invoice. offer . chains ( ) . len ( ) > 1 {
70
+ return Err ( Bolt12SemanticError :: UnexpectedChain ) ;
71
+ }
72
+ Ok ( Self { offer_bytes : & offer. bytes , invoice, keys } )
73
+ }
74
+
75
+ /// Builds a signed [`StaticInvoice`] after checking for valid semantics.
76
+ pub fn build_and_sign < T : secp256k1:: Signing > (
77
+ self , secp_ctx : & Secp256k1 < T > ,
78
+ ) -> Result < StaticInvoice , Bolt12SemanticError > {
79
+ #[ cfg( feature = "std" ) ]
80
+ {
81
+ if self . invoice . is_offer_expired ( ) {
82
+ return Err ( Bolt12SemanticError :: AlreadyExpired ) ;
83
+ }
84
+ }
85
+
86
+ #[ cfg( not( feature = "std" ) ) ]
87
+ {
88
+ if self . invoice . is_offer_expired_no_std ( self . invoice . created_at ( ) ) {
89
+ return Err ( Bolt12SemanticError :: AlreadyExpired ) ;
90
+ }
91
+ }
92
+
93
+ let Self { offer_bytes, invoice, keys } = self ;
94
+ let unsigned_invoice = UnsignedStaticInvoice :: new ( & offer_bytes, invoice) ;
95
+ let invoice = unsigned_invoice
96
+ . sign ( |message : & UnsignedStaticInvoice | {
97
+ Ok ( secp_ctx. sign_schnorr_no_aux_rand ( message. tagged_hash . as_digest ( ) , & keys) )
98
+ } )
99
+ . unwrap ( ) ;
100
+ Ok ( invoice)
101
+ }
102
+
103
+ invoice_builder_methods_common ! ( self , Self , self . invoice, Self , self , S , StaticInvoice , mut ) ;
104
+ }
105
+
106
+ /// A semantically valid [`StaticInvoice`] that hasn't been signed.
107
+ pub struct UnsignedStaticInvoice {
54
108
bytes : Vec < u8 > ,
55
109
contents : InvoiceContents ,
56
- signature : Signature ,
57
110
tagged_hash : TaggedHash ,
58
111
}
59
112
60
- /// The contents of a [`StaticInvoice`] for responding to an [`Offer`].
61
- ///
62
- /// [`Offer`]: crate::offers::offer::Offer
63
- struct InvoiceContents {
64
- offer : OfferContents ,
65
- payment_paths : Vec < ( BlindedPayInfo , BlindedPath ) > ,
66
- created_at : Duration ,
67
- relative_expiry : Option < Duration > ,
68
- fallbacks : Option < Vec < FallbackAddress > > ,
69
- features : Bolt12InvoiceFeatures ,
70
- signing_pubkey : PublicKey ,
71
- }
72
-
73
113
macro_rules! invoice_accessors { ( $self: ident, $contents: expr) => {
74
114
/// The chain that must be used when paying the invoice. [`StaticInvoice`]s currently can only be
75
115
/// created from offers that support a single chain.
@@ -138,6 +178,99 @@ macro_rules! invoice_accessors { ($self: ident, $contents: expr) => {
138
178
}
139
179
} }
140
180
181
+ impl UnsignedStaticInvoice {
182
+ fn new ( offer_bytes : & Vec < u8 > , contents : InvoiceContents ) -> Self {
183
+ let mut bytes = Vec :: new ( ) ;
184
+ WithoutLength ( offer_bytes) . write ( & mut bytes) . unwrap ( ) ;
185
+ contents. as_invoice_fields_tlv_stream ( ) . write ( & mut bytes) . unwrap ( ) ;
186
+
187
+ let tagged_hash = TaggedHash :: from_valid_tlv_stream_bytes ( SIGNATURE_TAG , & bytes) ;
188
+ Self { contents, tagged_hash, bytes }
189
+ }
190
+
191
+ /// Signs the [`TaggedHash`] of the invoice using the given function.
192
+ ///
193
+ /// Note: The hash computation may have included unknown, odd TLV records.
194
+ pub fn sign < F : SignStaticInvoiceFn > ( mut self , sign : F ) -> Result < StaticInvoice , SignError > {
195
+ let pubkey = self . contents . signing_pubkey ;
196
+ let signature = merkle:: sign_message ( sign, & self , pubkey) ?;
197
+
198
+ // Append the signature TLV record to the bytes.
199
+ let signature_tlv_stream = SignatureTlvStreamRef { signature : Some ( & signature) } ;
200
+ signature_tlv_stream. write ( & mut self . bytes ) . unwrap ( ) ;
201
+
202
+ Ok ( StaticInvoice {
203
+ bytes : self . bytes ,
204
+ contents : self . contents ,
205
+ signature,
206
+ tagged_hash : self . tagged_hash ,
207
+ } )
208
+ }
209
+
210
+ invoice_accessors_common ! ( self , self . contents, StaticInvoice ) ;
211
+ invoice_accessors ! ( self , self . contents) ;
212
+ }
213
+
214
+ impl AsRef < TaggedHash > for UnsignedStaticInvoice {
215
+ fn as_ref ( & self ) -> & TaggedHash {
216
+ & self . tagged_hash
217
+ }
218
+ }
219
+
220
+ /// A function for signing an [`UnsignedStaticInvoice`].
221
+ pub trait SignStaticInvoiceFn {
222
+ /// Signs a [`TaggedHash`] computed over the merkle root of `message`'s TLV stream.
223
+ fn sign_invoice ( & self , message : & UnsignedStaticInvoice ) -> Result < Signature , ( ) > ;
224
+ }
225
+
226
+ impl < F > SignStaticInvoiceFn for F
227
+ where
228
+ F : Fn ( & UnsignedStaticInvoice ) -> Result < Signature , ( ) > ,
229
+ {
230
+ fn sign_invoice ( & self , message : & UnsignedStaticInvoice ) -> Result < Signature , ( ) > {
231
+ self ( message)
232
+ }
233
+ }
234
+
235
+ impl < F > SignFn < UnsignedStaticInvoice > for F
236
+ where
237
+ F : SignStaticInvoiceFn ,
238
+ {
239
+ fn sign ( & self , message : & UnsignedStaticInvoice ) -> Result < Signature , ( ) > {
240
+ self . sign_invoice ( message)
241
+ }
242
+ }
243
+
244
+ /// A `StaticInvoice` is a reusable payment request corresponding to an [`Offer`].
245
+ ///
246
+ /// A static invoice may be sent in response to an [`InvoiceRequest`] and includes all the
247
+ /// information needed to pay the recipient. However, unlike [`Bolt12Invoice`]s, static invoices do
248
+ /// not provide proof-of-payment. Therefore, [`Bolt12Invoice`]s should be preferred when the
249
+ /// recipient is online to provide one.
250
+ ///
251
+ /// [`Offer`]: crate::offers::offer::Offer
252
+ /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
253
+ /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice
254
+ pub struct StaticInvoice {
255
+ bytes : Vec < u8 > ,
256
+ contents : InvoiceContents ,
257
+ signature : Signature ,
258
+ tagged_hash : TaggedHash ,
259
+ }
260
+
261
+ /// The contents of a [`StaticInvoice`] for responding to an [`Offer`].
262
+ ///
263
+ /// [`Offer`]: crate::offers::offer::Offer
264
+ struct InvoiceContents {
265
+ offer : OfferContents ,
266
+ payment_paths : Vec < ( BlindedPayInfo , BlindedPath ) > ,
267
+ created_at : Duration ,
268
+ relative_expiry : Option < Duration > ,
269
+ fallbacks : Option < Vec < FallbackAddress > > ,
270
+ features : Bolt12InvoiceFeatures ,
271
+ signing_pubkey : PublicKey ,
272
+ }
273
+
141
274
impl StaticInvoice {
142
275
invoice_accessors_common ! ( self , self . contents, StaticInvoice ) ;
143
276
invoice_accessors ! ( self , self . contents) ;
@@ -154,6 +287,51 @@ impl StaticInvoice {
154
287
}
155
288
156
289
impl InvoiceContents {
290
+ #[ cfg( feature = "std" ) ]
291
+ fn is_offer_expired ( & self ) -> bool {
292
+ self . offer . is_expired ( )
293
+ }
294
+
295
+ #[ cfg( not( feature = "std" ) ) ]
296
+ fn is_offer_expired_no_std ( & self , duration_since_epoch : Duration ) -> bool {
297
+ self . offer . is_expired_no_std ( duration_since_epoch)
298
+ }
299
+
300
+ fn new (
301
+ offer : & Offer , payment_paths : Vec < ( BlindedPayInfo , BlindedPath ) > , created_at : Duration ,
302
+ signing_pubkey : PublicKey ,
303
+ ) -> Self {
304
+ Self {
305
+ offer : offer. contents . clone ( ) ,
306
+ payment_paths,
307
+ created_at,
308
+ relative_expiry : None ,
309
+ fallbacks : None ,
310
+ features : Bolt12InvoiceFeatures :: empty ( ) ,
311
+ signing_pubkey,
312
+ }
313
+ }
314
+
315
+ fn as_invoice_fields_tlv_stream ( & self ) -> InvoiceTlvStreamRef {
316
+ let features = {
317
+ if self . features == Bolt12InvoiceFeatures :: empty ( ) {
318
+ None
319
+ } else {
320
+ Some ( & self . features )
321
+ }
322
+ } ;
323
+
324
+ InvoiceTlvStreamRef {
325
+ paths : Some ( Iterable ( self . payment_paths . iter ( ) . map ( |( _, path) | path) ) ) ,
326
+ blindedpay : Some ( Iterable ( self . payment_paths . iter ( ) . map ( |( payinfo, _) | payinfo) ) ) ,
327
+ created_at : Some ( self . created_at . as_secs ( ) ) ,
328
+ relative_expiry : self . relative_expiry . map ( |duration| duration. as_secs ( ) as u32 ) ,
329
+ fallbacks : self . fallbacks . as_ref ( ) ,
330
+ features,
331
+ node_id : Some ( & self . signing_pubkey ) ,
332
+ }
333
+ }
334
+
157
335
fn chain ( & self ) -> ChainHash {
158
336
debug_assert_eq ! ( self . offer. chains( ) . len( ) , 1 ) ;
159
337
self . offer . chains ( ) . first ( ) . cloned ( ) . unwrap_or_else ( || self . offer . implied_chain ( ) )
0 commit comments