Skip to content

Commit 98ce5e3

Browse files
committed
Refactor URI parsing and add Bolt12 offer in receive
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
1 parent 9d01e28 commit 98ce5e3

File tree

2 files changed

+79
-109
lines changed

2 files changed

+79
-109
lines changed

src/payment/unified_qr.rs

Lines changed: 28 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ impl UnifiedQrPayment {
6161
Self { onchain_payment, bolt11_invoice, bolt12_payment, logger }
6262
}
6363

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

8687
let amount_msats = amount_sats * 1_000;
8788

89+
let bolt12_offer = match self.bolt12_payment.receive(amount_msats, message) {
90+
Ok(offer) => Some(offer),
91+
Err(e) => {
92+
log_error!(self.logger, "Failed to create offer: {}", e);
93+
None
94+
},
95+
};
96+
8897
let bolt11_invoice = match self.bolt11_invoice.receive(amount_msats, message, expiry_sec) {
8998
Ok(invoice) => Some(invoice),
9099
Err(e) => {
@@ -93,7 +102,7 @@ impl UnifiedQrPayment {
93102
},
94103
};
95104

96-
let extras = Extras { bolt11_invoice, bolt12_offer: None };
105+
let extras = Extras { bolt11_invoice, bolt12_offer };
97106

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

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

173-
if let Some(start) = uri.find("lightning=") {
174-
let end = uri[start..].find('&').map_or(uri.len(), |i| start + i);
175-
let lightning_value = &uri[start + "lightning=".len()..end];
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];
176187
let uppercase_lighting_value = lightning_value.to_uppercase();
177-
uri.replace_range(start + "lightning=".len()..end, &uppercase_lighting_value);
188+
uri.replace_range(start_index + "lightning=".len()..end_index, &uppercase_lighting_value);
189+
190+
start = end_index
178191
}
179192
uri
180193
}
@@ -190,6 +203,9 @@ impl<'a> SerializeParams for &'a Extras {
190203
if let Some(bolt11_invoice) = &self.bolt11_invoice {
191204
params.push(("lightning", bolt11_invoice.to_string()));
192205
}
206+
if let Some(bolt12_offer) = &self.bolt12_offer {
207+
params.push(("lightning", bolt12_offer.to_string()));
208+
}
193209

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

222-
for prefix in lighting_str.split('&') {
223-
let prefix_lowercase = prefix.to_lowercase();
224-
if prefix_lowercase.starts_with("lnbc")
225-
|| prefix_lowercase.starts_with("lntb")
226-
|| prefix_lowercase.starts_with("lnbcrt")
227-
|| prefix_lowercase.starts_with("lnsb")
228-
{
229-
let invoice =
230-
prefix.parse::<Bolt11Invoice>().map_err(|_| Error::InvalidInvoice)?;
231-
self.bolt11_invoice = Some(invoice)
232-
} else if prefix_lowercase.starts_with("lno") {
233-
let offer = prefix.parse::<Offer>().map_err(|_| Error::InvalidOffer)?;
234-
self.bolt12_offer = Some(offer)
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);
235243
}
236244
}
237245
Ok(bip21::de::ParamKind::Known)
@@ -252,86 +260,9 @@ impl DeserializationError for Extras {
252260
#[cfg(test)]
253261
mod tests {
254262
use super::*;
255-
use crate::builder::NodeBuilder;
256-
use crate::config::Config;
257263
use crate::payment::unified_qr::Extras;
258264
use bitcoin::{Address, Network};
259265
use std::str::FromStr;
260-
use std::sync::{Arc, RwLock};
261-
use tokio::runtime::Runtime;
262-
263-
fn unified_qr_payment_handler() -> UnifiedQrPayment {
264-
let mut config = Config::default();
265-
config.network = Network::Bitcoin;
266-
267-
let builder = NodeBuilder::from_config(config);
268-
let node = builder.build().unwrap();
269-
270-
let liquidity_source = &node.liquidity_source;
271-
let peer_store = &node.peer_store;
272-
let payment_store = &node.payment_store;
273-
let runtime = Arc::new(RwLock::new(Some(Runtime::new().unwrap())));
274-
let channel_mgr = &node.channel_manager;
275-
let connection_mgr = &node.connection_manager;
276-
let key_mgr = &node.keys_manager;
277-
let config = &node.config;
278-
let logger = &node.logger;
279-
280-
let bolt11_invoice = Bolt11Payment::new(
281-
runtime.clone(),
282-
channel_mgr.clone(),
283-
connection_mgr.clone(),
284-
key_mgr.clone(),
285-
liquidity_source.clone(),
286-
payment_store.clone(),
287-
peer_store.clone(),
288-
config.clone(),
289-
logger.clone(),
290-
);
291-
292-
let bolt12_offer = Bolt12Payment::new(
293-
runtime.clone(),
294-
channel_mgr.clone(),
295-
payment_store.clone(),
296-
logger.clone(),
297-
);
298-
299-
let wallet = &node.wallet;
300-
let onchain_payment = OnchainPayment::new(
301-
runtime.clone(),
302-
wallet.clone(),
303-
channel_mgr.clone(),
304-
config.clone(),
305-
logger.clone(),
306-
);
307-
308-
let unified_qr_payment = UnifiedQrPayment::new(
309-
Arc::new(onchain_payment),
310-
Arc::new(bolt11_invoice),
311-
Arc::new(bolt12_offer),
312-
logger.clone(),
313-
);
314-
unified_qr_payment
315-
}
316-
317-
#[test]
318-
fn create_uri() {
319-
let unified_qr_payment = unified_qr_payment_handler();
320-
321-
let amount_sats = 100_000_000;
322-
let message = "Test Message";
323-
let expiry_sec = 4000;
324-
325-
let uqr_payment = unified_qr_payment.receive(amount_sats, message, expiry_sec);
326-
match uqr_payment.clone() {
327-
Ok(ref uri) => {
328-
assert!(uri.contains("BITCOIN:"));
329-
assert!(uri.contains("lightning="));
330-
println!("Generated URI: {}\n", uri);
331-
},
332-
Err(e) => panic!("Failed to generate URI: {:?}", e),
333-
}
334-
}
335266

336267
#[test]
337268
fn parse_uri() {

tests/integration_tests_rust.rs

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,47 @@ fn simple_bolt12_send_receive() {
551551
assert_eq!(node_a_payments.first().unwrap().amount_msat, Some(overpaid_amount));
552552
}
553553

554+
#[test]
555+
fn generate_bip21_uri() {
556+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
557+
let (node_a, node_b) = setup_two_nodes(&electrsd, false, true, false);
558+
559+
let address_a = node_a.onchain_payment().new_address().unwrap();
560+
let premined_sats = 5_000_000;
561+
562+
premine_and_distribute_funds(
563+
&bitcoind.client,
564+
&electrsd.client,
565+
vec![address_a],
566+
Amount::from_sat(premined_sats),
567+
);
568+
569+
node_a.sync_wallets().unwrap();
570+
open_channel(&node_a, &node_b, 4_000_000, true, &electrsd);
571+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6);
572+
573+
node_a.sync_wallets().unwrap();
574+
node_b.sync_wallets().unwrap();
575+
576+
expect_channel_ready_event!(node_a, node_b.node_id());
577+
expect_channel_ready_event!(node_b, node_a.node_id());
578+
579+
let expected_amount_sats = 100_000;
580+
let expiry_sec = 4_000;
581+
582+
let uqr_payment = node_b.unified_qr_payment().receive(expected_amount_sats, "asdf", expiry_sec);
583+
584+
match uqr_payment.clone() {
585+
Ok(ref uri) => {
586+
println!("Generated URI: {}", uri);
587+
assert!(uri.contains("BITCOIN:"));
588+
let count = uri.matches("lightning=").count();
589+
assert!(count >= 2, "Expected 2 lighting parameters in URI.");
590+
},
591+
Err(e) => panic!("Failed to generate URI: {:?}", e),
592+
}
593+
}
594+
554595
#[test]
555596
fn unified_qr_send_receive() {
556597
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
@@ -584,21 +625,13 @@ fn unified_qr_send_receive() {
584625
// Sleep one more sec to make sure the node announcement propagates.
585626
std::thread::sleep(std::time::Duration::from_secs(1));
586627

587-
let expected_amount_msats = 100_000_000;
588-
let offer = node_b.bolt12_payment().receive(expected_amount_msats, "hi");
589-
590-
let offer_str = offer.clone().unwrap().to_string();
591-
let bolt12_offer_param = format!("&lightning={}", offer_str);
592-
593628
let expected_amount_sats = 100_000;
594629
let expiry_sec = 4_000;
595630

596631
let uqr_payment = node_b.unified_qr_payment().receive(expected_amount_sats, "asdf", expiry_sec);
597-
598632
let uri_str = uqr_payment.clone().unwrap();
599-
let uri_with_offer = format!("{}{}", uri_str, bolt12_offer_param);
600633

601-
let offer_payment_id: PaymentId = match node_a.unified_qr_payment().send(&uri_with_offer) {
634+
let offer_payment_id: PaymentId = match node_a.unified_qr_payment().send(&uri_str) {
602635
Ok(QrPaymentResult::Bolt12 { payment_id }) => {
603636
println!("\nBolt12 payment sent successfully with PaymentID: {:?}", payment_id);
604637
payment_id
@@ -616,9 +649,11 @@ fn unified_qr_send_receive() {
616649

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

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

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

0 commit comments

Comments
 (0)