1616//!
1717//! [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
1818//! [`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+ //! ```
1972
2073use bitcoin:: blockdata:: constants:: ChainHash ;
2174use bitcoin:: network:: constants:: Network ;
@@ -26,10 +79,10 @@ use core::time::Duration;
2679use crate :: io;
2780use crate :: ln:: features:: InvoiceRequestFeatures ;
2881use 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 } ;
3184use crate :: offers:: parse:: { Bech32Encode , ParseError , ParsedMessage , SemanticError } ;
32- use crate :: offers:: payer:: { PayerContents , PayerTlvStream } ;
85+ use crate :: offers:: payer:: { PayerContents , PayerTlvStream , PayerTlvStreamRef } ;
3386use crate :: onion_message:: BlindedPath ;
3487use crate :: util:: ser:: { SeekReadable , WithoutLength , Writeable , Writer } ;
3588use crate :: util:: string:: PrintableString ;
@@ -39,6 +92,97 @@ use crate::prelude::*;
3992#[ cfg( feature = "std" ) ]
4093use std:: time:: SystemTime ;
4194
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+
42186/// A `Refund` is a request to send an `Invoice` without a preceding [`Offer`].
43187///
44188/// Typically, after an invoice is paid, the recipient may publish a refund allowing the sender to
@@ -118,7 +262,7 @@ impl Refund {
118262
119263 /// A chain that the refund is valid for.
120264 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 ( ) )
122266 }
123267
124268 /// The amount to refund in msats (i.e., the minimum lightning-payable unit for [`chain`]).
@@ -150,14 +294,72 @@ impl AsRef<[u8]> for Refund {
150294 }
151295}
152296
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+
153343impl Writeable for Refund {
154344 fn write < W : Writer > ( & self , writer : & mut W ) -> Result < ( ) , io:: Error > {
155345 WithoutLength ( & self . bytes ) . write ( writer)
156346 }
157347}
158348
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+
159355type RefundTlvStream = ( PayerTlvStream , OfferTlvStream , InvoiceRequestTlvStream ) ;
160356
357+ type RefundTlvStreamRef < ' a > = (
358+ PayerTlvStreamRef < ' a > ,
359+ OfferTlvStreamRef < ' a > ,
360+ InvoiceRequestTlvStreamRef < ' a > ,
361+ ) ;
362+
161363impl SeekReadable for RefundTlvStream {
162364 fn read < R : io:: Read + io:: Seek > ( r : & mut R ) -> Result < Self , DecodeError > {
163365 let payer = SeekReadable :: read ( r) ?;
0 commit comments