Skip to content

Commit e5f4c0b

Browse files
committed
Builder for creating refunds
1 parent 7d0f1cb commit e5f4c0b

File tree

1 file changed

+166
-4
lines changed

1 file changed

+166
-4
lines changed

lightning/src/offers/refund.rs

+166-4
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
//!
1717
//! [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
1818
//! [`Offer`]: crate::offers::offer::Offer
19+
//!
20+
//! ```ignore
21+
//! ```
1922
2023
use bitcoin::blockdata::constants::ChainHash;
2124
use bitcoin::network::constants::Network;
@@ -26,10 +29,10 @@ use core::time::Duration;
2629
use crate::io;
2730
use crate::ln::features::InvoiceRequestFeatures;
2831
use crate::ln::msgs::DecodeError;
29-
use crate::offers::invoice_request::InvoiceRequestTlvStream;
30-
use crate::offers::offer::OfferTlvStream;
32+
use crate::offers::invoice_request::{InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef};
33+
use crate::offers::offer::{OfferTlvStream, OfferTlvStreamRef};
3134
use crate::offers::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError};
32-
use crate::offers::payer::{PayerContents, PayerTlvStream};
35+
use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef};
3336
use crate::onion_message::BlindedPath;
3437
use crate::util::ser::{SeekReadable, WithoutLength, Writeable, Writer};
3538
use crate::util::string::PrintableString;
@@ -39,6 +42,102 @@ use crate::prelude::*;
3942
#[cfg(feature = "std")]
4043
use std::time::SystemTime;
4144

45+
/// Builds a [`Refund`] for the "offer for money" flow.
46+
///
47+
/// See [module-level documentation] for usage.
48+
///
49+
/// [module-level documentation]: self
50+
pub struct RefundBuilder {
51+
refund: RefundContents,
52+
}
53+
54+
impl RefundBuilder {
55+
/// Creates a new builder for a refund using the [`Refund::payer_id`] for signing invoices. Use
56+
/// a different pubkey per refund to avoid correlating refunds.
57+
///
58+
/// Additionally, sets the required [`Refund::description`], [`Refund::metadata`], and
59+
/// [`Refund::amount_msats`].
60+
pub fn new(
61+
description: String, metadata: Vec<u8>, payer_id: PublicKey, amount_msats: u64
62+
) -> Self {
63+
let refund = RefundContents {
64+
payer: PayerContents(metadata), metadata: None, description, absolute_expiry: None,
65+
issuer: None, paths: None, chain: None, amount_msats,
66+
features: InvoiceRequestFeatures::empty(), payer_id, payer_note: None,
67+
};
68+
69+
RefundBuilder { refund }
70+
}
71+
72+
/// Sets the [`Refund::absolute_expiry`] as seconds since the Unix epoch. Any expiry that has
73+
/// already passed is valid and can be checked for using [`Refund::is_expired`].
74+
///
75+
/// Successive calls to this method will override the previous setting.
76+
pub fn absolute_expiry(mut self, absolute_expiry: Duration) -> Self {
77+
self.refund.absolute_expiry = Some(absolute_expiry);
78+
self
79+
}
80+
81+
/// Sets the [`Refund::issuer`].
82+
///
83+
/// Successive calls to this method will override the previous setting.
84+
pub fn issuer(mut self, issuer: String) -> Self {
85+
self.refund.issuer = Some(issuer);
86+
self
87+
}
88+
89+
/// Adds a blinded path to [`Refund::paths`]. Must include at least one path if only connected
90+
/// by private channels or if [`Refund::payer_id`] is not a public node id.
91+
///
92+
/// Successive calls to this method will add another blinded path. Caller is responsible for not
93+
/// adding duplicate paths.
94+
pub fn path(mut self, path: BlindedPath) -> Self {
95+
self.refund.paths.get_or_insert_with(Vec::new).push(path);
96+
self
97+
}
98+
99+
/// Sets the [`Refund::chain`] of the given [`Network`] for paying an invoice. If not
100+
/// called, [`Network::Bitcoin`] is assumed.
101+
///
102+
/// Successive calls to this method will override the previous setting.
103+
pub fn chain(mut self, network: Network) -> Self {
104+
self.refund.chain = Some(ChainHash::using_genesis_block(network));
105+
self
106+
}
107+
108+
/// Sets the [`Refund::features`].
109+
///
110+
/// Successive calls to this method will override the previous setting.
111+
#[cfg(test)]
112+
pub fn features(mut self, features: InvoiceRequestFeatures) -> Self {
113+
self.refund.features = features;
114+
self
115+
}
116+
117+
/// Sets the [`Refund::payer_note`].
118+
///
119+
/// Successive calls to this method will override the previous setting.
120+
pub fn payer_note(mut self, payer_note: String) -> Self {
121+
self.refund.payer_note = Some(payer_note);
122+
self
123+
}
124+
125+
/// Builds a [`Refund`] after checking for valid semantics.
126+
pub fn build(mut self) -> Result<Refund, SemanticError> {
127+
if self.refund.chain() == self.refund.implied_chain() {
128+
self.refund.chain = None;
129+
}
130+
131+
let mut bytes = Vec::new();
132+
self.refund.write(&mut bytes).unwrap();
133+
134+
Ok(Refund {
135+
bytes,
136+
contents: self.refund,
137+
})
138+
}
139+
}
140+
42141
/// A `Refund` is a request to send an `Invoice` without a preceding [`Offer`].
43142
///
44143
/// Typically, after an invoice is paid, the recipient may publish a refund allowing the sender to
@@ -118,7 +217,7 @@ impl Refund {
118217

119218
/// A chain that the refund is valid for.
120219
pub fn chain(&self) -> ChainHash {
121-
self.contents.chain.unwrap_or_else(|| ChainHash::using_genesis_block(Network::Bitcoin))
220+
self.contents.chain.unwrap_or_else(|| self.contents.implied_chain())
122221
}
123222

124223
/// The amount to refund in msats (i.e., the minimum lightning-payable unit for [`chain`]).
@@ -142,6 +241,11 @@ impl Refund {
142241
pub fn payer_note(&self) -> Option<PrintableString> {
143242
self.contents.payer_note.as_ref().map(|payer_note| PrintableString(payer_note.as_str()))
144243
}
244+
245+
#[cfg(test)]
246+
fn as_tlv_stream(&self) -> RefundTlvStreamRef {
247+
self.contents.as_tlv_stream()
248+
}
145249
}
146250

147251
impl AsRef<[u8]> for Refund {
@@ -150,14 +254,72 @@ impl AsRef<[u8]> for Refund {
150254
}
151255
}
152256

257+
impl RefundContents {
258+
fn chain(&self) -> ChainHash {
259+
self.chain.unwrap_or_else(|| self.implied_chain())
260+
}
261+
262+
pub fn implied_chain(&self) -> ChainHash {
263+
ChainHash::using_genesis_block(Network::Bitcoin)
264+
}
265+
266+
pub(super) fn as_tlv_stream(&self) -> RefundTlvStreamRef {
267+
let payer = PayerTlvStreamRef {
268+
metadata: Some(&self.payer.0),
269+
};
270+
271+
let offer = OfferTlvStreamRef {
272+
chains: None,
273+
metadata: self.metadata.as_ref(),
274+
currency: None,
275+
amount: None,
276+
description: Some(&self.description),
277+
features: None,
278+
absolute_expiry: self.absolute_expiry.map(|duration| duration.as_secs()),
279+
paths: self.paths.as_ref(),
280+
issuer: self.issuer.as_ref(),
281+
quantity_max: None,
282+
node_id: None,
283+
};
284+
285+
let features = {
286+
if self.features == InvoiceRequestFeatures::empty() { None }
287+
else { Some(&self.features) }
288+
};
289+
290+
let invoice_request = InvoiceRequestTlvStreamRef {
291+
chain: self.chain.as_ref(),
292+
amount: Some(self.amount_msats),
293+
features,
294+
quantity: None,
295+
payer_id: Some(&self.payer_id),
296+
payer_note: self.payer_note.as_ref(),
297+
};
298+
299+
(payer, offer, invoice_request)
300+
}
301+
}
302+
153303
impl Writeable for Refund {
154304
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
155305
WithoutLength(&self.bytes).write(writer)
156306
}
157307
}
158308

309+
impl Writeable for RefundContents {
310+
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
311+
self.as_tlv_stream().write(writer)
312+
}
313+
}
314+
159315
type RefundTlvStream = (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream);
160316

317+
type RefundTlvStreamRef<'a> = (
318+
PayerTlvStreamRef<'a>,
319+
OfferTlvStreamRef<'a>,
320+
InvoiceRequestTlvStreamRef<'a>,
321+
);
322+
161323
impl SeekReadable for RefundTlvStream {
162324
fn read<R: io::Read + io::Seek>(r: &mut R) -> Result<Self, DecodeError> {
163325
let payer = SeekReadable::read(r)?;

0 commit comments

Comments
 (0)