16
16
//!
17
17
//! [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
18
18
//! [`Offer`]: crate::offers::offer::Offer
19
+ //!
20
+ //! ```ignore
21
+ //! extern crate bitcoin;
22
+ //! extern crate core;
23
+ //! extern crate lightning;
24
+ //!
25
+ //! use core::convert::TryFrom;
26
+ //! use core::time::Duration;
27
+ //!
28
+ //! use bitcoin::network::constants::Network;
29
+ //! use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, SecretKey};
30
+ //! use lightning::offers::parse::ParseError;
31
+ //! use lightning::offers::refund::{Refund, RefundBuilder};
32
+ //! use lightning::util::ser::{Readable, Writeable};
33
+ //!
34
+ //! # use lightning::onion_message::BlindedPath;
35
+ //! # #[cfg(feature = "std")]
36
+ //! # use std::time::SystemTime;
37
+ //! #
38
+ //! # fn create_blinded_path() -> BlindedPath { unimplemented!() }
39
+ //! # fn create_another_blinded_path() -> BlindedPath { unimplemented!() }
40
+ //! #
41
+ //! # #[cfg(feature = "std")]
42
+ //! # fn build() -> Result<(), ParseError> {
43
+ //! let secp_ctx = Secp256k1::new();
44
+ //! let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap());
45
+ //! let pubkey = PublicKey::from(keys);
46
+ //!
47
+ //! let expiration = SystemTime::now() + Duration::from_secs(24 * 60 * 60);
48
+ //! let refund = RefundBuilder::new("coffee, large".to_string(), vec![1; 32], pubkey, 20_000)?
49
+ //! .absolute_expiry(expiration.duration_since(SystemTime::UNIX_EPOCH).unwrap())
50
+ //! .issuer("Foo Bar".to_string())
51
+ //! .path(create_blinded_path())
52
+ //! .path(create_another_blinded_path())
53
+ //! .chain(Network::Bitcoin)
54
+ //! .payer_note("refund for order #12345".to_string())
55
+ //! .build()?;
56
+ //!
57
+ //! // Encode as a bech32 string for use in a QR code.
58
+ //! let encoded_refund = refund.to_string();
59
+ //!
60
+ //! // Parse from a bech32 string after scanning from a QR code.
61
+ //! let refund = encoded_refund.parse::<Refund>()?;
62
+ //!
63
+ //! // Encode refund as raw bytes.
64
+ //! let mut bytes = Vec::new();
65
+ //! refund.write(&mut bytes).unwrap();
66
+ //!
67
+ //! // Decode raw bytes into an refund.
68
+ //! let refund = Refund::try_from(bytes)?;
69
+ //! # Ok(())
70
+ //! # }
71
+ //! ```
19
72
20
73
use bitcoin:: blockdata:: constants:: ChainHash ;
21
74
use bitcoin:: network:: constants:: Network ;
@@ -26,10 +79,10 @@ use core::time::Duration;
26
79
use crate :: io;
27
80
use crate :: ln:: features:: InvoiceRequestFeatures ;
28
81
use crate :: ln:: msgs:: { DecodeError , MAX_VALUE_MSAT } ;
29
- use crate :: offers:: invoice_request:: InvoiceRequestTlvStream ;
30
- use crate :: offers:: offer:: OfferTlvStream ;
82
+ use crate :: offers:: invoice_request:: { InvoiceRequestTlvStream , InvoiceRequestTlvStreamRef } ;
83
+ use crate :: offers:: offer:: { OfferTlvStream , OfferTlvStreamRef } ;
31
84
use crate :: offers:: parse:: { Bech32Encode , ParseError , ParsedMessage , SemanticError } ;
32
- use crate :: offers:: payer:: { PayerContents , PayerTlvStream } ;
85
+ use crate :: offers:: payer:: { PayerContents , PayerTlvStream , PayerTlvStreamRef } ;
33
86
use crate :: onion_message:: BlindedPath ;
34
87
use crate :: util:: ser:: { SeekReadable , WithoutLength , Writeable , Writer } ;
35
88
use crate :: util:: string:: PrintableString ;
@@ -39,6 +92,97 @@ use crate::prelude::*;
39
92
#[ cfg( feature = "std" ) ]
40
93
use std:: time:: SystemTime ;
41
94
95
+ /// Builds a [`Refund`] for the "offer for money" flow.
96
+ ///
97
+ /// See [module-level documentation] for usage.
98
+ ///
99
+ /// [module-level documentation]: self
100
+ pub struct RefundBuilder {
101
+ refund : RefundContents ,
102
+ }
103
+
104
+ impl RefundBuilder {
105
+ /// Creates a new builder for a refund using the [`Refund::payer_id`] for signing invoices. Use
106
+ /// a different pubkey per refund to avoid correlating refunds.
107
+ ///
108
+ /// Additionally, sets the required [`Refund::description`], [`Refund::metadata`], and
109
+ /// [`Refund::amount_msats`].
110
+ pub fn new (
111
+ description : String , metadata : Vec < u8 > , payer_id : PublicKey , amount_msats : u64
112
+ ) -> Result < Self , SemanticError > {
113
+ if amount_msats > MAX_VALUE_MSAT {
114
+ return Err ( SemanticError :: InvalidAmount ) ;
115
+ }
116
+
117
+ let refund = RefundContents {
118
+ payer : PayerContents ( metadata) , metadata : None , description, absolute_expiry : None ,
119
+ issuer : None , paths : None , chain : None , amount_msats,
120
+ features : InvoiceRequestFeatures :: empty ( ) , payer_id, payer_note : None ,
121
+ } ;
122
+
123
+ Ok ( RefundBuilder { refund } )
124
+ }
125
+
126
+ /// Sets the [`Refund::absolute_expiry`] as seconds since the Unix epoch. Any expiry that has
127
+ /// already passed is valid and can be checked for using [`Refund::is_expired`].
128
+ ///
129
+ /// Successive calls to this method will override the previous setting.
130
+ pub fn absolute_expiry ( mut self , absolute_expiry : Duration ) -> Self {
131
+ self . refund . absolute_expiry = Some ( absolute_expiry) ;
132
+ self
133
+ }
134
+
135
+ /// Sets the [`Refund::issuer`].
136
+ ///
137
+ /// Successive calls to this method will override the previous setting.
138
+ pub fn issuer ( mut self , issuer : String ) -> Self {
139
+ self . refund . issuer = Some ( issuer) ;
140
+ self
141
+ }
142
+
143
+ /// Adds a blinded path to [`Refund::paths`]. Must include at least one path if only connected
144
+ /// by private channels or if [`Refund::payer_id`] is not a public node id.
145
+ ///
146
+ /// Successive calls to this method will add another blinded path. Caller is responsible for not
147
+ /// adding duplicate paths.
148
+ pub fn path ( mut self , path : BlindedPath ) -> Self {
149
+ self . refund . paths . get_or_insert_with ( Vec :: new) . push ( path) ;
150
+ self
151
+ }
152
+
153
+ /// Sets the [`Refund::chain`] of the given [`Network`] for paying an invoice. If not
154
+ /// called, [`Network::Bitcoin`] is assumed.
155
+ ///
156
+ /// Successive calls to this method will override the previous setting.
157
+ pub fn chain ( mut self , network : Network ) -> Self {
158
+ self . refund . chain = Some ( ChainHash :: using_genesis_block ( network) ) ;
159
+ self
160
+ }
161
+
162
+ /// Sets the [`Refund::payer_note`].
163
+ ///
164
+ /// Successive calls to this method will override the previous setting.
165
+ pub fn payer_note ( mut self , payer_note : String ) -> Self {
166
+ self . refund . payer_note = Some ( payer_note) ;
167
+ self
168
+ }
169
+
170
+ /// Builds a [`Refund`] after checking for valid semantics.
171
+ pub fn build ( mut self ) -> Result < Refund , SemanticError > {
172
+ if self . refund . chain ( ) == self . refund . implied_chain ( ) {
173
+ self . refund . chain = None ;
174
+ }
175
+
176
+ let mut bytes = Vec :: new ( ) ;
177
+ self . refund . write ( & mut bytes) . unwrap ( ) ;
178
+
179
+ Ok ( Refund {
180
+ bytes,
181
+ contents : self . refund ,
182
+ } )
183
+ }
184
+ }
185
+
42
186
/// A `Refund` is a request to send an `Invoice` without a preceding [`Offer`].
43
187
///
44
188
/// Typically, after an invoice is paid, the recipient may publish a refund allowing the sender to
@@ -118,7 +262,7 @@ impl Refund {
118
262
119
263
/// A chain that the refund is valid for.
120
264
pub fn chain ( & self ) -> ChainHash {
121
- self . contents . chain . unwrap_or_else ( || ChainHash :: using_genesis_block ( Network :: Bitcoin ) )
265
+ self . contents . chain . unwrap_or_else ( || self . contents . implied_chain ( ) )
122
266
}
123
267
124
268
/// The amount to refund in msats (i.e., the minimum lightning-payable unit for [`chain`]).
@@ -150,14 +294,72 @@ impl AsRef<[u8]> for Refund {
150
294
}
151
295
}
152
296
297
+ impl RefundContents {
298
+ fn chain ( & self ) -> ChainHash {
299
+ self . chain . unwrap_or_else ( || self . implied_chain ( ) )
300
+ }
301
+
302
+ pub fn implied_chain ( & self ) -> ChainHash {
303
+ ChainHash :: using_genesis_block ( Network :: Bitcoin )
304
+ }
305
+
306
+ pub ( super ) fn as_tlv_stream ( & self ) -> RefundTlvStreamRef {
307
+ let payer = PayerTlvStreamRef {
308
+ metadata : Some ( & self . payer . 0 ) ,
309
+ } ;
310
+
311
+ let offer = OfferTlvStreamRef {
312
+ chains : None ,
313
+ metadata : self . metadata . as_ref ( ) ,
314
+ currency : None ,
315
+ amount : None ,
316
+ description : Some ( & self . description ) ,
317
+ features : None ,
318
+ absolute_expiry : self . absolute_expiry . map ( |duration| duration. as_secs ( ) ) ,
319
+ paths : self . paths . as_ref ( ) ,
320
+ issuer : self . issuer . as_ref ( ) ,
321
+ quantity_max : None ,
322
+ node_id : None ,
323
+ } ;
324
+
325
+ let features = {
326
+ if self . features == InvoiceRequestFeatures :: empty ( ) { None }
327
+ else { Some ( & self . features ) }
328
+ } ;
329
+
330
+ let invoice_request = InvoiceRequestTlvStreamRef {
331
+ chain : self . chain . as_ref ( ) ,
332
+ amount : Some ( self . amount_msats ) ,
333
+ features,
334
+ quantity : None ,
335
+ payer_id : Some ( & self . payer_id ) ,
336
+ payer_note : self . payer_note . as_ref ( ) ,
337
+ } ;
338
+
339
+ ( payer, offer, invoice_request)
340
+ }
341
+ }
342
+
153
343
impl Writeable for Refund {
154
344
fn write < W : Writer > ( & self , writer : & mut W ) -> Result < ( ) , io:: Error > {
155
345
WithoutLength ( & self . bytes ) . write ( writer)
156
346
}
157
347
}
158
348
349
+ impl Writeable for RefundContents {
350
+ fn write < W : Writer > ( & self , writer : & mut W ) -> Result < ( ) , io:: Error > {
351
+ self . as_tlv_stream ( ) . write ( writer)
352
+ }
353
+ }
354
+
159
355
type RefundTlvStream = ( PayerTlvStream , OfferTlvStream , InvoiceRequestTlvStream ) ;
160
356
357
+ type RefundTlvStreamRef < ' a > = (
358
+ PayerTlvStreamRef < ' a > ,
359
+ OfferTlvStreamRef < ' a > ,
360
+ InvoiceRequestTlvStreamRef < ' a > ,
361
+ ) ;
362
+
161
363
impl SeekReadable for RefundTlvStream {
162
364
fn read < R : io:: Read + io:: Seek > ( r : & mut R ) -> Result < Self , DecodeError > {
163
365
let payer = SeekReadable :: read ( r) ?;
0 commit comments