Skip to content

Commit

Permalink
Refactor URI parsing and add Bolt12 offer in receive
Browse files Browse the repository at this point in the history
Changes include:
  - Modified serialize_params to serialize both invoices and offers
  - Refactored deserialize_temp by removing the code that was
    parsing based on the lightning invoice/offer prefix. I instead
    used for loop to iterate over each lightning parameter,
    attempting to parse the string as an offer first, and then as an
    invoice. May need to log an error if neither succeeds
  - Added support for Bolt12 offers in the receive method
  - Updated capitalize_params function to handle multiple lightning
    parameters
  - Added a generate_bip21_uri test to show what the uri looks
    like in integration_tests_rust
  - Adjusted integration tests. Still needs work

Still trying to figure out a bug related to Bolt12 offers being
"paid" when it should fall back to an on-chain tx
  • Loading branch information
slanesuke committed Jul 5, 2024
1 parent 9d01e28 commit 98ce5e3
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 109 deletions.
125 changes: 28 additions & 97 deletions src/payment/unified_qr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ impl UnifiedQrPayment {
Self { onchain_payment, bolt11_invoice, bolt12_payment, logger }
}

/// Generates a URI with an on-chain address and [BOLT 11] invoice.
/// Generates a URI with an on-chain address, [BOLT 11] invoice and [BOLT 12] offer.
///
/// The URI allows users to send the payment request allowing the wallet to decide
/// which payment method to use. This enables a fallback mechanism: older wallets
Expand All @@ -78,13 +78,22 @@ impl UnifiedQrPayment {
/// The generated URI can then be given to a QR code library.
///
/// [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md
/// [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md
pub fn receive(
&self, amount_sats: u64, message: &str, expiry_sec: u32,
) -> Result<String, Error> {
let onchain_address = self.onchain_payment.new_address()?;

let amount_msats = amount_sats * 1_000;

let bolt12_offer = match self.bolt12_payment.receive(amount_msats, message) {
Ok(offer) => Some(offer),
Err(e) => {
log_error!(self.logger, "Failed to create offer: {}", e);
None
},
};

let bolt11_invoice = match self.bolt11_invoice.receive(amount_msats, message, expiry_sec) {
Ok(invoice) => Some(invoice),
Err(e) => {
Expand All @@ -93,7 +102,7 @@ impl UnifiedQrPayment {
},
};

let extras = Extras { bolt11_invoice, bolt12_offer: None };
let extras = Extras { bolt11_invoice, bolt12_offer };

let mut uri = LnUri::with_extras(onchain_address, extras);
uri.amount = Some(Amount::from_sat(amount_sats));
Expand Down Expand Up @@ -126,7 +135,7 @@ impl UnifiedQrPayment {
if let Some(offer) = uri.extras.bolt12_offer {
match self.bolt12_payment.send(&offer, None) {
Ok(payment_id) => return Ok(QrPaymentResult::Bolt12 { payment_id }),
Err(e) => log_error!(self.logger, "Failed to generate BOLT12 offer: {:?}", e),
Err(e) => log_error!(self.logger, "Failed to send BOLT12 offer: {:?}", e),
}
}

Expand Down Expand Up @@ -170,11 +179,15 @@ pub enum QrPaymentResult {
fn capitalize_qr_params(uri: bip21::Uri<NetworkChecked, Extras>) -> String {
let mut uri = format!("{:#}", uri);

if let Some(start) = uri.find("lightning=") {
let end = uri[start..].find('&').map_or(uri.len(), |i| start + i);
let lightning_value = &uri[start + "lightning=".len()..end];
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 + "lightning=".len()..end, &uppercase_lighting_value);
uri.replace_range(start_index + "lightning=".len()..end_index, &uppercase_lighting_value);

start = end_index
}
uri
}
Expand All @@ -190,6 +203,9 @@ impl<'a> SerializeParams for &'a Extras {
if let Some(bolt11_invoice) = &self.bolt11_invoice {
params.push(("lightning", bolt11_invoice.to_string()));
}
if let Some(bolt12_offer) = &self.bolt12_offer {
params.push(("lightning", bolt12_offer.to_string()));
}

params.into_iter()
}
Expand Down Expand Up @@ -219,19 +235,11 @@ impl<'a> bip21::de::DeserializationState<'a> for DeserializationState {
let lighting_str =
String::try_from(value).map_err(|_| Error::UriParameterParsingFailed)?;

for prefix in lighting_str.split('&') {
let prefix_lowercase = prefix.to_lowercase();
if prefix_lowercase.starts_with("lnbc")
|| prefix_lowercase.starts_with("lntb")
|| prefix_lowercase.starts_with("lnbcrt")
|| prefix_lowercase.starts_with("lnsb")
{
let invoice =
prefix.parse::<Bolt11Invoice>().map_err(|_| Error::InvalidInvoice)?;
self.bolt11_invoice = Some(invoice)
} else if prefix_lowercase.starts_with("lno") {
let offer = prefix.parse::<Offer>().map_err(|_| Error::InvalidOffer)?;
self.bolt12_offer = Some(offer)
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);
}
}
Ok(bip21::de::ParamKind::Known)
Expand All @@ -252,86 +260,9 @@ impl DeserializationError for Extras {
#[cfg(test)]
mod tests {
use super::*;
use crate::builder::NodeBuilder;
use crate::config::Config;
use crate::payment::unified_qr::Extras;
use bitcoin::{Address, Network};
use std::str::FromStr;
use std::sync::{Arc, RwLock};
use tokio::runtime::Runtime;

fn unified_qr_payment_handler() -> UnifiedQrPayment {
let mut config = Config::default();
config.network = Network::Bitcoin;

let builder = NodeBuilder::from_config(config);
let node = builder.build().unwrap();

let liquidity_source = &node.liquidity_source;
let peer_store = &node.peer_store;
let payment_store = &node.payment_store;
let runtime = Arc::new(RwLock::new(Some(Runtime::new().unwrap())));
let channel_mgr = &node.channel_manager;
let connection_mgr = &node.connection_manager;
let key_mgr = &node.keys_manager;
let config = &node.config;
let logger = &node.logger;

let bolt11_invoice = Bolt11Payment::new(
runtime.clone(),
channel_mgr.clone(),
connection_mgr.clone(),
key_mgr.clone(),
liquidity_source.clone(),
payment_store.clone(),
peer_store.clone(),
config.clone(),
logger.clone(),
);

let bolt12_offer = Bolt12Payment::new(
runtime.clone(),
channel_mgr.clone(),
payment_store.clone(),
logger.clone(),
);

let wallet = &node.wallet;
let onchain_payment = OnchainPayment::new(
runtime.clone(),
wallet.clone(),
channel_mgr.clone(),
config.clone(),
logger.clone(),
);

let unified_qr_payment = UnifiedQrPayment::new(
Arc::new(onchain_payment),
Arc::new(bolt11_invoice),
Arc::new(bolt12_offer),
logger.clone(),
);
unified_qr_payment
}

#[test]
fn create_uri() {
let unified_qr_payment = unified_qr_payment_handler();

let amount_sats = 100_000_000;
let message = "Test Message";
let expiry_sec = 4000;

let uqr_payment = unified_qr_payment.receive(amount_sats, message, expiry_sec);
match uqr_payment.clone() {
Ok(ref uri) => {
assert!(uri.contains("BITCOIN:"));
assert!(uri.contains("lightning="));
println!("Generated URI: {}\n", uri);
},
Err(e) => panic!("Failed to generate URI: {:?}", e),
}
}

#[test]
fn parse_uri() {
Expand Down
63 changes: 51 additions & 12 deletions tests/integration_tests_rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,47 @@ fn simple_bolt12_send_receive() {
assert_eq!(node_a_payments.first().unwrap().amount_msat, Some(overpaid_amount));
}

#[test]
fn generate_bip21_uri() {
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
let (node_a, node_b) = setup_two_nodes(&electrsd, false, true, false);

let address_a = node_a.onchain_payment().new_address().unwrap();
let premined_sats = 5_000_000;

premine_and_distribute_funds(
&bitcoind.client,
&electrsd.client,
vec![address_a],
Amount::from_sat(premined_sats),
);

node_a.sync_wallets().unwrap();
open_channel(&node_a, &node_b, 4_000_000, true, &electrsd);
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6);

node_a.sync_wallets().unwrap();
node_b.sync_wallets().unwrap();

expect_channel_ready_event!(node_a, node_b.node_id());
expect_channel_ready_event!(node_b, node_a.node_id());

let expected_amount_sats = 100_000;
let expiry_sec = 4_000;

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

match uqr_payment.clone() {
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.");
},
Err(e) => panic!("Failed to generate URI: {:?}", e),
}
}

#[test]
fn unified_qr_send_receive() {
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
Expand Down Expand Up @@ -584,21 +625,13 @@ fn unified_qr_send_receive() {
// Sleep one more sec to make sure the node announcement propagates.
std::thread::sleep(std::time::Duration::from_secs(1));

let expected_amount_msats = 100_000_000;
let offer = node_b.bolt12_payment().receive(expected_amount_msats, "hi");

let offer_str = offer.clone().unwrap().to_string();
let bolt12_offer_param = format!("&lightning={}", offer_str);

let expected_amount_sats = 100_000;
let expiry_sec = 4_000;

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

let uri_str = uqr_payment.clone().unwrap();
let uri_with_offer = format!("{}{}", uri_str, bolt12_offer_param);

let offer_payment_id: PaymentId = match node_a.unified_qr_payment().send(&uri_with_offer) {
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);
payment_id
Expand All @@ -616,9 +649,11 @@ fn unified_qr_send_receive() {

expect_payment_successful_event!(node_a, Some(offer_payment_id), None);

let uri_with_invalid_offer = format!("{}{}", uri_str, "&lightning=some_invalid_offer");
// Removed one character from the offer to fall back on to invoice.
// Still needs work
let uri_str_with_invalid_offer = &uri_str[..uri_str.len() - 1];
let invoice_payment_id: PaymentId =
match node_a.unified_qr_payment().send(&uri_with_invalid_offer) {
match node_a.unified_qr_payment().send(uri_str_with_invalid_offer) {
Ok(QrPaymentResult::Bolt12 { payment_id: _ }) => {
panic!("Expected Bolt11 payment but got Bolt12");
},
Expand All @@ -639,7 +674,11 @@ fn unified_qr_send_receive() {
let onchain_uqr_payment =
node_b.unified_qr_payment().receive(expect_onchain_amount_sats, "asdf", 4_000).unwrap();

let txid = match node_a.unified_qr_payment().send(onchain_uqr_payment.as_str()) {
// Removed a character from the offer, so it would move on to the other parameters.
let txid = match node_a
.unified_qr_payment()
.send(&onchain_uqr_payment.as_str()[..onchain_uqr_payment.len() - 1])
{
Ok(QrPaymentResult::Bolt12 { payment_id: _ }) => {
panic!("Expected on-chain payment but got Bolt12")
},
Expand Down

0 comments on commit 98ce5e3

Please sign in to comment.