Skip to content

Commit e8f9f2b

Browse files
committed
Update BOLT12 offer to use lno key
In this commit: - In serialize_params, BOLT12 offers were changed to be serialized with the `lno` key rather than the `lightning` key - During deserializing, I had to make the same update. Used a match to check whether it was a `lightning` or `lno` key and then parsed accordingly. - Next, a small name change: capitalize_qr_params to format_uri. Previously I changed the value after "&lightning" to all caps, but the "&lno=" value wasn't being changed. So, added a helper method inside format_uri to capitalize the values given the key! - Updated corresponding tests with `lno` update Small nits: - Updated QrPaymentResult with more thorough docs - Added a parsing test with an offer
1 parent bcdc558 commit e8f9f2b

File tree

2 files changed

+86
-41
lines changed

2 files changed

+86
-41
lines changed

src/payment/unified_qr.rs

Lines changed: 84 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ use lightning::offers::offer::Offer;
2929
use std::sync::Arc;
3030
use std::vec::IntoIter;
3131

32-
type LnUri<'a> = bip21::Uri<'a, NetworkChecked, Extras>;
32+
type Uri<'a> = bip21::Uri<'a, NetworkChecked, Extras>;
3333

3434
#[derive(Debug, Clone)]
3535
struct Extras {
@@ -104,18 +104,18 @@ impl UnifiedQrPayment {
104104

105105
let extras = Extras { bolt11_invoice, bolt12_offer };
106106

107-
let mut uri = LnUri::with_extras(onchain_address, extras);
107+
let mut uri = Uri::with_extras(onchain_address, extras);
108108
uri.amount = Some(Amount::from_sat(amount_sats));
109109
uri.message = Some(message.into());
110110

111-
Ok(capitalize_qr_params(uri))
111+
Ok(format_uri(uri))
112112
}
113113

114114
/// Sends a payment given a BIP21 URI.
115115
///
116116
/// This method parses the provided URI string and attempts to send the payment. If the URI
117117
/// has an offer and or invoice, it will try to pay the offer first followed by the invoice.
118-
/// If they both fail, the on-chain payment will be attempted.
118+
/// If they both fail, the on-chain payment will be paid.
119119
///
120120
/// Returns a `PaymentId` if the offer or invoice is paid, and a `Txid` if the on-chain
121121
/// transaction is paid, or an `Error` if there was an issue with parsing the URI,
@@ -153,42 +153,53 @@ impl UnifiedQrPayment {
153153
}
154154
}
155155

156-
/// Represents the [`PaymentId`] or [`Txid`] while using a [BIP 21] QR code.
156+
/// `QrPaymentResult` represents the result of a payment made using a [BIP 21] QR code.
157+
///
158+
/// After a successful on-chain transaction, the transaction ID ([`Txid`]) is returned.
159+
/// For BOLT11 and BOLT12 payments, the corresponding [`PaymentId`] is returned.
157160
///
158161
/// [BIP 21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki
159162
/// [`PaymentId]: lightning::ln::channelmanager::PaymentId
160163
/// [`Txid`]: bitcoin::hash_types::Txid
161164
pub enum QrPaymentResult {
162-
/// The transaction ID of the on-chain payment.
165+
/// An on-chain payment.
163166
Onchain {
164-
///
167+
/// The transaction ID (txid) of the on-chain payment.
165168
txid: Txid,
166169
},
167-
/// The payment ID for the BOLT11 invoice.
170+
/// A [BOLT 11] payment.
171+
///
172+
/// [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md
168173
Bolt11 {
169-
///
174+
/// The payment ID for the BOLT11 invoice.
170175
payment_id: PaymentId,
171176
},
172-
/// The payment ID for the BOLT12 offer.
177+
/// A [BOLT 12] offer payment, i.e., a payment for an [`Offer`].
178+
///
179+
/// [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md
180+
/// [`Offer`]: crate::lightning::offers::offer::Offer
173181
Bolt12 {
174-
///
182+
/// The payment ID for the BOLT12 offer.
175183
payment_id: PaymentId,
176184
},
177185
}
178186

179-
fn capitalize_qr_params(uri: bip21::Uri<NetworkChecked, Extras>) -> String {
187+
fn format_uri(uri: bip21::Uri<NetworkChecked, Extras>) -> String {
180188
let mut uri = format!("{:#}", uri);
181189

182-
let mut start = 0;
183-
while let Some(index) = uri[start..].find("lightning=") {
184-
let start_index = start + index;
185-
let end_index = uri[start_index..].find('&').map_or(uri.len(), |i| start_index + i);
186-
let lightning_value = &uri[start_index + "lightning=".len()..end_index];
187-
let uppercase_lighting_value = lightning_value.to_uppercase();
188-
uri.replace_range(start_index + "lightning=".len()..end_index, &uppercase_lighting_value);
189-
190-
start = end_index
190+
fn value_to_uppercase(uri: &mut String, key: &str) {
191+
let mut start = 0;
192+
while let Some(index) = uri[start..].find(key) {
193+
let start_index = start + index;
194+
let end_index = uri[start_index..].find('&').map_or(uri.len(), |i| start_index + i);
195+
let lightning_value = &uri[start_index + key.len()..end_index];
196+
let uppercase_lighting_value = lightning_value.to_uppercase();
197+
uri.replace_range(start_index + key.len()..end_index, &uppercase_lighting_value);
198+
start = end_index
199+
}
191200
}
201+
value_to_uppercase(&mut uri, "lightning=");
202+
value_to_uppercase(&mut uri, "lno=");
192203
uri
193204
}
194205

@@ -204,7 +215,7 @@ impl<'a> SerializeParams for &'a Extras {
204215
params.push(("lightning", bolt11_invoice.to_string()));
205216
}
206217
if let Some(bolt12_offer) = &self.bolt12_offer {
207-
params.push(("lightning", bolt12_offer.to_string()));
218+
params.push(("lno", bolt12_offer.to_string()));
208219
}
209220

210221
params.into_iter()
@@ -225,26 +236,34 @@ impl<'a> bip21::de::DeserializationState<'a> for DeserializationState {
225236
type Value = Extras;
226237

227238
fn is_param_known(&self, key: &str) -> bool {
228-
key == "lightning"
239+
key == "lightning" || key == "lno"
229240
}
230241

231242
fn deserialize_temp(
232243
&mut self, key: &str, value: Param<'_>,
233244
) -> Result<ParamKind, <Self::Value as DeserializationError>::Error> {
234-
if key == "lightning" {
235-
let lighting_str =
236-
String::try_from(value).map_err(|_| Error::UriParameterParsingFailed)?;
237-
238-
for param in lighting_str.split('&') {
239-
if let Ok(offer) = param.parse::<Offer>() {
240-
self.bolt12_offer = Some(offer);
241-
} else if let Ok(invoice) = param.parse::<Bolt11Invoice>() {
242-
self.bolt11_invoice = Some(invoice);
245+
match key {
246+
"lightning" => {
247+
let bolt11_value =
248+
String::try_from(value).map_err(|_| Error::UriParameterParsingFailed)?;
249+
for param in bolt11_value.split('&') {
250+
if let Ok(invoice) = param.parse::<Bolt11Invoice>() {
251+
self.bolt11_invoice = Some(invoice);
252+
}
243253
}
244-
}
245-
Ok(bip21::de::ParamKind::Known)
246-
} else {
247-
Ok(bip21::de::ParamKind::Unknown)
254+
Ok(bip21::de::ParamKind::Known)
255+
},
256+
"lno" => {
257+
let bolt12_value =
258+
String::try_from(value).map_err(|_| Error::UriParameterParsingFailed)?;
259+
for param in bolt12_value.split('&') {
260+
if let Ok(offer) = param.parse::<Offer>() {
261+
self.bolt12_offer = Some(offer);
262+
}
263+
}
264+
Ok(bip21::de::ParamKind::Known)
265+
},
266+
_ => Ok(bip21::de::ParamKind::Unknown),
248267
}
249268
}
250269

@@ -289,6 +308,33 @@ mod tests {
289308
panic!("No Lightning invoice found");
290309
}
291310

311+
let uri_with_offer = "BITCOIN:BCRT1QM0NW9S05QDPGC6F52FPKA9U6Q6VWTT5WVS30R2?amount=0.001&message=asdf&lightning=LNBCRT1M1PNGMY98DQ8V9EKGESNP4QDH5SL00QK4842UZMZVJVX2NLUZT4E6P2ZC2DLAGCU565TP42AUDYPP5XD0PRS5CRDLZVU8DNQQU08W9F4YP0XRXW06ZSHCLCHZU9X28HSSSSP5ES30JG9J4VK2CRW80YXTLRJU2M097TXMFTHR00VC5V0LGKVMURRQ9QYYSGQCQPCXQRRAQRZJQ0Q0K9CDYFSVZAJ5V3PDWYWDMHLEYCVD7TG0SVMY4AM4P6GQZJZ5XQQQQYQQX2QQQUQQQQLGQQQQQQQQFQWDQZX24PSHN68A9D4X4HD89F3XVC7DGGRDTFCA5WH4KZ546GSRTJVACA34QQ3DZ9W4JHLJD3XZRW44RA0RET6RDSRJCEZQC6AXANX6QPHZKHJK&lno=LNO1QGSQVGNWGCG35Z6EE2H3YCZRADDM72XRFUA9UVE2RLRM9DEU7XYFZRCYZPGTGRDWMGU44QPYUXLHLLMLWN4QSPQ97HSSQZSYV9EKGESSWCPK7JRAAUZ6574TSTVFJFSE20LSFWH8G9GTPFHL4RRJN23VX4TH35SRWKCNQ6S8R9ZW9HU5RXMPXVYCJVK2KY3NTEA8VXZTMWJF4NAJCCAQZQ7YZ7KDDZ600LAW2S2E7Q6XDYLPSMLMV4YAY0QXX5NC8QH05JRNUYQPQCAHK8Y5KQ8H9X624LS6A9GWFTGKYYPUZVUKKM93DWETTL8A7NE84L7SNHCSGR006EACQRQP8YWY6WPS0TS";
312+
let parsed_uri_with_offer = uri_with_offer
313+
.parse::<bip21::Uri<NetworkUnchecked, Extras>>()
314+
.expect("Failed Parsing")
315+
.require_network(Network::Regtest)
316+
.expect("Invalid Network");
317+
318+
assert_eq!(
319+
parsed_uri_with_offer.address,
320+
bitcoin::Address::from_str("BCRT1QM0NW9S05QDPGC6F52FPKA9U6Q6VWTT5WVS30R2")
321+
.unwrap()
322+
.require_network(Network::Regtest)
323+
.unwrap()
324+
);
325+
326+
if let Some(invoice) = parsed_uri_with_offer.extras.bolt11_invoice {
327+
assert_eq!(invoice, Bolt11Invoice::from_str("LNBCRT1M1PNGMY98DQ8V9EKGESNP4QDH5SL00QK4842UZMZVJVX2NLUZT4E6P2ZC2DLAGCU565TP42AUDYPP5XD0PRS5CRDLZVU8DNQQU08W9F4YP0XRXW06ZSHCLCHZU9X28HSSSSP5ES30JG9J4VK2CRW80YXTLRJU2M097TXMFTHR00VC5V0LGKVMURRQ9QYYSGQCQPCXQRRAQRZJQ0Q0K9CDYFSVZAJ5V3PDWYWDMHLEYCVD7TG0SVMY4AM4P6GQZJZ5XQQQQYQQX2QQQUQQQQLGQQQQQQQQFQWDQZX24PSHN68A9D4X4HD89F3XVC7DGGRDTFCA5WH4KZ546GSRTJVACA34QQ3DZ9W4JHLJD3XZRW44RA0RET6RDSRJCEZQC6AXANX6QPHZKHJK").unwrap());
328+
} else {
329+
panic!("No invoice found.")
330+
}
331+
332+
if let Some(offer) = parsed_uri_with_offer.extras.bolt12_offer {
333+
assert_eq!(offer, Offer::from_str("LNO1QGSQVGNWGCG35Z6EE2H3YCZRADDM72XRFUA9UVE2RLRM9DEU7XYFZRCYZPGTGRDWMGU44QPYUXLHLLMLWN4QSPQ97HSSQZSYV9EKGESSWCPK7JRAAUZ6574TSTVFJFSE20LSFWH8G9GTPFHL4RRJN23VX4TH35SRWKCNQ6S8R9ZW9HU5RXMPXVYCJVK2KY3NTEA8VXZTMWJF4NAJCCAQZQ7YZ7KDDZ600LAW2S2E7Q6XDYLPSMLMV4YAY0QXX5NC8QH05JRNUYQPQCAHK8Y5KQ8H9X624LS6A9GWFTGKYYPUZVUKKM93DWETTL8A7NE84L7SNHCSGR006EACQRQP8YWY6WPS0TS").unwrap());
334+
} else {
335+
panic!("No offer found.");
336+
}
337+
292338
let zeus_test = "bitcoin:TB1QQ32G6LM2XKT0U2UGASH5DC4CFT3JTPEW65PZZ5?lightning=LNTB500U1PN89HH6PP5MA7K6DRM5SYVD05NTXMGSRNM728J7EHM8KV6VC96YNLKN7G7VDYQDQQCQZRCXQR8Q7SP5HU30L0EEXKYYPQSQYEZELZWUPT62HLJ0KV2662CALGPAML50QPXQ9QXPQYSGQDKTVFXEC8H2DG2GY3C95ETAJ0QKX50XAUCU304PPFV2SQVGFHZ6RMZWJV8MC3M0LXF3GW852C5VSK0DELK0JHLYUTYZDF7QKNAMT4PQQQN24WM&amount=0.0005";
293339
let uri_test2 = zeus_test
294340
.parse::<bip21::Uri<NetworkUnchecked, Extras>>()
@@ -307,7 +353,7 @@ mod tests {
307353
if let Some(invoice) = uri_test2.extras.bolt11_invoice {
308354
assert_eq!(invoice, Bolt11Invoice::from_str("LNTB500U1PN89HH6PP5MA7K6DRM5SYVD05NTXMGSRNM728J7EHM8KV6VC96YNLKN7G7VDYQDQQCQZRCXQR8Q7SP5HU30L0EEXKYYPQSQYEZELZWUPT62HLJ0KV2662CALGPAML50QPXQ9QXPQYSGQDKTVFXEC8H2DG2GY3C95ETAJ0QKX50XAUCU304PPFV2SQVGFHZ6RMZWJV8MC3M0LXF3GW852C5VSK0DELK0JHLYUTYZDF7QKNAMT4PQQQN24WM").unwrap());
309355
} else {
310-
panic!("No offer found.");
356+
panic!("No invoice found.");
311357
}
312358
assert_eq!(Amount::from(uri_test2.amount.unwrap()), Amount::from_sat(50000));
313359

@@ -350,7 +396,7 @@ mod tests {
350396
if let Some(invoice) = uri_test4.extras.bolt11_invoice {
351397
assert_eq!(invoice, Bolt11Invoice::from_str("lnbc1pn8g249pp5f6ytj32ty90jhvw69enf30hwfgdhyymjewywcmfjevflg6s4z86qdqqcqzzgxqyz5vqrzjqwnvuc0u4txn35cafc7w94gxvq5p3cu9dd95f7hlrh0fvs46wpvhdfjjzh2j9f7ye5qqqqryqqqqthqqpysp5mm832athgcal3m7h35sc29j63lmgzvwc5smfjh2es65elc2ns7dq9qrsgqu2xcje2gsnjp0wn97aknyd3h58an7sjj6nhcrm40846jxphv47958c6th76whmec8ttr2wmg6sxwchvxmsc00kqrzqcga6lvsf9jtqgqy5yexa").unwrap());
352398
} else {
353-
panic!("No Invoice found");
399+
panic!("No invoice found");
354400
}
355401
}
356402
}

tests/integration_tests_rust.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -585,8 +585,8 @@ fn generate_bip21_uri() {
585585
Ok(ref uri) => {
586586
println!("Generated URI: {}", uri);
587587
assert!(uri.contains("BITCOIN:"));
588-
let count = uri.matches("lightning=").count();
589-
assert!(count >= 2, "Expected 2 lighting parameters in URI.");
588+
assert!(uri.contains("lightning="));
589+
assert!(uri.contains("lno="));
590590
},
591591
Err(e) => panic!("Failed to generate URI: {:?}", e),
592592
}
@@ -630,7 +630,6 @@ fn unified_qr_send_receive() {
630630

631631
let uqr_payment = node_b.unified_qr_payment().receive(expected_amount_sats, "asdf", expiry_sec);
632632
let uri_str = uqr_payment.clone().unwrap();
633-
634633
let offer_payment_id: PaymentId = match node_a.unified_qr_payment().send(&uri_str) {
635634
Ok(QrPaymentResult::Bolt12 { payment_id }) => {
636635
println!("\nBolt12 payment sent successfully with PaymentID: {:?}", payment_id);

0 commit comments

Comments
 (0)