Skip to content

Commit 6236e0d

Browse files
committed
Allow quantity in Refund
The spec always allowed this but the reason was unclear. It's useful if the refund is for an invoice paid for offer where a quantity was given in the request. The description in the refund would be from the offer, which may have given a unit for each item. So allowing a quantity makes it clear how many items the refund is for.
1 parent 4059677 commit 6236e0d

File tree

1 file changed

+43
-18
lines changed

1 file changed

+43
-18
lines changed

lightning/src/offers/refund.rs

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ impl RefundBuilder {
120120
let refund = RefundContents {
121121
payer: PayerContents(metadata), metadata: None, description, absolute_expiry: None,
122122
issuer: None, paths: None, chain: None, amount_msats,
123-
features: InvoiceRequestFeatures::empty(), payer_id, payer_note: None,
123+
features: InvoiceRequestFeatures::empty(), quantity: None, payer_id, payer_note: None,
124124
};
125125

126126
Ok(RefundBuilder { refund })
@@ -162,6 +162,20 @@ impl RefundBuilder {
162162
self
163163
}
164164

165+
/// Sets [`Refund::quantity`] of items. This is purely for informational purposes. It is useful
166+
/// when the refund pertains to an [`Invoice`] that paid for more than one item from an
167+
/// [`Offer`] as specified by [`InvoiceRequest::quantity`].
168+
///
169+
/// Successive calls to this method will override the previous setting.
170+
///
171+
/// [`Invoice`]: crate::offers::invoice::Invoice
172+
/// [`InvoiceRequest::quantity`]: crate::offers::invoice_request::InvoiceRequest::quantity
173+
/// [`Offer`]: crate::offers::offer::Offer
174+
pub fn quantity(mut self, quantity: u64) -> Self {
175+
self.refund.quantity = Some(quantity);
176+
self
177+
}
178+
165179
/// Sets the [`Refund::payer_note`].
166180
///
167181
/// Successive calls to this method will override the previous setting.
@@ -224,6 +238,7 @@ pub(super) struct RefundContents {
224238
chain: Option<ChainHash>,
225239
amount_msats: u64,
226240
features: InvoiceRequestFeatures,
241+
quantity: Option<u64>,
227242
payer_id: PublicKey,
228243
payer_note: Option<String>,
229244
}
@@ -285,6 +300,11 @@ impl Refund {
285300
&self.contents.features
286301
}
287302

303+
/// The quantity of an item that refund is for.
304+
pub fn quantity(&self) -> Option<u64> {
305+
self.contents.quantity
306+
}
307+
288308
/// A public node id to send to in the case where there are no [`paths`]. Otherwise, a possibly
289309
/// transient pubkey.
290310
///
@@ -396,7 +416,7 @@ impl RefundContents {
396416
chain: self.chain.as_ref(),
397417
amount: Some(self.amount_msats),
398418
features,
399-
quantity: None,
419+
quantity: self.quantity,
400420
payer_id: Some(&self.payer_id),
401421
payer_note: self.payer_note.as_ref(),
402422
};
@@ -514,11 +534,6 @@ impl TryFrom<RefundTlvStream> for RefundContents {
514534

515535
let features = features.unwrap_or_else(InvoiceRequestFeatures::empty);
516536

517-
// TODO: Check why this isn't in the spec.
518-
if quantity.is_some() {
519-
return Err(SemanticError::UnexpectedQuantity);
520-
}
521-
522537
let payer_id = match payer_id {
523538
None => return Err(SemanticError::MissingPayerId),
524539
Some(payer_id) => payer_id,
@@ -527,7 +542,7 @@ impl TryFrom<RefundTlvStream> for RefundContents {
527542
// TODO: Should metadata be included?
528543
Ok(RefundContents {
529544
payer, metadata, description, absolute_expiry, issuer, paths, chain, amount_msats,
530-
features, payer_id, payer_note,
545+
features, quantity, payer_id, payer_note,
531546
})
532547
}
533548
}
@@ -755,6 +770,24 @@ mod tests {
755770
assert_eq!(tlv_stream.chain, Some(&testnet));
756771
}
757772

773+
#[test]
774+
fn builds_refund_with_quantity() {
775+
let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()
776+
.quantity(10)
777+
.build().unwrap();
778+
let (_, _, tlv_stream) = refund.as_tlv_stream();
779+
assert_eq!(refund.quantity(), Some(10));
780+
assert_eq!(tlv_stream.quantity, Some(10));
781+
782+
let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()
783+
.quantity(10)
784+
.quantity(1)
785+
.build().unwrap();
786+
let (_, _, tlv_stream) = refund.as_tlv_stream();
787+
assert_eq!(refund.quantity(), Some(1));
788+
assert_eq!(tlv_stream.quantity, Some(1));
789+
}
790+
758791
#[test]
759792
fn builds_refund_with_payer_note() {
760793
let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()
@@ -888,6 +921,7 @@ mod tests {
888921
.path(paths[1].clone())
889922
.chain(Network::Testnet)
890923
.features_unchecked(InvoiceRequestFeatures::unknown())
924+
.quantity(10)
891925
.payer_note("baz".into())
892926
.build()
893927
.unwrap();
@@ -900,6 +934,7 @@ mod tests {
900934
assert_eq!(refund.issuer(), Some(PrintableString("bar")));
901935
assert_eq!(refund.chain(), ChainHash::using_genesis_block(Network::Testnet));
902936
assert_eq!(refund.features(), &InvoiceRequestFeatures::unknown());
937+
assert_eq!(refund.quantity(), Some(10));
903938
assert_eq!(refund.payer_note(), Some(PrintableString("baz")));
904939
},
905940
Err(e) => panic!("error parsing refund: {:?}", e),
@@ -967,16 +1002,6 @@ mod tests {
9671002
assert_eq!(e, ParseError::InvalidSemantics(SemanticError::UnexpectedSigningPubkey));
9681003
},
9691004
}
970-
971-
let mut tlv_stream = refund.as_tlv_stream();
972-
tlv_stream.2.quantity = Some(10);
973-
974-
match Refund::try_from(tlv_stream.to_bytes()) {
975-
Ok(_) => panic!("expected error"),
976-
Err(e) => {
977-
assert_eq!(e, ParseError::InvalidSemantics(SemanticError::UnexpectedQuantity));
978-
},
979-
}
9801005
}
9811006

9821007
#[test]

0 commit comments

Comments
 (0)