Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions lightning/src/offers/invoice.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -338,8 +338,10 @@ struct InvoiceFields {

impl Invoice {
/// Paths to the recipient originating from publicly reachable nodes, including information
/// needed for routing payments across them. Blinded paths provide recipient privacy by
/// obfuscating its node id.
/// needed for routing payments across them.
///
/// Blinded paths provide recipient privacy by obfuscating its node id. Note, however, that this
/// privacy is lost if a public node id is used for [`Invoice::signing_pubkey`].
pub fn payment_paths(&self) -> &[(BlindedPath, BlindedPayInfo)] {
&self.contents.fields().payment_paths[..]
}
Expand Down
41 changes: 36 additions & 5 deletions lightning/src/offers/invoice_request.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -333,7 +333,9 @@ impl InvoiceRequest {
/// for the invoice.
///
/// The `payment_paths` parameter is useful for maintaining the payment recipient's privacy. It
/// must contain one or more elements.
/// must contain one or more elements ordered from most-preferred to least-preferred, if there's
/// a preference. Note, however, that any privacy is lost if a public node id was used for
/// [`Offer::signing_pubkey`].
///
/// Errors if the request contains unknown required features.
///
Expand DownExpand Up@@ -845,11 +847,12 @@ mod tests {

#[test]
fn builds_invoice_request_with_quantity() {
let one = NonZeroU64::new(1).unwrap();
let ten = NonZeroU64::new(10).unwrap();

let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
Expand All@@ -860,7 +863,7 @@ mod tests {

match OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.amount_msats(2_000).unwrap()
Expand DownExpand Up@@ -918,6 +921,17 @@ mod tests {
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(e, SemanticError::MissingQuantity),
}

match OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::Bounded(one))
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build()
{
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(e, SemanticError::MissingQuantity),
}
}

#[test]
Expand DownExpand Up@@ -1102,11 +1116,12 @@ mod tests {

#[test]
fn parses_invoice_request_with_quantity() {
let one = NonZeroU64::new(1).unwrap();
let ten = NonZeroU64::new(10).unwrap();

let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
Expand All@@ -1121,7 +1136,7 @@ mod tests {

let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.amount_msats(2_000).unwrap()
Comment thread
jkczyz marked this conversation as resolved.
Expand DownExpand Up@@ -1206,6 +1221,22 @@ mod tests {
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingQuantity)),
}

let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::Bounded(one))
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build_unchecked()
.sign(payer_sign).unwrap();

let mut buffer = Vec::new();
invoice_request.write(&mut buffer).unwrap();

match InvoiceRequest::try_from(buffer) {
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingQuantity)),
}
}

#[test]
Expand Down
76 changes: 38 additions & 38 deletions lightning/src/offers/offer.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -106,7 +106,7 @@ impl OfferBuilder {
let offer = OfferContents {
chains: None, metadata: None, amount: None, description,
features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None,
supported_quantity: Quantity::one(), signing_pubkey,
supported_quantity: Quantity::One, signing_pubkey,
};
OfferBuilder { offer }
}
Expand DownExpand Up@@ -178,7 +178,7 @@ impl OfferBuilder {
}

/// Sets the quantity of items for [`Offer::supported_quantity`]. If not called, defaults to
/// [`Quantity::one`].
/// [`Quantity::One`].
///
/// Successive calls to this method will override the previous setting.
pub fn supported_quantity(mut self, quantity: Quantity) -> Self {
Expand DownExpand Up@@ -464,19 +464,17 @@ impl OfferContents {

fn is_valid_quantity(&self, quantity: u64) -> bool {
match self.supported_quantity {
Quantity::Bounded(n) => {
let n = n.get();
if n == 1 { false }
else { quantity > 0 && quantity <= n }
},
Quantity::Bounded(n) => quantity <= n.get(),
Quantity::Unbounded => quantity > 0,
Quantity::One => quantity == 1,
}
}

fn expects_quantity(&self) -> bool {
match self.supported_quantity {
Quantity::Bounded(n) => n.get() != 1,
Quantity::Bounded(_) => true,
Quantity::Unbounded => true,
Quantity::One => false,
}
}

Expand DownExpand Up@@ -549,25 +547,24 @@ pub type CurrencyCode = [u8; 3];
/// Quantity of items supported by an [`Offer`].
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Quantity {
/// Up to a specific number of items (inclusive).
/// Up to a specific number of items (inclusive). Use when more than one item can be requested
/// but is limited (e.g., because of per customer or inventory limits).
///
/// May be used with `NonZeroU64::new(1)` but prefer to use [`Quantity::One`] if only one item

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you added this comment on the wrong variant. More generally, I'd expect this to tell me why I should use one or the other, not just that I should.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Fixed and further documented each variant. PTAL

/// is supported.
Bounded(NonZeroU64),
/// One or more items.
/// One or more items. Use when more than one item can be requested without any limit.
Unbounded,
/// Only one item. Use when only a single item can be requested.
One,
}

impl Quantity {
/// The default quantity of one.
pub fn one() -> Self {
Quantity::Bounded(NonZeroU64::new(1).unwrap())
}

fn to_tlv_record(&self) -> Option<u64> {
match self {
Quantity::Bounded(n) => {
let n = n.get();
if n == 1 { None } else { Some(n) }
},
Quantity::Bounded(n) => Some(n.get()),
Quantity::Unbounded => Some(0),
Quantity::One => None,
}
}
}
Expand DownExpand Up@@ -639,9 +636,8 @@ impl TryFrom<OfferTlvStream> for OfferContents {
.map(|seconds_from_epoch| Duration::from_secs(seconds_from_epoch));

let supported_quantity = match quantity_max {
None => Quantity::one(),
None => Quantity::One,
Some(0) => Quantity::Unbounded,
Some(1) => return Err(SemanticError::InvalidQuantity),
Some(n) => Quantity::Bounded(NonZeroU64::new(n).unwrap()),
};

Expand DownExpand Up@@ -708,7 +704,7 @@ mod tests {
assert!(!offer.is_expired());
assert_eq!(offer.paths(), &[]);
assert_eq!(offer.issuer(), None);
assert_eq!(offer.supported_quantity(), Quantity::one());
assert_eq!(offer.supported_quantity(), Quantity::One);
assert_eq!(offer.signing_pubkey(), pubkey(42));

assert_eq!(
Expand DownExpand Up@@ -930,14 +926,15 @@ mod tests {

#[test]
fn builds_offer_with_supported_quantity() {
let one = NonZeroU64::new(1).unwrap();
let ten = NonZeroU64::new(10).unwrap();

let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build()
.unwrap();
let tlv_stream = offer.as_tlv_stream();
assert_eq!(offer.supported_quantity(), Quantity::one());
assert_eq!(offer.supported_quantity(), Quantity::One);
assert_eq!(tlv_stream.quantity_max, None);

let offer = OfferBuilder::new("foo".into(), pubkey(42))
Expand All@@ -956,13 +953,21 @@ mod tests {
assert_eq!(offer.supported_quantity(), Quantity::Bounded(ten));
assert_eq!(tlv_stream.quantity_max, Some(10));

let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::Bounded(one))
.build()
.unwrap();
let tlv_stream = offer.as_tlv_stream();
assert_eq!(offer.supported_quantity(), Quantity::Bounded(one));
assert_eq!(tlv_stream.quantity_max, Some(1));

let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::Bounded(ten))
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build()
.unwrap();
let tlv_stream = offer.as_tlv_stream();
assert_eq!(offer.supported_quantity(), Quantity::one());
assert_eq!(offer.supported_quantity(), Quantity::One);
assert_eq!(tlv_stream.quantity_max, None);
}

Expand DownExpand Up@@ -1094,7 +1099,7 @@ mod tests {
#[test]
fn parses_offer_with_quantity() {
let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build()
.unwrap();
if let Err(e) = offer.to_string().parse::<Offer>() {
Expand All@@ -1117,17 +1122,12 @@ mod tests {
panic!("error parsing offer: {:?}", e);
}

let mut tlv_stream = offer.as_tlv_stream();
tlv_stream.quantity_max = Some(1);

let mut encoded_offer = Vec::new();
tlv_stream.write(&mut encoded_offer).unwrap();

match Offer::try_from(encoded_offer) {
Ok(_) => panic!("expected error"),
Err(e) => {
assert_eq!(e, ParseError::InvalidSemantics(SemanticError::InvalidQuantity));
},
let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::Bounded(NonZeroU64::new(1).unwrap()))
.build()
.unwrap();
if let Err(e) = offer.to_string().parse::<Offer>() {
panic!("error parsing offer: {:?}", e);
}
}

Expand Down
2 changes: 2 additions & 0 deletions lightning/src/offers/parse.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -157,6 +157,8 @@ pub enum SemanticError {
InvalidQuantity,
/// A quantity or quantity bounds was provided but was not expected.
UnexpectedQuantity,
/// Metadata was provided but was not expected.
UnexpectedMetadata,
/// Payer metadata was expected but was missing.
MissingPayerMetadata,
/// A payer id was expected but was missing.
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
BOLT 12 spec updates by jkczyz · Pull Request #1972 · lightningdevkit/rust-lightning · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions lightning/src/offers/invoice.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -338,8 +338,10 @@ struct InvoiceFields {

impl Invoice {
/// Paths to the recipient originating from publicly reachable nodes, including information
/// needed for routing payments across them. Blinded paths provide recipient privacy by
/// obfuscating its node id.
/// needed for routing payments across them.
///
/// Blinded paths provide recipient privacy by obfuscating its node id. Note, however, that this
/// privacy is lost if a public node id is used for [`Invoice::signing_pubkey`].
pub fn payment_paths(&self) -> &[(BlindedPath, BlindedPayInfo)] {
&self.contents.fields().payment_paths[..]
}
Expand Down
41 changes: 36 additions & 5 deletions lightning/src/offers/invoice_request.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -333,7 +333,9 @@ impl InvoiceRequest {
/// for the invoice.
///
/// The `payment_paths` parameter is useful for maintaining the payment recipient's privacy. It
/// must contain one or more elements.
/// must contain one or more elements ordered from most-preferred to least-preferred, if there's
/// a preference. Note, however, that any privacy is lost if a public node id was used for
/// [`Offer::signing_pubkey`].
///
/// Errors if the request contains unknown required features.
///
Expand DownExpand Up@@ -845,11 +847,12 @@ mod tests {

#[test]
fn builds_invoice_request_with_quantity() {
let one = NonZeroU64::new(1).unwrap();
let ten = NonZeroU64::new(10).unwrap();

let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
Expand All@@ -860,7 +863,7 @@ mod tests {

match OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.amount_msats(2_000).unwrap()
Expand DownExpand Up@@ -918,6 +921,17 @@ mod tests {
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(e, SemanticError::MissingQuantity),
}

match OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::Bounded(one))
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build()
{
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(e, SemanticError::MissingQuantity),
}
}

#[test]
Expand DownExpand Up@@ -1102,11 +1116,12 @@ mod tests {

#[test]
fn parses_invoice_request_with_quantity() {
let one = NonZeroU64::new(1).unwrap();
let ten = NonZeroU64::new(10).unwrap();

let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
Expand All@@ -1121,7 +1136,7 @@ mod tests {

let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.amount_msats(2_000).unwrap()
Comment thread
jkczyz marked this conversation as resolved.
Expand DownExpand Up@@ -1206,6 +1221,22 @@ mod tests {
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingQuantity)),
}

let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::Bounded(one))
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build_unchecked()
.sign(payer_sign).unwrap();

let mut buffer = Vec::new();
invoice_request.write(&mut buffer).unwrap();

match InvoiceRequest::try_from(buffer) {
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingQuantity)),
}
}

#[test]
Expand Down
76 changes: 38 additions & 38 deletions lightning/src/offers/offer.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -106,7 +106,7 @@ impl OfferBuilder {
let offer = OfferContents {
chains: None, metadata: None, amount: None, description,
features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None,
supported_quantity: Quantity::one(), signing_pubkey,
supported_quantity: Quantity::One, signing_pubkey,
};
OfferBuilder { offer }
}
Expand DownExpand Up@@ -178,7 +178,7 @@ impl OfferBuilder {
}

/// Sets the quantity of items for [`Offer::supported_quantity`]. If not called, defaults to
/// [`Quantity::one`].
/// [`Quantity::One`].
///
/// Successive calls to this method will override the previous setting.
pub fn supported_quantity(mut self, quantity: Quantity) -> Self {
Expand DownExpand Up@@ -464,19 +464,17 @@ impl OfferContents {

fn is_valid_quantity(&self, quantity: u64) -> bool {
match self.supported_quantity {
Quantity::Bounded(n) => {
let n = n.get();
if n == 1 { false }
else { quantity > 0 && quantity <= n }
},
Quantity::Bounded(n) => quantity <= n.get(),
Quantity::Unbounded => quantity > 0,
Quantity::One => quantity == 1,
}
}

fn expects_quantity(&self) -> bool {
match self.supported_quantity {
Quantity::Bounded(n) => n.get() != 1,
Quantity::Bounded(_) => true,
Quantity::Unbounded => true,
Quantity::One => false,
}
}

Expand DownExpand Up@@ -549,25 +547,24 @@ pub type CurrencyCode = [u8; 3];
/// Quantity of items supported by an [`Offer`].
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Quantity {
/// Up to a specific number of items (inclusive).
/// Up to a specific number of items (inclusive). Use when more than one item can be requested
/// but is limited (e.g., because of per customer or inventory limits).
///
/// May be used with `NonZeroU64::new(1)` but prefer to use [`Quantity::One`] if only one item

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you added this comment on the wrong variant. More generally, I'd expect this to tell me why I should use one or the other, not just that I should.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Fixed and further documented each variant. PTAL

/// is supported.
Bounded(NonZeroU64),
/// One or more items.
/// One or more items. Use when more than one item can be requested without any limit.
Unbounded,
/// Only one item. Use when only a single item can be requested.
One,
}

impl Quantity {
/// The default quantity of one.
pub fn one() -> Self {
Quantity::Bounded(NonZeroU64::new(1).unwrap())
}

fn to_tlv_record(&self) -> Option<u64> {
match self {
Quantity::Bounded(n) => {
let n = n.get();
if n == 1 { None } else { Some(n) }
},
Quantity::Bounded(n) => Some(n.get()),
Quantity::Unbounded => Some(0),
Quantity::One => None,
}
}
}
Expand DownExpand Up@@ -639,9 +636,8 @@ impl TryFrom<OfferTlvStream> for OfferContents {
.map(|seconds_from_epoch| Duration::from_secs(seconds_from_epoch));

let supported_quantity = match quantity_max {
None => Quantity::one(),
None => Quantity::One,
Some(0) => Quantity::Unbounded,
Some(1) => return Err(SemanticError::InvalidQuantity),
Some(n) => Quantity::Bounded(NonZeroU64::new(n).unwrap()),
};

Expand DownExpand Up@@ -708,7 +704,7 @@ mod tests {
assert!(!offer.is_expired());
assert_eq!(offer.paths(), &[]);
assert_eq!(offer.issuer(), None);
assert_eq!(offer.supported_quantity(), Quantity::one());
assert_eq!(offer.supported_quantity(), Quantity::One);
assert_eq!(offer.signing_pubkey(), pubkey(42));

assert_eq!(
Expand DownExpand Up@@ -930,14 +926,15 @@ mod tests {

#[test]
fn builds_offer_with_supported_quantity() {
let one = NonZeroU64::new(1).unwrap();
let ten = NonZeroU64::new(10).unwrap();

let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build()
.unwrap();
let tlv_stream = offer.as_tlv_stream();
assert_eq!(offer.supported_quantity(), Quantity::one());
assert_eq!(offer.supported_quantity(), Quantity::One);
assert_eq!(tlv_stream.quantity_max, None);

let offer = OfferBuilder::new("foo".into(), pubkey(42))
Expand All@@ -956,13 +953,21 @@ mod tests {
assert_eq!(offer.supported_quantity(), Quantity::Bounded(ten));
assert_eq!(tlv_stream.quantity_max, Some(10));

let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::Bounded(one))
.build()
.unwrap();
let tlv_stream = offer.as_tlv_stream();
assert_eq!(offer.supported_quantity(), Quantity::Bounded(one));
assert_eq!(tlv_stream.quantity_max, Some(1));

let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::Bounded(ten))
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build()
.unwrap();
let tlv_stream = offer.as_tlv_stream();
assert_eq!(offer.supported_quantity(), Quantity::one());
assert_eq!(offer.supported_quantity(), Quantity::One);
assert_eq!(tlv_stream.quantity_max, None);
}

Expand DownExpand Up@@ -1094,7 +1099,7 @@ mod tests {
#[test]
fn parses_offer_with_quantity() {
let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build()
.unwrap();
if let Err(e) = offer.to_string().parse::<Offer>() {
Expand All@@ -1117,17 +1122,12 @@ mod tests {
panic!("error parsing offer: {:?}", e);
}

let mut tlv_stream = offer.as_tlv_stream();
tlv_stream.quantity_max = Some(1);

let mut encoded_offer = Vec::new();
tlv_stream.write(&mut encoded_offer).unwrap();

match Offer::try_from(encoded_offer) {
Ok(_) => panic!("expected error"),
Err(e) => {
assert_eq!(e, ParseError::InvalidSemantics(SemanticError::InvalidQuantity));
},
let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::Bounded(NonZeroU64::new(1).unwrap()))
.build()
.unwrap();
if let Err(e) = offer.to_string().parse::<Offer>() {
panic!("error parsing offer: {:?}", e);
}
}

Expand Down
2 changes: 2 additions & 0 deletions lightning/src/offers/parse.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -157,6 +157,8 @@ pub enum SemanticError {
InvalidQuantity,
/// A quantity or quantity bounds was provided but was not expected.
UnexpectedQuantity,
/// Metadata was provided but was not expected.
UnexpectedMetadata,
/// Payer metadata was expected but was missing.
MissingPayerMetadata,
/// A payer id was expected but was missing.
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' BOLT 12 spec updates by jkczyz · Pull Request #1972 · lightningdevkit/rust-lightning · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions lightning/src/offers/invoice.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -338,8 +338,10 @@ struct InvoiceFields {

impl Invoice {
/// Paths to the recipient originating from publicly reachable nodes, including information
/// needed for routing payments across them. Blinded paths provide recipient privacy by
/// obfuscating its node id.
/// needed for routing payments across them.
///
/// Blinded paths provide recipient privacy by obfuscating its node id. Note, however, that this
/// privacy is lost if a public node id is used for [`Invoice::signing_pubkey`].
pub fn payment_paths(&self) -> &[(BlindedPath, BlindedPayInfo)] {
&self.contents.fields().payment_paths[..]
}
Expand Down
41 changes: 36 additions & 5 deletions lightning/src/offers/invoice_request.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -333,7 +333,9 @@ impl InvoiceRequest {
/// for the invoice.
///
/// The `payment_paths` parameter is useful for maintaining the payment recipient's privacy. It
/// must contain one or more elements.
/// must contain one or more elements ordered from most-preferred to least-preferred, if there's
/// a preference. Note, however, that any privacy is lost if a public node id was used for
/// [`Offer::signing_pubkey`].
///
/// Errors if the request contains unknown required features.
///
Expand DownExpand Up@@ -845,11 +847,12 @@ mod tests {

#[test]
fn builds_invoice_request_with_quantity() {
let one = NonZeroU64::new(1).unwrap();
let ten = NonZeroU64::new(10).unwrap();

let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
Expand All@@ -860,7 +863,7 @@ mod tests {

match OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.amount_msats(2_000).unwrap()
Expand DownExpand Up@@ -918,6 +921,17 @@ mod tests {
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(e, SemanticError::MissingQuantity),
}

match OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::Bounded(one))
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build()
{
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(e, SemanticError::MissingQuantity),
}
}

#[test]
Expand DownExpand Up@@ -1102,11 +1116,12 @@ mod tests {

#[test]
fn parses_invoice_request_with_quantity() {
let one = NonZeroU64::new(1).unwrap();
let ten = NonZeroU64::new(10).unwrap();

let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
Expand All@@ -1121,7 +1136,7 @@ mod tests {

let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.amount_msats(2_000).unwrap()
Comment thread
jkczyz marked this conversation as resolved.
Expand DownExpand Up@@ -1206,6 +1221,22 @@ mod tests {
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingQuantity)),
}

let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::Bounded(one))
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build_unchecked()
.sign(payer_sign).unwrap();

let mut buffer = Vec::new();
invoice_request.write(&mut buffer).unwrap();

match InvoiceRequest::try_from(buffer) {
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingQuantity)),
}
}

#[test]
Expand Down
76 changes: 38 additions & 38 deletions lightning/src/offers/offer.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -106,7 +106,7 @@ impl OfferBuilder {
let offer = OfferContents {
chains: None, metadata: None, amount: None, description,
features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None,
supported_quantity: Quantity::one(), signing_pubkey,
supported_quantity: Quantity::One, signing_pubkey,
};
OfferBuilder { offer }
}
Expand DownExpand Up@@ -178,7 +178,7 @@ impl OfferBuilder {
}

/// Sets the quantity of items for [`Offer::supported_quantity`]. If not called, defaults to
/// [`Quantity::one`].
/// [`Quantity::One`].
///
/// Successive calls to this method will override the previous setting.
pub fn supported_quantity(mut self, quantity: Quantity) -> Self {
Expand DownExpand Up@@ -464,19 +464,17 @@ impl OfferContents {

fn is_valid_quantity(&self, quantity: u64) -> bool {
match self.supported_quantity {
Quantity::Bounded(n) => {
let n = n.get();
if n == 1 { false }
else { quantity > 0 && quantity <= n }
},
Quantity::Bounded(n) => quantity <= n.get(),
Quantity::Unbounded => quantity > 0,
Quantity::One => quantity == 1,
}
}

fn expects_quantity(&self) -> bool {
match self.supported_quantity {
Quantity::Bounded(n) => n.get() != 1,
Quantity::Bounded(_) => true,
Quantity::Unbounded => true,
Quantity::One => false,
}
}

Expand DownExpand Up@@ -549,25 +547,24 @@ pub type CurrencyCode = [u8; 3];
/// Quantity of items supported by an [`Offer`].
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Quantity {
/// Up to a specific number of items (inclusive).
/// Up to a specific number of items (inclusive). Use when more than one item can be requested
/// but is limited (e.g., because of per customer or inventory limits).
///
/// May be used with `NonZeroU64::new(1)` but prefer to use [`Quantity::One`] if only one item

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you added this comment on the wrong variant. More generally, I'd expect this to tell me why I should use one or the other, not just that I should.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Fixed and further documented each variant. PTAL

/// is supported.
Bounded(NonZeroU64),
/// One or more items.
/// One or more items. Use when more than one item can be requested without any limit.
Unbounded,
/// Only one item. Use when only a single item can be requested.
One,
}

impl Quantity {
/// The default quantity of one.
pub fn one() -> Self {
Quantity::Bounded(NonZeroU64::new(1).unwrap())
}

fn to_tlv_record(&self) -> Option<u64> {
match self {
Quantity::Bounded(n) => {
let n = n.get();
if n == 1 { None } else { Some(n) }
},
Quantity::Bounded(n) => Some(n.get()),
Quantity::Unbounded => Some(0),
Quantity::One => None,
}
}
}
Expand DownExpand Up@@ -639,9 +636,8 @@ impl TryFrom<OfferTlvStream> for OfferContents {
.map(|seconds_from_epoch| Duration::from_secs(seconds_from_epoch));

let supported_quantity = match quantity_max {
None => Quantity::one(),
None => Quantity::One,
Some(0) => Quantity::Unbounded,
Some(1) => return Err(SemanticError::InvalidQuantity),
Some(n) => Quantity::Bounded(NonZeroU64::new(n).unwrap()),
};

Expand DownExpand Up@@ -708,7 +704,7 @@ mod tests {
assert!(!offer.is_expired());
assert_eq!(offer.paths(), &[]);
assert_eq!(offer.issuer(), None);
assert_eq!(offer.supported_quantity(), Quantity::one());
assert_eq!(offer.supported_quantity(), Quantity::One);
assert_eq!(offer.signing_pubkey(), pubkey(42));

assert_eq!(
Expand DownExpand Up@@ -930,14 +926,15 @@ mod tests {

#[test]
fn builds_offer_with_supported_quantity() {
let one = NonZeroU64::new(1).unwrap();
let ten = NonZeroU64::new(10).unwrap();

let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build()
.unwrap();
let tlv_stream = offer.as_tlv_stream();
assert_eq!(offer.supported_quantity(), Quantity::one());
assert_eq!(offer.supported_quantity(), Quantity::One);
assert_eq!(tlv_stream.quantity_max, None);

let offer = OfferBuilder::new("foo".into(), pubkey(42))
Expand All@@ -956,13 +953,21 @@ mod tests {
assert_eq!(offer.supported_quantity(), Quantity::Bounded(ten));
assert_eq!(tlv_stream.quantity_max, Some(10));

let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::Bounded(one))
.build()
.unwrap();
let tlv_stream = offer.as_tlv_stream();
assert_eq!(offer.supported_quantity(), Quantity::Bounded(one));
assert_eq!(tlv_stream.quantity_max, Some(1));

let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::Bounded(ten))
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build()
.unwrap();
let tlv_stream = offer.as_tlv_stream();
assert_eq!(offer.supported_quantity(), Quantity::one());
assert_eq!(offer.supported_quantity(), Quantity::One);
assert_eq!(tlv_stream.quantity_max, None);
}

Expand DownExpand Up@@ -1094,7 +1099,7 @@ mod tests {
#[test]
fn parses_offer_with_quantity() {
let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build()
.unwrap();
if let Err(e) = offer.to_string().parse::<Offer>() {
Expand All@@ -1117,17 +1122,12 @@ mod tests {
panic!("error parsing offer: {:?}", e);
}

let mut tlv_stream = offer.as_tlv_stream();
tlv_stream.quantity_max = Some(1);

let mut encoded_offer = Vec::new();
tlv_stream.write(&mut encoded_offer).unwrap();

match Offer::try_from(encoded_offer) {
Ok(_) => panic!("expected error"),
Err(e) => {
assert_eq!(e, ParseError::InvalidSemantics(SemanticError::InvalidQuantity));
},
let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::Bounded(NonZeroU64::new(1).unwrap()))
.build()
.unwrap();
if let Err(e) = offer.to_string().parse::<Offer>() {
panic!("error parsing offer: {:?}", e);
}
}

Expand Down
2 changes: 2 additions & 0 deletions lightning/src/offers/parse.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -157,6 +157,8 @@ pub enum SemanticError {
InvalidQuantity,
/// A quantity or quantity bounds was provided but was not expected.
UnexpectedQuantity,
/// Metadata was provided but was not expected.
UnexpectedMetadata,
/// Payer metadata was expected but was missing.
MissingPayerMetadata,
/// A payer id was expected but was missing.
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' BOLT 12 spec updates by jkczyz · Pull Request #1972 · lightningdevkit/rust-lightning · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions lightning/src/offers/invoice.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -338,8 +338,10 @@ struct InvoiceFields {

impl Invoice {
/// Paths to the recipient originating from publicly reachable nodes, including information
/// needed for routing payments across them. Blinded paths provide recipient privacy by
/// obfuscating its node id.
/// needed for routing payments across them.
///
/// Blinded paths provide recipient privacy by obfuscating its node id. Note, however, that this
/// privacy is lost if a public node id is used for [`Invoice::signing_pubkey`].
pub fn payment_paths(&self) -> &[(BlindedPath, BlindedPayInfo)] {
&self.contents.fields().payment_paths[..]
}
Expand Down
41 changes: 36 additions & 5 deletions lightning/src/offers/invoice_request.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -333,7 +333,9 @@ impl InvoiceRequest {
/// for the invoice.
///
/// The `payment_paths` parameter is useful for maintaining the payment recipient's privacy. It
/// must contain one or more elements.
/// must contain one or more elements ordered from most-preferred to least-preferred, if there's
/// a preference. Note, however, that any privacy is lost if a public node id was used for
/// [`Offer::signing_pubkey`].
///
/// Errors if the request contains unknown required features.
///
Expand DownExpand Up@@ -845,11 +847,12 @@ mod tests {

#[test]
fn builds_invoice_request_with_quantity() {
let one = NonZeroU64::new(1).unwrap();
let ten = NonZeroU64::new(10).unwrap();

let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
Expand All@@ -860,7 +863,7 @@ mod tests {

match OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.amount_msats(2_000).unwrap()
Expand DownExpand Up@@ -918,6 +921,17 @@ mod tests {
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(e, SemanticError::MissingQuantity),
}

match OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::Bounded(one))
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build()
{
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(e, SemanticError::MissingQuantity),
}
}

#[test]
Expand DownExpand Up@@ -1102,11 +1116,12 @@ mod tests {

#[test]
fn parses_invoice_request_with_quantity() {
let one = NonZeroU64::new(1).unwrap();
let ten = NonZeroU64::new(10).unwrap();

let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
Expand All@@ -1121,7 +1136,7 @@ mod tests {

let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.amount_msats(2_000).unwrap()
Comment thread
jkczyz marked this conversation as resolved.
Expand DownExpand Up@@ -1206,6 +1221,22 @@ mod tests {
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingQuantity)),
}

let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::Bounded(one))
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build_unchecked()
.sign(payer_sign).unwrap();

let mut buffer = Vec::new();
invoice_request.write(&mut buffer).unwrap();

match InvoiceRequest::try_from(buffer) {
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingQuantity)),
}
}

#[test]
Expand Down
76 changes: 38 additions & 38 deletions lightning/src/offers/offer.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -106,7 +106,7 @@ impl OfferBuilder {
let offer = OfferContents {
chains: None, metadata: None, amount: None, description,
features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None,
supported_quantity: Quantity::one(), signing_pubkey,
supported_quantity: Quantity::One, signing_pubkey,
};
OfferBuilder { offer }
}
Expand DownExpand Up@@ -178,7 +178,7 @@ impl OfferBuilder {
}

/// Sets the quantity of items for [`Offer::supported_quantity`]. If not called, defaults to
/// [`Quantity::one`].
/// [`Quantity::One`].
///
/// Successive calls to this method will override the previous setting.
pub fn supported_quantity(mut self, quantity: Quantity) -> Self {
Expand DownExpand Up@@ -464,19 +464,17 @@ impl OfferContents {

fn is_valid_quantity(&self, quantity: u64) -> bool {
match self.supported_quantity {
Quantity::Bounded(n) => {
let n = n.get();
if n == 1 { false }
else { quantity > 0 && quantity <= n }
},
Quantity::Bounded(n) => quantity <= n.get(),
Quantity::Unbounded => quantity > 0,
Quantity::One => quantity == 1,
}
}

fn expects_quantity(&self) -> bool {
match self.supported_quantity {
Quantity::Bounded(n) => n.get() != 1,
Quantity::Bounded(_) => true,
Quantity::Unbounded => true,
Quantity::One => false,
}
}

Expand DownExpand Up@@ -549,25 +547,24 @@ pub type CurrencyCode = [u8; 3];
/// Quantity of items supported by an [`Offer`].
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Quantity {
/// Up to a specific number of items (inclusive).
/// Up to a specific number of items (inclusive). Use when more than one item can be requested
/// but is limited (e.g., because of per customer or inventory limits).
///
/// May be used with `NonZeroU64::new(1)` but prefer to use [`Quantity::One`] if only one item

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you added this comment on the wrong variant. More generally, I'd expect this to tell me why I should use one or the other, not just that I should.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Fixed and further documented each variant. PTAL

/// is supported.
Bounded(NonZeroU64),
/// One or more items.
/// One or more items. Use when more than one item can be requested without any limit.
Unbounded,
/// Only one item. Use when only a single item can be requested.
One,
}

impl Quantity {
/// The default quantity of one.
pub fn one() -> Self {
Quantity::Bounded(NonZeroU64::new(1).unwrap())
}

fn to_tlv_record(&self) -> Option<u64> {
match self {
Quantity::Bounded(n) => {
let n = n.get();
if n == 1 { None } else { Some(n) }
},
Quantity::Bounded(n) => Some(n.get()),
Quantity::Unbounded => Some(0),
Quantity::One => None,
}
}
}
Expand DownExpand Up@@ -639,9 +636,8 @@ impl TryFrom<OfferTlvStream> for OfferContents {
.map(|seconds_from_epoch| Duration::from_secs(seconds_from_epoch));

let supported_quantity = match quantity_max {
None => Quantity::one(),
None => Quantity::One,
Some(0) => Quantity::Unbounded,
Some(1) => return Err(SemanticError::InvalidQuantity),
Some(n) => Quantity::Bounded(NonZeroU64::new(n).unwrap()),
};

Expand DownExpand Up@@ -708,7 +704,7 @@ mod tests {
assert!(!offer.is_expired());
assert_eq!(offer.paths(), &[]);
assert_eq!(offer.issuer(), None);
assert_eq!(offer.supported_quantity(), Quantity::one());
assert_eq!(offer.supported_quantity(), Quantity::One);
assert_eq!(offer.signing_pubkey(), pubkey(42));

assert_eq!(
Expand DownExpand Up@@ -930,14 +926,15 @@ mod tests {

#[test]
fn builds_offer_with_supported_quantity() {
let one = NonZeroU64::new(1).unwrap();
let ten = NonZeroU64::new(10).unwrap();

let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build()
.unwrap();
let tlv_stream = offer.as_tlv_stream();
assert_eq!(offer.supported_quantity(), Quantity::one());
assert_eq!(offer.supported_quantity(), Quantity::One);
assert_eq!(tlv_stream.quantity_max, None);

let offer = OfferBuilder::new("foo".into(), pubkey(42))
Expand All@@ -956,13 +953,21 @@ mod tests {
assert_eq!(offer.supported_quantity(), Quantity::Bounded(ten));
assert_eq!(tlv_stream.quantity_max, Some(10));

let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::Bounded(one))
.build()
.unwrap();
let tlv_stream = offer.as_tlv_stream();
assert_eq!(offer.supported_quantity(), Quantity::Bounded(one));
assert_eq!(tlv_stream.quantity_max, Some(1));

let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::Bounded(ten))
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build()
.unwrap();
let tlv_stream = offer.as_tlv_stream();
assert_eq!(offer.supported_quantity(), Quantity::one());
assert_eq!(offer.supported_quantity(), Quantity::One);
assert_eq!(tlv_stream.quantity_max, None);
}

Expand DownExpand Up@@ -1094,7 +1099,7 @@ mod tests {
#[test]
fn parses_offer_with_quantity() {
let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build()
.unwrap();
if let Err(e) = offer.to_string().parse::<Offer>() {
Expand All@@ -1117,17 +1122,12 @@ mod tests {
panic!("error parsing offer: {:?}", e);
}

let mut tlv_stream = offer.as_tlv_stream();
tlv_stream.quantity_max = Some(1);

let mut encoded_offer = Vec::new();
tlv_stream.write(&mut encoded_offer).unwrap();

match Offer::try_from(encoded_offer) {
Ok(_) => panic!("expected error"),
Err(e) => {
assert_eq!(e, ParseError::InvalidSemantics(SemanticError::InvalidQuantity));
},
let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::Bounded(NonZeroU64::new(1).unwrap()))
.build()
.unwrap();
if let Err(e) = offer.to_string().parse::<Offer>() {
panic!("error parsing offer: {:?}", e);
}
}

Expand Down
2 changes: 2 additions & 0 deletions lightning/src/offers/parse.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -157,6 +157,8 @@ pub enum SemanticError {
InvalidQuantity,
/// A quantity or quantity bounds was provided but was not expected.
UnexpectedQuantity,
/// Metadata was provided but was not expected.
UnexpectedMetadata,
/// Payer metadata was expected but was missing.
MissingPayerMetadata,
/// A payer id was expected but was missing.
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' BOLT 12 spec updates by jkczyz · Pull Request #1972 · lightningdevkit/rust-lightning · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions lightning/src/offers/invoice.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -338,8 +338,10 @@ struct InvoiceFields {

impl Invoice {
/// Paths to the recipient originating from publicly reachable nodes, including information
/// needed for routing payments across them. Blinded paths provide recipient privacy by
/// obfuscating its node id.
/// needed for routing payments across them.
///
/// Blinded paths provide recipient privacy by obfuscating its node id. Note, however, that this
/// privacy is lost if a public node id is used for [`Invoice::signing_pubkey`].
pub fn payment_paths(&self) -> &[(BlindedPath, BlindedPayInfo)] {
&self.contents.fields().payment_paths[..]
}
Expand Down
41 changes: 36 additions & 5 deletions lightning/src/offers/invoice_request.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -333,7 +333,9 @@ impl InvoiceRequest {
/// for the invoice.
///
/// The `payment_paths` parameter is useful for maintaining the payment recipient's privacy. It
/// must contain one or more elements.
/// must contain one or more elements ordered from most-preferred to least-preferred, if there's
/// a preference. Note, however, that any privacy is lost if a public node id was used for
/// [`Offer::signing_pubkey`].
///
/// Errors if the request contains unknown required features.
///
Expand DownExpand Up@@ -845,11 +847,12 @@ mod tests {

#[test]
fn builds_invoice_request_with_quantity() {
let one = NonZeroU64::new(1).unwrap();
let ten = NonZeroU64::new(10).unwrap();

let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
Expand All@@ -860,7 +863,7 @@ mod tests {

match OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.amount_msats(2_000).unwrap()
Expand DownExpand Up@@ -918,6 +921,17 @@ mod tests {
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(e, SemanticError::MissingQuantity),
}

match OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::Bounded(one))
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build()
{
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(e, SemanticError::MissingQuantity),
}
}

#[test]
Expand DownExpand Up@@ -1102,11 +1116,12 @@ mod tests {

#[test]
fn parses_invoice_request_with_quantity() {
let one = NonZeroU64::new(1).unwrap();
let ten = NonZeroU64::new(10).unwrap();

let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
Expand All@@ -1121,7 +1136,7 @@ mod tests {

let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.amount_msats(2_000).unwrap()
Comment thread
jkczyz marked this conversation as resolved.
Expand DownExpand Up@@ -1206,6 +1221,22 @@ mod tests {
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingQuantity)),
}

let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::Bounded(one))
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build_unchecked()
.sign(payer_sign).unwrap();

let mut buffer = Vec::new();
invoice_request.write(&mut buffer).unwrap();

match InvoiceRequest::try_from(buffer) {
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingQuantity)),
}
}

#[test]
Expand Down
76 changes: 38 additions & 38 deletions lightning/src/offers/offer.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -106,7 +106,7 @@ impl OfferBuilder {
let offer = OfferContents {
chains: None, metadata: None, amount: None, description,
features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None,
supported_quantity: Quantity::one(), signing_pubkey,
supported_quantity: Quantity::One, signing_pubkey,
};
OfferBuilder { offer }
}
Expand DownExpand Up@@ -178,7 +178,7 @@ impl OfferBuilder {
}

/// Sets the quantity of items for [`Offer::supported_quantity`]. If not called, defaults to
/// [`Quantity::one`].
/// [`Quantity::One`].
///
/// Successive calls to this method will override the previous setting.
pub fn supported_quantity(mut self, quantity: Quantity) -> Self {
Expand DownExpand Up@@ -464,19 +464,17 @@ impl OfferContents {

fn is_valid_quantity(&self, quantity: u64) -> bool {
match self.supported_quantity {
Quantity::Bounded(n) => {
let n = n.get();
if n == 1 { false }
else { quantity > 0 && quantity <= n }
},
Quantity::Bounded(n) => quantity <= n.get(),
Quantity::Unbounded => quantity > 0,
Quantity::One => quantity == 1,
}
}

fn expects_quantity(&self) -> bool {
match self.supported_quantity {
Quantity::Bounded(n) => n.get() != 1,
Quantity::Bounded(_) => true,
Quantity::Unbounded => true,
Quantity::One => false,
}
}

Expand DownExpand Up@@ -549,25 +547,24 @@ pub type CurrencyCode = [u8; 3];
/// Quantity of items supported by an [`Offer`].
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Quantity {
/// Up to a specific number of items (inclusive).
/// Up to a specific number of items (inclusive). Use when more than one item can be requested
/// but is limited (e.g., because of per customer or inventory limits).
///
/// May be used with `NonZeroU64::new(1)` but prefer to use [`Quantity::One`] if only one item

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you added this comment on the wrong variant. More generally, I'd expect this to tell me why I should use one or the other, not just that I should.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Fixed and further documented each variant. PTAL

/// is supported.
Bounded(NonZeroU64),
/// One or more items.
/// One or more items. Use when more than one item can be requested without any limit.
Unbounded,
/// Only one item. Use when only a single item can be requested.
One,
}

impl Quantity {
/// The default quantity of one.
pub fn one() -> Self {
Quantity::Bounded(NonZeroU64::new(1).unwrap())
}

fn to_tlv_record(&self) -> Option<u64> {
match self {
Quantity::Bounded(n) => {
let n = n.get();
if n == 1 { None } else { Some(n) }
},
Quantity::Bounded(n) => Some(n.get()),
Quantity::Unbounded => Some(0),
Quantity::One => None,
}
}
}
Expand DownExpand Up@@ -639,9 +636,8 @@ impl TryFrom<OfferTlvStream> for OfferContents {
.map(|seconds_from_epoch| Duration::from_secs(seconds_from_epoch));

let supported_quantity = match quantity_max {
None => Quantity::one(),
None => Quantity::One,
Some(0) => Quantity::Unbounded,
Some(1) => return Err(SemanticError::InvalidQuantity),
Some(n) => Quantity::Bounded(NonZeroU64::new(n).unwrap()),
};

Expand DownExpand Up@@ -708,7 +704,7 @@ mod tests {
assert!(!offer.is_expired());
assert_eq!(offer.paths(), &[]);
assert_eq!(offer.issuer(), None);
assert_eq!(offer.supported_quantity(), Quantity::one());
assert_eq!(offer.supported_quantity(), Quantity::One);
assert_eq!(offer.signing_pubkey(), pubkey(42));

assert_eq!(
Expand DownExpand Up@@ -930,14 +926,15 @@ mod tests {

#[test]
fn builds_offer_with_supported_quantity() {
let one = NonZeroU64::new(1).unwrap();
let ten = NonZeroU64::new(10).unwrap();

let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build()
.unwrap();
let tlv_stream = offer.as_tlv_stream();
assert_eq!(offer.supported_quantity(), Quantity::one());
assert_eq!(offer.supported_quantity(), Quantity::One);
assert_eq!(tlv_stream.quantity_max, None);

let offer = OfferBuilder::new("foo".into(), pubkey(42))
Expand All@@ -956,13 +953,21 @@ mod tests {
assert_eq!(offer.supported_quantity(), Quantity::Bounded(ten));
assert_eq!(tlv_stream.quantity_max, Some(10));

let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::Bounded(one))
.build()
.unwrap();
let tlv_stream = offer.as_tlv_stream();
assert_eq!(offer.supported_quantity(), Quantity::Bounded(one));
assert_eq!(tlv_stream.quantity_max, Some(1));

let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::Bounded(ten))
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build()
.unwrap();
let tlv_stream = offer.as_tlv_stream();
assert_eq!(offer.supported_quantity(), Quantity::one());
assert_eq!(offer.supported_quantity(), Quantity::One);
assert_eq!(tlv_stream.quantity_max, None);
}

Expand DownExpand Up@@ -1094,7 +1099,7 @@ mod tests {
#[test]
fn parses_offer_with_quantity() {
let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build()
.unwrap();
if let Err(e) = offer.to_string().parse::<Offer>() {
Expand All@@ -1117,17 +1122,12 @@ mod tests {
panic!("error parsing offer: {:?}", e);
}

let mut tlv_stream = offer.as_tlv_stream();
tlv_stream.quantity_max = Some(1);

let mut encoded_offer = Vec::new();
tlv_stream.write(&mut encoded_offer).unwrap();

match Offer::try_from(encoded_offer) {
Ok(_) => panic!("expected error"),
Err(e) => {
assert_eq!(e, ParseError::InvalidSemantics(SemanticError::InvalidQuantity));
},
let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::Bounded(NonZeroU64::new(1).unwrap()))
.build()
.unwrap();
if let Err(e) = offer.to_string().parse::<Offer>() {
panic!("error parsing offer: {:?}", e);
}
}

Expand Down
2 changes: 2 additions & 0 deletions lightning/src/offers/parse.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -157,6 +157,8 @@ pub enum SemanticError {
InvalidQuantity,
/// A quantity or quantity bounds was provided but was not expected.
UnexpectedQuantity,
/// Metadata was provided but was not expected.
UnexpectedMetadata,
/// Payer metadata was expected but was missing.
MissingPayerMetadata,
/// A payer id was expected but was missing.
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' BOLT 12 spec updates by jkczyz · Pull Request #1972 · lightningdevkit/rust-lightning · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions lightning/src/offers/invoice.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -338,8 +338,10 @@ struct InvoiceFields {

impl Invoice {
/// Paths to the recipient originating from publicly reachable nodes, including information
/// needed for routing payments across them. Blinded paths provide recipient privacy by
/// obfuscating its node id.
/// needed for routing payments across them.
///
/// Blinded paths provide recipient privacy by obfuscating its node id. Note, however, that this
/// privacy is lost if a public node id is used for [`Invoice::signing_pubkey`].
pub fn payment_paths(&self) -> &[(BlindedPath, BlindedPayInfo)] {
&self.contents.fields().payment_paths[..]
}
Expand Down
41 changes: 36 additions & 5 deletions lightning/src/offers/invoice_request.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -333,7 +333,9 @@ impl InvoiceRequest {
/// for the invoice.
///
/// The `payment_paths` parameter is useful for maintaining the payment recipient's privacy. It
/// must contain one or more elements.
/// must contain one or more elements ordered from most-preferred to least-preferred, if there's
/// a preference. Note, however, that any privacy is lost if a public node id was used for
/// [`Offer::signing_pubkey`].
///
/// Errors if the request contains unknown required features.
///
Expand DownExpand Up@@ -845,11 +847,12 @@ mod tests {

#[test]
fn builds_invoice_request_with_quantity() {
let one = NonZeroU64::new(1).unwrap();
let ten = NonZeroU64::new(10).unwrap();

let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
Expand All@@ -860,7 +863,7 @@ mod tests {

match OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.amount_msats(2_000).unwrap()
Expand DownExpand Up@@ -918,6 +921,17 @@ mod tests {
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(e, SemanticError::MissingQuantity),
}

match OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::Bounded(one))
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build()
{
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(e, SemanticError::MissingQuantity),
}
}

#[test]
Expand DownExpand Up@@ -1102,11 +1116,12 @@ mod tests {

#[test]
fn parses_invoice_request_with_quantity() {
let one = NonZeroU64::new(1).unwrap();
let ten = NonZeroU64::new(10).unwrap();

let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
Expand All@@ -1121,7 +1136,7 @@ mod tests {

let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.amount_msats(2_000).unwrap()
Comment thread
jkczyz marked this conversation as resolved.
Expand DownExpand Up@@ -1206,6 +1221,22 @@ mod tests {
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingQuantity)),
}

let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::Bounded(one))
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build_unchecked()
.sign(payer_sign).unwrap();

let mut buffer = Vec::new();
invoice_request.write(&mut buffer).unwrap();

match InvoiceRequest::try_from(buffer) {
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingQuantity)),
}
}

#[test]
Expand Down
76 changes: 38 additions & 38 deletions lightning/src/offers/offer.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -106,7 +106,7 @@ impl OfferBuilder {
let offer = OfferContents {
chains: None, metadata: None, amount: None, description,
features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None,
supported_quantity: Quantity::one(), signing_pubkey,
supported_quantity: Quantity::One, signing_pubkey,
};
OfferBuilder { offer }
}
Expand DownExpand Up@@ -178,7 +178,7 @@ impl OfferBuilder {
}

/// Sets the quantity of items for [`Offer::supported_quantity`]. If not called, defaults to
/// [`Quantity::one`].
/// [`Quantity::One`].
///
/// Successive calls to this method will override the previous setting.
pub fn supported_quantity(mut self, quantity: Quantity) -> Self {
Expand DownExpand Up@@ -464,19 +464,17 @@ impl OfferContents {

fn is_valid_quantity(&self, quantity: u64) -> bool {
match self.supported_quantity {
Quantity::Bounded(n) => {
let n = n.get();
if n == 1 { false }
else { quantity > 0 && quantity <= n }
},
Quantity::Bounded(n) => quantity <= n.get(),
Quantity::Unbounded => quantity > 0,
Quantity::One => quantity == 1,
}
}

fn expects_quantity(&self) -> bool {
match self.supported_quantity {
Quantity::Bounded(n) => n.get() != 1,
Quantity::Bounded(_) => true,
Quantity::Unbounded => true,
Quantity::One => false,
}
}

Expand DownExpand Up@@ -549,25 +547,24 @@ pub type CurrencyCode = [u8; 3];
/// Quantity of items supported by an [`Offer`].
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Quantity {
/// Up to a specific number of items (inclusive).
/// Up to a specific number of items (inclusive). Use when more than one item can be requested
/// but is limited (e.g., because of per customer or inventory limits).
///
/// May be used with `NonZeroU64::new(1)` but prefer to use [`Quantity::One`] if only one item

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you added this comment on the wrong variant. More generally, I'd expect this to tell me why I should use one or the other, not just that I should.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Fixed and further documented each variant. PTAL

/// is supported.
Bounded(NonZeroU64),
/// One or more items.
/// One or more items. Use when more than one item can be requested without any limit.
Unbounded,
/// Only one item. Use when only a single item can be requested.
One,
}

impl Quantity {
/// The default quantity of one.
pub fn one() -> Self {
Quantity::Bounded(NonZeroU64::new(1).unwrap())
}

fn to_tlv_record(&self) -> Option<u64> {
match self {
Quantity::Bounded(n) => {
let n = n.get();
if n == 1 { None } else { Some(n) }
},
Quantity::Bounded(n) => Some(n.get()),
Quantity::Unbounded => Some(0),
Quantity::One => None,
}
}
}
Expand DownExpand Up@@ -639,9 +636,8 @@ impl TryFrom<OfferTlvStream> for OfferContents {
.map(|seconds_from_epoch| Duration::from_secs(seconds_from_epoch));

let supported_quantity = match quantity_max {
None => Quantity::one(),
None => Quantity::One,
Some(0) => Quantity::Unbounded,
Some(1) => return Err(SemanticError::InvalidQuantity),
Some(n) => Quantity::Bounded(NonZeroU64::new(n).unwrap()),
};

Expand DownExpand Up@@ -708,7 +704,7 @@ mod tests {
assert!(!offer.is_expired());
assert_eq!(offer.paths(), &[]);
assert_eq!(offer.issuer(), None);
assert_eq!(offer.supported_quantity(), Quantity::one());
assert_eq!(offer.supported_quantity(), Quantity::One);
assert_eq!(offer.signing_pubkey(), pubkey(42));

assert_eq!(
Expand DownExpand Up@@ -930,14 +926,15 @@ mod tests {

#[test]
fn builds_offer_with_supported_quantity() {
let one = NonZeroU64::new(1).unwrap();
let ten = NonZeroU64::new(10).unwrap();

let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build()
.unwrap();
let tlv_stream = offer.as_tlv_stream();
assert_eq!(offer.supported_quantity(), Quantity::one());
assert_eq!(offer.supported_quantity(), Quantity::One);
assert_eq!(tlv_stream.quantity_max, None);

let offer = OfferBuilder::new("foo".into(), pubkey(42))
Expand All@@ -956,13 +953,21 @@ mod tests {
assert_eq!(offer.supported_quantity(), Quantity::Bounded(ten));
assert_eq!(tlv_stream.quantity_max, Some(10));

let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::Bounded(one))
.build()
.unwrap();
let tlv_stream = offer.as_tlv_stream();
assert_eq!(offer.supported_quantity(), Quantity::Bounded(one));
assert_eq!(tlv_stream.quantity_max, Some(1));

let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::Bounded(ten))
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build()
.unwrap();
let tlv_stream = offer.as_tlv_stream();
assert_eq!(offer.supported_quantity(), Quantity::one());
assert_eq!(offer.supported_quantity(), Quantity::One);
assert_eq!(tlv_stream.quantity_max, None);
}

Expand DownExpand Up@@ -1094,7 +1099,7 @@ mod tests {
#[test]
fn parses_offer_with_quantity() {
let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build()
.unwrap();
if let Err(e) = offer.to_string().parse::<Offer>() {
Expand All@@ -1117,17 +1122,12 @@ mod tests {
panic!("error parsing offer: {:?}", e);
}

let mut tlv_stream = offer.as_tlv_stream();
tlv_stream.quantity_max = Some(1);

let mut encoded_offer = Vec::new();
tlv_stream.write(&mut encoded_offer).unwrap();

match Offer::try_from(encoded_offer) {
Ok(_) => panic!("expected error"),
Err(e) => {
assert_eq!(e, ParseError::InvalidSemantics(SemanticError::InvalidQuantity));
},
let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::Bounded(NonZeroU64::new(1).unwrap()))
.build()
.unwrap();
if let Err(e) = offer.to_string().parse::<Offer>() {
panic!("error parsing offer: {:?}", e);
}
}

Expand Down
2 changes: 2 additions & 0 deletions lightning/src/offers/parse.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -157,6 +157,8 @@ pub enum SemanticError {
InvalidQuantity,
/// A quantity or quantity bounds was provided but was not expected.
UnexpectedQuantity,
/// Metadata was provided but was not expected.
UnexpectedMetadata,
/// Payer metadata was expected but was missing.
MissingPayerMetadata,
/// A payer id was expected but was missing.
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' BOLT 12 spec updates by jkczyz · Pull Request #1972 · lightningdevkit/rust-lightning · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions lightning/src/offers/invoice.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -338,8 +338,10 @@ struct InvoiceFields {

impl Invoice {
/// Paths to the recipient originating from publicly reachable nodes, including information
/// needed for routing payments across them. Blinded paths provide recipient privacy by
/// obfuscating its node id.
/// needed for routing payments across them.
///
/// Blinded paths provide recipient privacy by obfuscating its node id. Note, however, that this
/// privacy is lost if a public node id is used for [`Invoice::signing_pubkey`].
pub fn payment_paths(&self) -> &[(BlindedPath, BlindedPayInfo)] {
&self.contents.fields().payment_paths[..]
}
Expand Down
41 changes: 36 additions & 5 deletions lightning/src/offers/invoice_request.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -333,7 +333,9 @@ impl InvoiceRequest {
/// for the invoice.
///
/// The `payment_paths` parameter is useful for maintaining the payment recipient's privacy. It
/// must contain one or more elements.
/// must contain one or more elements ordered from most-preferred to least-preferred, if there's
/// a preference. Note, however, that any privacy is lost if a public node id was used for
/// [`Offer::signing_pubkey`].
///
/// Errors if the request contains unknown required features.
///
Expand DownExpand Up@@ -845,11 +847,12 @@ mod tests {

#[test]
fn builds_invoice_request_with_quantity() {
let one = NonZeroU64::new(1).unwrap();
let ten = NonZeroU64::new(10).unwrap();

let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
Expand All@@ -860,7 +863,7 @@ mod tests {

match OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.amount_msats(2_000).unwrap()
Expand DownExpand Up@@ -918,6 +921,17 @@ mod tests {
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(e, SemanticError::MissingQuantity),
}

match OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::Bounded(one))
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build()
{
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(e, SemanticError::MissingQuantity),
}
}

#[test]
Expand DownExpand Up@@ -1102,11 +1116,12 @@ mod tests {

#[test]
fn parses_invoice_request_with_quantity() {
let one = NonZeroU64::new(1).unwrap();
let ten = NonZeroU64::new(10).unwrap();

let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
Expand All@@ -1121,7 +1136,7 @@ mod tests {

let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.amount_msats(2_000).unwrap()
Comment thread
jkczyz marked this conversation as resolved.
Expand DownExpand Up@@ -1206,6 +1221,22 @@ mod tests {
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingQuantity)),
}

let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::Bounded(one))
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build_unchecked()
.sign(payer_sign).unwrap();

let mut buffer = Vec::new();
invoice_request.write(&mut buffer).unwrap();

match InvoiceRequest::try_from(buffer) {
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingQuantity)),
}
}

#[test]
Expand Down
76 changes: 38 additions & 38 deletions lightning/src/offers/offer.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -106,7 +106,7 @@ impl OfferBuilder {
let offer = OfferContents {
chains: None, metadata: None, amount: None, description,
features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None,
supported_quantity: Quantity::one(), signing_pubkey,
supported_quantity: Quantity::One, signing_pubkey,
};
OfferBuilder { offer }
}
Expand DownExpand Up@@ -178,7 +178,7 @@ impl OfferBuilder {
}

/// Sets the quantity of items for [`Offer::supported_quantity`]. If not called, defaults to
/// [`Quantity::one`].
/// [`Quantity::One`].
///
/// Successive calls to this method will override the previous setting.
pub fn supported_quantity(mut self, quantity: Quantity) -> Self {
Expand DownExpand Up@@ -464,19 +464,17 @@ impl OfferContents {

fn is_valid_quantity(&self, quantity: u64) -> bool {
match self.supported_quantity {
Quantity::Bounded(n) => {
let n = n.get();
if n == 1 { false }
else { quantity > 0 && quantity <= n }
},
Quantity::Bounded(n) => quantity <= n.get(),
Quantity::Unbounded => quantity > 0,
Quantity::One => quantity == 1,
}
}

fn expects_quantity(&self) -> bool {
match self.supported_quantity {
Quantity::Bounded(n) => n.get() != 1,
Quantity::Bounded(_) => true,
Quantity::Unbounded => true,
Quantity::One => false,
}
}

Expand DownExpand Up@@ -549,25 +547,24 @@ pub type CurrencyCode = [u8; 3];
/// Quantity of items supported by an [`Offer`].
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Quantity {
/// Up to a specific number of items (inclusive).
/// Up to a specific number of items (inclusive). Use when more than one item can be requested
/// but is limited (e.g., because of per customer or inventory limits).
///
/// May be used with `NonZeroU64::new(1)` but prefer to use [`Quantity::One`] if only one item

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you added this comment on the wrong variant. More generally, I'd expect this to tell me why I should use one or the other, not just that I should.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Fixed and further documented each variant. PTAL

/// is supported.
Bounded(NonZeroU64),
/// One or more items.
/// One or more items. Use when more than one item can be requested without any limit.
Unbounded,
/// Only one item. Use when only a single item can be requested.
One,
}

impl Quantity {
/// The default quantity of one.
pub fn one() -> Self {
Quantity::Bounded(NonZeroU64::new(1).unwrap())
}

fn to_tlv_record(&self) -> Option<u64> {
match self {
Quantity::Bounded(n) => {
let n = n.get();
if n == 1 { None } else { Some(n) }
},
Quantity::Bounded(n) => Some(n.get()),
Quantity::Unbounded => Some(0),
Quantity::One => None,
}
}
}
Expand DownExpand Up@@ -639,9 +636,8 @@ impl TryFrom<OfferTlvStream> for OfferContents {
.map(|seconds_from_epoch| Duration::from_secs(seconds_from_epoch));

let supported_quantity = match quantity_max {
None => Quantity::one(),
None => Quantity::One,
Some(0) => Quantity::Unbounded,
Some(1) => return Err(SemanticError::InvalidQuantity),
Some(n) => Quantity::Bounded(NonZeroU64::new(n).unwrap()),
};

Expand DownExpand Up@@ -708,7 +704,7 @@ mod tests {
assert!(!offer.is_expired());
assert_eq!(offer.paths(), &[]);
assert_eq!(offer.issuer(), None);
assert_eq!(offer.supported_quantity(), Quantity::one());
assert_eq!(offer.supported_quantity(), Quantity::One);
assert_eq!(offer.signing_pubkey(), pubkey(42));

assert_eq!(
Expand DownExpand Up@@ -930,14 +926,15 @@ mod tests {

#[test]
fn builds_offer_with_supported_quantity() {
let one = NonZeroU64::new(1).unwrap();
let ten = NonZeroU64::new(10).unwrap();

let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build()
.unwrap();
let tlv_stream = offer.as_tlv_stream();
assert_eq!(offer.supported_quantity(), Quantity::one());
assert_eq!(offer.supported_quantity(), Quantity::One);
assert_eq!(tlv_stream.quantity_max, None);

let offer = OfferBuilder::new("foo".into(), pubkey(42))
Expand All@@ -956,13 +953,21 @@ mod tests {
assert_eq!(offer.supported_quantity(), Quantity::Bounded(ten));
assert_eq!(tlv_stream.quantity_max, Some(10));

let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::Bounded(one))
.build()
.unwrap();
let tlv_stream = offer.as_tlv_stream();
assert_eq!(offer.supported_quantity(), Quantity::Bounded(one));
assert_eq!(tlv_stream.quantity_max, Some(1));

let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::Bounded(ten))
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build()
.unwrap();
let tlv_stream = offer.as_tlv_stream();
assert_eq!(offer.supported_quantity(), Quantity::one());
assert_eq!(offer.supported_quantity(), Quantity::One);
assert_eq!(tlv_stream.quantity_max, None);
}

Expand DownExpand Up@@ -1094,7 +1099,7 @@ mod tests {
#[test]
fn parses_offer_with_quantity() {
let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build()
.unwrap();
if let Err(e) = offer.to_string().parse::<Offer>() {
Expand All@@ -1117,17 +1122,12 @@ mod tests {
panic!("error parsing offer: {:?}", e);
}

let mut tlv_stream = offer.as_tlv_stream();
tlv_stream.quantity_max = Some(1);

let mut encoded_offer = Vec::new();
tlv_stream.write(&mut encoded_offer).unwrap();

match Offer::try_from(encoded_offer) {
Ok(_) => panic!("expected error"),
Err(e) => {
assert_eq!(e, ParseError::InvalidSemantics(SemanticError::InvalidQuantity));
},
let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::Bounded(NonZeroU64::new(1).unwrap()))
.build()
.unwrap();
if let Err(e) = offer.to_string().parse::<Offer>() {
panic!("error parsing offer: {:?}", e);
}
}

Expand Down
2 changes: 2 additions & 0 deletions lightning/src/offers/parse.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -157,6 +157,8 @@ pub enum SemanticError {
InvalidQuantity,
/// A quantity or quantity bounds was provided but was not expected.
UnexpectedQuantity,
/// Metadata was provided but was not expected.
UnexpectedMetadata,
/// Payer metadata was expected but was missing.
MissingPayerMetadata,
/// A payer id was expected but was missing.
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); BOLT 12 spec updates by jkczyz · Pull Request #1972 · lightningdevkit/rust-lightning · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions lightning/src/offers/invoice.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -338,8 +338,10 @@ struct InvoiceFields {

impl Invoice {
/// Paths to the recipient originating from publicly reachable nodes, including information
/// needed for routing payments across them. Blinded paths provide recipient privacy by
/// obfuscating its node id.
/// needed for routing payments across them.
///
/// Blinded paths provide recipient privacy by obfuscating its node id. Note, however, that this
/// privacy is lost if a public node id is used for [`Invoice::signing_pubkey`].
pub fn payment_paths(&self) -> &[(BlindedPath, BlindedPayInfo)] {
&self.contents.fields().payment_paths[..]
}
Expand Down
41 changes: 36 additions & 5 deletions lightning/src/offers/invoice_request.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -333,7 +333,9 @@ impl InvoiceRequest {
/// for the invoice.
///
/// The `payment_paths` parameter is useful for maintaining the payment recipient's privacy. It
/// must contain one or more elements.
/// must contain one or more elements ordered from most-preferred to least-preferred, if there's
/// a preference. Note, however, that any privacy is lost if a public node id was used for
/// [`Offer::signing_pubkey`].
///
/// Errors if the request contains unknown required features.
///
Expand DownExpand Up@@ -845,11 +847,12 @@ mod tests {

#[test]
fn builds_invoice_request_with_quantity() {
let one = NonZeroU64::new(1).unwrap();
let ten = NonZeroU64::new(10).unwrap();

let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
Expand All@@ -860,7 +863,7 @@ mod tests {

match OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.amount_msats(2_000).unwrap()
Expand DownExpand Up@@ -918,6 +921,17 @@ mod tests {
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(e, SemanticError::MissingQuantity),
}

match OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::Bounded(one))
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build()
{
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(e, SemanticError::MissingQuantity),
}
}

#[test]
Expand DownExpand Up@@ -1102,11 +1116,12 @@ mod tests {

#[test]
fn parses_invoice_request_with_quantity() {
let one = NonZeroU64::new(1).unwrap();
let ten = NonZeroU64::new(10).unwrap();

let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
Expand All@@ -1121,7 +1136,7 @@ mod tests {

let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.amount_msats(2_000).unwrap()
Comment thread
jkczyz marked this conversation as resolved.
Expand DownExpand Up@@ -1206,6 +1221,22 @@ mod tests {
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingQuantity)),
}

let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
.amount_msats(1000)
.supported_quantity(Quantity::Bounded(one))
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build_unchecked()
.sign(payer_sign).unwrap();

let mut buffer = Vec::new();
invoice_request.write(&mut buffer).unwrap();

match InvoiceRequest::try_from(buffer) {
Ok(_) => panic!("expected error"),
Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingQuantity)),
}
}

#[test]
Expand Down
76 changes: 38 additions & 38 deletions lightning/src/offers/offer.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -106,7 +106,7 @@ impl OfferBuilder {
let offer = OfferContents {
chains: None, metadata: None, amount: None, description,
features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None,
supported_quantity: Quantity::one(), signing_pubkey,
supported_quantity: Quantity::One, signing_pubkey,
};
OfferBuilder { offer }
}
Expand DownExpand Up@@ -178,7 +178,7 @@ impl OfferBuilder {
}

/// Sets the quantity of items for [`Offer::supported_quantity`]. If not called, defaults to
/// [`Quantity::one`].
/// [`Quantity::One`].
///
/// Successive calls to this method will override the previous setting.
pub fn supported_quantity(mut self, quantity: Quantity) -> Self {
Expand DownExpand Up@@ -464,19 +464,17 @@ impl OfferContents {

fn is_valid_quantity(&self, quantity: u64) -> bool {
match self.supported_quantity {
Quantity::Bounded(n) => {
let n = n.get();
if n == 1 { false }
else { quantity > 0 && quantity <= n }
},
Quantity::Bounded(n) => quantity <= n.get(),
Quantity::Unbounded => quantity > 0,
Quantity::One => quantity == 1,
}
}

fn expects_quantity(&self) -> bool {
match self.supported_quantity {
Quantity::Bounded(n) => n.get() != 1,
Quantity::Bounded(_) => true,
Quantity::Unbounded => true,
Quantity::One => false,
}
}

Expand DownExpand Up@@ -549,25 +547,24 @@ pub type CurrencyCode = [u8; 3];
/// Quantity of items supported by an [`Offer`].
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Quantity {
/// Up to a specific number of items (inclusive).
/// Up to a specific number of items (inclusive). Use when more than one item can be requested
/// but is limited (e.g., because of per customer or inventory limits).
///
/// May be used with `NonZeroU64::new(1)` but prefer to use [`Quantity::One`] if only one item

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you added this comment on the wrong variant. More generally, I'd expect this to tell me why I should use one or the other, not just that I should.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Fixed and further documented each variant. PTAL

/// is supported.
Bounded(NonZeroU64),
/// One or more items.
/// One or more items. Use when more than one item can be requested without any limit.
Unbounded,
/// Only one item. Use when only a single item can be requested.
One,
}

impl Quantity {
/// The default quantity of one.
pub fn one() -> Self {
Quantity::Bounded(NonZeroU64::new(1).unwrap())
}

fn to_tlv_record(&self) -> Option<u64> {
match self {
Quantity::Bounded(n) => {
let n = n.get();
if n == 1 { None } else { Some(n) }
},
Quantity::Bounded(n) => Some(n.get()),
Quantity::Unbounded => Some(0),
Quantity::One => None,
}
}
}
Expand DownExpand Up@@ -639,9 +636,8 @@ impl TryFrom<OfferTlvStream> for OfferContents {
.map(|seconds_from_epoch| Duration::from_secs(seconds_from_epoch));

let supported_quantity = match quantity_max {
None => Quantity::one(),
None => Quantity::One,
Some(0) => Quantity::Unbounded,
Some(1) => return Err(SemanticError::InvalidQuantity),
Some(n) => Quantity::Bounded(NonZeroU64::new(n).unwrap()),
};

Expand DownExpand Up@@ -708,7 +704,7 @@ mod tests {
assert!(!offer.is_expired());
assert_eq!(offer.paths(), &[]);
assert_eq!(offer.issuer(), None);
assert_eq!(offer.supported_quantity(), Quantity::one());
assert_eq!(offer.supported_quantity(), Quantity::One);
assert_eq!(offer.signing_pubkey(), pubkey(42));

assert_eq!(
Expand DownExpand Up@@ -930,14 +926,15 @@ mod tests {

#[test]
fn builds_offer_with_supported_quantity() {
let one = NonZeroU64::new(1).unwrap();
let ten = NonZeroU64::new(10).unwrap();

let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build()
.unwrap();
let tlv_stream = offer.as_tlv_stream();
assert_eq!(offer.supported_quantity(), Quantity::one());
assert_eq!(offer.supported_quantity(), Quantity::One);
assert_eq!(tlv_stream.quantity_max, None);

let offer = OfferBuilder::new("foo".into(), pubkey(42))
Expand All@@ -956,13 +953,21 @@ mod tests {
assert_eq!(offer.supported_quantity(), Quantity::Bounded(ten));
assert_eq!(tlv_stream.quantity_max, Some(10));

let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::Bounded(one))
.build()
.unwrap();
let tlv_stream = offer.as_tlv_stream();
assert_eq!(offer.supported_quantity(), Quantity::Bounded(one));
assert_eq!(tlv_stream.quantity_max, Some(1));

let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::Bounded(ten))
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build()
.unwrap();
let tlv_stream = offer.as_tlv_stream();
assert_eq!(offer.supported_quantity(), Quantity::one());
assert_eq!(offer.supported_quantity(), Quantity::One);
assert_eq!(tlv_stream.quantity_max, None);
}

Expand DownExpand Up@@ -1094,7 +1099,7 @@ mod tests {
#[test]
fn parses_offer_with_quantity() {
let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::one())
.supported_quantity(Quantity::One)
.build()
.unwrap();
if let Err(e) = offer.to_string().parse::<Offer>() {
Expand All@@ -1117,17 +1122,12 @@ mod tests {
panic!("error parsing offer: {:?}", e);
}

let mut tlv_stream = offer.as_tlv_stream();
tlv_stream.quantity_max = Some(1);

let mut encoded_offer = Vec::new();
tlv_stream.write(&mut encoded_offer).unwrap();

match Offer::try_from(encoded_offer) {
Ok(_) => panic!("expected error"),
Err(e) => {
assert_eq!(e, ParseError::InvalidSemantics(SemanticError::InvalidQuantity));
},
let offer = OfferBuilder::new("foo".into(), pubkey(42))
.supported_quantity(Quantity::Bounded(NonZeroU64::new(1).unwrap()))
.build()
.unwrap();
if let Err(e) = offer.to_string().parse::<Offer>() {
panic!("error parsing offer: {:?}", e);
}
}

Expand Down
2 changes: 2 additions & 0 deletions lightning/src/offers/parse.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -157,6 +157,8 @@ pub enum SemanticError {
InvalidQuantity,
/// A quantity or quantity bounds was provided but was not expected.
UnexpectedQuantity,
/// Metadata was provided but was not expected.
UnexpectedMetadata,
/// Payer metadata was expected but was missing.
MissingPayerMetadata,
/// A payer id was expected but was missing.
Expand Down
Loading