Skip to content

Commit

Permalink
Update BOLT12 offer to use lno key
Browse files Browse the repository at this point in the history
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
  • Loading branch information
slanesuke committed Jul 9, 2024
1 parent 98ce5e3 commit 94efe34
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 41 deletions.
122 changes: 84 additions & 38 deletions src/payment/unified_qr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ use lightning::offers::offer::Offer;
use std::sync::Arc;
use std::vec::IntoIter;

type LnUri<'a> = bip21::Uri<'a, NetworkChecked, Extras>;
type Uri<'a> = bip21::Uri<'a, NetworkChecked, Extras>;

#[derive(Debug, Clone)]
struct Extras {
Expand Down Expand Up @@ -104,18 +104,18 @@ impl UnifiedQrPayment {

let extras = Extras { bolt11_invoice, bolt12_offer };

let mut uri = LnUri::with_extras(onchain_address, extras);
let mut uri = Uri::with_extras(onchain_address, extras);
uri.amount = Some(Amount::from_sat(amount_sats));
uri.message = Some(message.into());

Ok(capitalize_qr_params(uri))
Ok(format_uri(uri))
}

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

/// Represents the [`PaymentId`] or [`Txid`] while using a [BIP 21] QR code.
/// `QrPaymentResult` represents the result of a payment made using a [BIP 21] QR code.
///
/// After a successful on-chain transaction, the transaction ID ([`Txid`]) is returned.
/// For BOLT11 and BOLT12 payments, the corresponding [`PaymentId`] is returned.
///
/// [BIP 21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki
/// [`PaymentId]: lightning::ln::channelmanager::PaymentId
/// [`Txid`]: bitcoin::hash_types::Txid
pub enum QrPaymentResult {
/// The transaction ID of the on-chain payment.
/// An on-chain payment.
Onchain {
///
/// The transaction ID (txid) of the on-chain payment.
txid: Txid,
},
/// The payment ID for the BOLT11 invoice.
/// A [BOLT 11] payment.
///
/// [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md
Bolt11 {
///
/// The payment ID for the BOLT11 invoice.
payment_id: PaymentId,
},
/// The payment ID for the BOLT12 offer.
/// A [BOLT 12] offer payment, i.e., a payment for an [`Offer`].
///
/// [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md
/// [`Offer`]: crate::lightning::offers::offer::Offer
Bolt12 {
///
/// The payment ID for the BOLT12 offer.
payment_id: PaymentId,
},
}

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

let mut start = 0;
while let Some(index) = uri[start..].find("lightning=") {
let start_index = start + index;
let end_index = uri[start_index..].find('&').map_or(uri.len(), |i| start_index + i);
let lightning_value = &uri[start_index + "lightning=".len()..end_index];
let uppercase_lighting_value = lightning_value.to_uppercase();
uri.replace_range(start_index + "lightning=".len()..end_index, &uppercase_lighting_value);

start = end_index
fn value_to_uppercase(uri: &mut String, key: &str) {
let mut start = 0;
while let Some(index) = uri[start..].find(key) {
let start_index = start + index;
let end_index = uri[start_index..].find('&').map_or(uri.len(), |i| start_index + i);
let lightning_value = &uri[start_index + key.len()..end_index];
let uppercase_lighting_value = lightning_value.to_uppercase();
uri.replace_range(start_index + key.len()..end_index, &uppercase_lighting_value);
start = end_index
}
}
value_to_uppercase(&mut uri, "lightning=");
value_to_uppercase(&mut uri, "lno=");
uri
}

Expand All @@ -204,7 +215,7 @@ impl<'a> SerializeParams for &'a Extras {
params.push(("lightning", bolt11_invoice.to_string()));
}
if let Some(bolt12_offer) = &self.bolt12_offer {
params.push(("lightning", bolt12_offer.to_string()));
params.push(("lno", bolt12_offer.to_string()));
}

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

fn is_param_known(&self, key: &str) -> bool {
key == "lightning"
key == "lightning" || key == "lno"
}

fn deserialize_temp(
&mut self, key: &str, value: Param<'_>,
) -> Result<ParamKind, <Self::Value as DeserializationError>::Error> {
if key == "lightning" {
let lighting_str =
String::try_from(value).map_err(|_| Error::UriParameterParsingFailed)?;

for param in lighting_str.split('&') {
if let Ok(offer) = param.parse::<Offer>() {
self.bolt12_offer = Some(offer);
} else if let Ok(invoice) = param.parse::<Bolt11Invoice>() {
self.bolt11_invoice = Some(invoice);
match key {
"lightning" => {
let bolt11_value =
String::try_from(value).map_err(|_| Error::UriParameterParsingFailed)?;
for param in bolt11_value.split('&') {
if let Ok(invoice) = param.parse::<Bolt11Invoice>() {
self.bolt11_invoice = Some(invoice);
}
}
}
Ok(bip21::de::ParamKind::Known)
} else {
Ok(bip21::de::ParamKind::Unknown)
Ok(bip21::de::ParamKind::Known)
},
"lno" => {
let bolt12_value =
String::try_from(value).map_err(|_| Error::UriParameterParsingFailed)?;
for param in bolt12_value.split('&') {
if let Ok(offer) = param.parse::<Offer>() {
self.bolt12_offer = Some(offer);
}
}
Ok(bip21::de::ParamKind::Known)
},
_ => Ok(bip21::de::ParamKind::Unknown),
}
}

Expand Down Expand Up @@ -289,6 +308,33 @@ mod tests {
panic!("No Lightning invoice found");
}

let uri_with_offer = "BITCOIN:BCRT1QM0NW9S05QDPGC6F52FPKA9U6Q6VWTT5WVS30R2?amount=0.001&message=asdf&lightning=LNBCRT1M1PNGMY98DQ8V9EKGESNP4QDH5SL00QK4842UZMZVJVX2NLUZT4E6P2ZC2DLAGCU565TP42AUDYPP5XD0PRS5CRDLZVU8DNQQU08W9F4YP0XRXW06ZSHCLCHZU9X28HSSSSP5ES30JG9J4VK2CRW80YXTLRJU2M097TXMFTHR00VC5V0LGKVMURRQ9QYYSGQCQPCXQRRAQRZJQ0Q0K9CDYFSVZAJ5V3PDWYWDMHLEYCVD7TG0SVMY4AM4P6GQZJZ5XQQQQYQQX2QQQUQQQQLGQQQQQQQQFQWDQZX24PSHN68A9D4X4HD89F3XVC7DGGRDTFCA5WH4KZ546GSRTJVACA34QQ3DZ9W4JHLJD3XZRW44RA0RET6RDSRJCEZQC6AXANX6QPHZKHJK&lno=LNO1QGSQVGNWGCG35Z6EE2H3YCZRADDM72XRFUA9UVE2RLRM9DEU7XYFZRCYZPGTGRDWMGU44QPYUXLHLLMLWN4QSPQ97HSSQZSYV9EKGESSWCPK7JRAAUZ6574TSTVFJFSE20LSFWH8G9GTPFHL4RRJN23VX4TH35SRWKCNQ6S8R9ZW9HU5RXMPXVYCJVK2KY3NTEA8VXZTMWJF4NAJCCAQZQ7YZ7KDDZ600LAW2S2E7Q6XDYLPSMLMV4YAY0QXX5NC8QH05JRNUYQPQCAHK8Y5KQ8H9X624LS6A9GWFTGKYYPUZVUKKM93DWETTL8A7NE84L7SNHCSGR006EACQRQP8YWY6WPS0TS";
let parsed_uri_with_offer = uri_with_offer
.parse::<bip21::Uri<NetworkUnchecked, Extras>>()
.expect("Failed Parsing")
.require_network(Network::Regtest)
.expect("Invalid Network");

assert_eq!(
parsed_uri_with_offer.address,
bitcoin::Address::from_str("BCRT1QM0NW9S05QDPGC6F52FPKA9U6Q6VWTT5WVS30R2")
.unwrap()
.require_network(Network::Regtest)
.unwrap()
);

if let Some(invoice) = parsed_uri_with_offer.extras.bolt11_invoice {
assert_eq!(invoice, Bolt11Invoice::from_str("LNBCRT1M1PNGMY98DQ8V9EKGESNP4QDH5SL00QK4842UZMZVJVX2NLUZT4E6P2ZC2DLAGCU565TP42AUDYPP5XD0PRS5CRDLZVU8DNQQU08W9F4YP0XRXW06ZSHCLCHZU9X28HSSSSP5ES30JG9J4VK2CRW80YXTLRJU2M097TXMFTHR00VC5V0LGKVMURRQ9QYYSGQCQPCXQRRAQRZJQ0Q0K9CDYFSVZAJ5V3PDWYWDMHLEYCVD7TG0SVMY4AM4P6GQZJZ5XQQQQYQQX2QQQUQQQQLGQQQQQQQQFQWDQZX24PSHN68A9D4X4HD89F3XVC7DGGRDTFCA5WH4KZ546GSRTJVACA34QQ3DZ9W4JHLJD3XZRW44RA0RET6RDSRJCEZQC6AXANX6QPHZKHJK").unwrap());
} else {
panic!("No invoice found.")
}

if let Some(offer) = parsed_uri_with_offer.extras.bolt12_offer {
assert_eq!(offer, Offer::from_str("LNO1QGSQVGNWGCG35Z6EE2H3YCZRADDM72XRFUA9UVE2RLRM9DEU7XYFZRCYZPGTGRDWMGU44QPYUXLHLLMLWN4QSPQ97HSSQZSYV9EKGESSWCPK7JRAAUZ6574TSTVFJFSE20LSFWH8G9GTPFHL4RRJN23VX4TH35SRWKCNQ6S8R9ZW9HU5RXMPXVYCJVK2KY3NTEA8VXZTMWJF4NAJCCAQZQ7YZ7KDDZ600LAW2S2E7Q6XDYLPSMLMV4YAY0QXX5NC8QH05JRNUYQPQCAHK8Y5KQ8H9X624LS6A9GWFTGKYYPUZVUKKM93DWETTL8A7NE84L7SNHCSGR006EACQRQP8YWY6WPS0TS").unwrap());
} else {
panic!("No offer found.");
}

let zeus_test = "bitcoin:TB1QQ32G6LM2XKT0U2UGASH5DC4CFT3JTPEW65PZZ5?lightning=LNTB500U1PN89HH6PP5MA7K6DRM5SYVD05NTXMGSRNM728J7EHM8KV6VC96YNLKN7G7VDYQDQQCQZRCXQR8Q7SP5HU30L0EEXKYYPQSQYEZELZWUPT62HLJ0KV2662CALGPAML50QPXQ9QXPQYSGQDKTVFXEC8H2DG2GY3C95ETAJ0QKX50XAUCU304PPFV2SQVGFHZ6RMZWJV8MC3M0LXF3GW852C5VSK0DELK0JHLYUTYZDF7QKNAMT4PQQQN24WM&amount=0.0005";
let uri_test2 = zeus_test
.parse::<bip21::Uri<NetworkUnchecked, Extras>>()
Expand All @@ -307,7 +353,7 @@ mod tests {
if let Some(invoice) = uri_test2.extras.bolt11_invoice {
assert_eq!(invoice, Bolt11Invoice::from_str("LNTB500U1PN89HH6PP5MA7K6DRM5SYVD05NTXMGSRNM728J7EHM8KV6VC96YNLKN7G7VDYQDQQCQZRCXQR8Q7SP5HU30L0EEXKYYPQSQYEZELZWUPT62HLJ0KV2662CALGPAML50QPXQ9QXPQYSGQDKTVFXEC8H2DG2GY3C95ETAJ0QKX50XAUCU304PPFV2SQVGFHZ6RMZWJV8MC3M0LXF3GW852C5VSK0DELK0JHLYUTYZDF7QKNAMT4PQQQN24WM").unwrap());
} else {
panic!("No offer found.");
panic!("No invoice found.");
}
assert_eq!(Amount::from(uri_test2.amount.unwrap()), Amount::from_sat(50000));

Expand Down Expand Up @@ -350,7 +396,7 @@ mod tests {
if let Some(invoice) = uri_test4.extras.bolt11_invoice {
assert_eq!(invoice, Bolt11Invoice::from_str("lnbc1pn8g249pp5f6ytj32ty90jhvw69enf30hwfgdhyymjewywcmfjevflg6s4z86qdqqcqzzgxqyz5vqrzjqwnvuc0u4txn35cafc7w94gxvq5p3cu9dd95f7hlrh0fvs46wpvhdfjjzh2j9f7ye5qqqqryqqqqthqqpysp5mm832athgcal3m7h35sc29j63lmgzvwc5smfjh2es65elc2ns7dq9qrsgqu2xcje2gsnjp0wn97aknyd3h58an7sjj6nhcrm40846jxphv47958c6th76whmec8ttr2wmg6sxwchvxmsc00kqrzqcga6lvsf9jtqgqy5yexa").unwrap());
} else {
panic!("No Invoice found");
panic!("No invoice found");
}
}
}
5 changes: 2 additions & 3 deletions tests/integration_tests_rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -585,8 +585,8 @@ fn generate_bip21_uri() {
Ok(ref uri) => {
println!("Generated URI: {}", uri);
assert!(uri.contains("BITCOIN:"));
let count = uri.matches("lightning=").count();
assert!(count >= 2, "Expected 2 lighting parameters in URI.");
assert!(uri.contains("lightning="));
assert!(uri.contains("lno="));
},
Err(e) => panic!("Failed to generate URI: {:?}", e),
}
Expand Down Expand Up @@ -630,7 +630,6 @@ fn unified_qr_send_receive() {

let uqr_payment = node_b.unified_qr_payment().receive(expected_amount_sats, "asdf", expiry_sec);
let uri_str = uqr_payment.clone().unwrap();

let offer_payment_id: PaymentId = match node_a.unified_qr_payment().send(&uri_str) {
Ok(QrPaymentResult::Bolt12 { payment_id }) => {
println!("\nBolt12 payment sent successfully with PaymentID: {:?}", payment_id);
Expand Down

0 comments on commit 94efe34

Please sign in to comment.