1use std::sync::Arc;
15use std::vec::IntoIter;
16
17use bip21::de::ParamKind;
18use bip21::{DeserializationError, DeserializeParams, Param, SerializeParams};
19use bitcoin::address::{NetworkChecked, NetworkUnchecked};
20use bitcoin::{Amount, Txid};
21use lightning::ln::channelmanager::PaymentId;
22use lightning::offers::offer::Offer;
23use lightning::routing::router::RouteParametersConfig;
24use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description};
25
26use crate::error::Error;
27use crate::ffi::maybe_wrap;
28use crate::logger::{log_error, LdkLogger, Logger};
29use crate::payment::{Bolt11Payment, Bolt12Payment, OnchainPayment};
30use crate::Config;
31
32type Uri<'a> = bip21::Uri<'a, NetworkChecked, Extras>;
33
34#[derive(Debug, Clone)]
35struct Extras {
36 bolt11_invoice: Option<Bolt11Invoice>,
37 bolt12_offer: Option<Offer>,
38}
39
40pub struct UnifiedQrPayment {
50 onchain_payment: Arc<OnchainPayment>,
51 bolt11_invoice: Arc<Bolt11Payment>,
52 bolt12_payment: Arc<Bolt12Payment>,
53 config: Arc<Config>,
54 logger: Arc<Logger>,
55}
56
57impl UnifiedQrPayment {
58 pub(crate) fn new(
59 onchain_payment: Arc<OnchainPayment>, bolt11_invoice: Arc<Bolt11Payment>,
60 bolt12_payment: Arc<Bolt12Payment>, config: Arc<Config>, logger: Arc<Logger>,
61 ) -> Self {
62 Self { onchain_payment, bolt11_invoice, bolt12_payment, config, logger }
63 }
64
65 pub fn receive(
92 &self, amount_sats: u64, description: &str, expiry_sec: u32,
93 ) -> Result<String, Error> {
94 let onchain_address = self.onchain_payment.new_address()?;
95
96 let amount_msats = amount_sats * 1_000;
97
98 let bolt12_offer =
99 match self.bolt12_payment.receive_inner(amount_msats, description, None, None) {
100 Ok(offer) => Some(offer),
101 Err(e) => {
102 log_error!(self.logger, "Failed to create offer: {}", e);
103 None
104 },
105 };
106
107 let invoice_description = Bolt11InvoiceDescription::Direct(
108 Description::new(description.to_string()).map_err(|_| Error::InvoiceCreationFailed)?,
109 );
110 let bolt11_invoice = match self.bolt11_invoice.receive_inner(
111 Some(amount_msats),
112 &invoice_description,
113 expiry_sec,
114 None,
115 ) {
116 Ok(invoice) => Some(invoice),
117 Err(e) => {
118 log_error!(self.logger, "Failed to create invoice {}", e);
119 None
120 },
121 };
122
123 let extras = Extras { bolt11_invoice, bolt12_offer };
124
125 let mut uri = Uri::with_extras(onchain_address, extras);
126 uri.amount = Some(Amount::from_sat(amount_sats));
127 uri.message = Some(description.into());
128
129 Ok(format_uri(uri))
130 }
131
132 pub fn send(
146 &self, uri_str: &str, route_parameters: Option<RouteParametersConfig>,
147 ) -> Result<QrPaymentResult, Error> {
148 let uri: bip21::Uri<NetworkUnchecked, Extras> =
149 uri_str.parse().map_err(|_| Error::InvalidUri)?;
150
151 let uri_network_checked =
152 uri.clone().require_network(self.config.network).map_err(|_| Error::InvalidNetwork)?;
153
154 if let Some(offer) = uri_network_checked.extras.bolt12_offer {
155 let offer = maybe_wrap(offer);
156 match self.bolt12_payment.send(&offer, None, None, route_parameters) {
157 Ok(payment_id) => return Ok(QrPaymentResult::Bolt12 { payment_id }),
158 Err(e) => log_error!(self.logger, "Failed to send BOLT12 offer: {:?}. This is part of a unified QR code payment. Falling back to the BOLT11 invoice.", e),
159 }
160 }
161
162 if let Some(invoice) = uri_network_checked.extras.bolt11_invoice {
163 let invoice = maybe_wrap(invoice);
164 match self.bolt11_invoice.send(&invoice, route_parameters) {
165 Ok(payment_id) => return Ok(QrPaymentResult::Bolt11 { payment_id }),
166 Err(e) => log_error!(self.logger, "Failed to send BOLT11 invoice: {:?}. This is part of a unified QR code payment. Falling back to the on-chain transaction.", e),
167 }
168 }
169
170 let amount = match uri_network_checked.amount {
171 Some(amount) => amount,
172 None => {
173 log_error!(self.logger, "No amount specified in the URI. Aborting the payment.");
174 return Err(Error::InvalidAmount);
175 },
176 };
177
178 let txid = self.onchain_payment.send_to_address(
179 &uri_network_checked.address,
180 amount.to_sat(),
181 None,
182 )?;
183
184 Ok(QrPaymentResult::Onchain { txid })
185 }
186}
187
188#[derive(Debug)]
197pub enum QrPaymentResult {
198 Onchain {
200 txid: Txid,
202 },
203 Bolt11 {
207 payment_id: PaymentId,
209 },
210 Bolt12 {
215 payment_id: PaymentId,
217 },
218}
219
220fn format_uri(uri: bip21::Uri<NetworkChecked, Extras>) -> String {
221 let mut uri = format!("{:#}", uri);
222
223 fn value_to_uppercase(uri: &mut String, key: &str) {
224 let mut start = 0;
225 while let Some(index) = uri[start..].find(key) {
226 let start_index = start + index;
227 let end_index = uri[start_index..].find('&').map_or(uri.len(), |i| start_index + i);
228 let lightning_value = &uri[start_index + key.len()..end_index];
229 let uppercase_lighting_value = lightning_value.to_uppercase();
230 uri.replace_range(start_index + key.len()..end_index, &uppercase_lighting_value);
231 start = end_index
232 }
233 }
234 value_to_uppercase(&mut uri, "lightning=");
235 value_to_uppercase(&mut uri, "lno=");
236 uri
237}
238
239impl<'a> SerializeParams for &'a Extras {
240 type Key = &'static str;
241 type Value = String;
242 type Iterator = IntoIter<(Self::Key, Self::Value)>;
243
244 fn serialize_params(self) -> Self::Iterator {
245 let mut params = Vec::new();
246
247 if let Some(bolt11_invoice) = &self.bolt11_invoice {
248 params.push(("lightning", bolt11_invoice.to_string()));
249 }
250 if let Some(bolt12_offer) = &self.bolt12_offer {
251 params.push(("lno", bolt12_offer.to_string()));
252 }
253
254 params.into_iter()
255 }
256}
257
258impl<'a> DeserializeParams<'a> for Extras {
259 type DeserializationState = DeserializationState;
260}
261
262#[derive(Default)]
263struct DeserializationState {
264 bolt11_invoice: Option<Bolt11Invoice>,
265 bolt12_offer: Option<Offer>,
266}
267
268impl<'a> bip21::de::DeserializationState<'a> for DeserializationState {
269 type Value = Extras;
270
271 fn is_param_known(&self, key: &str) -> bool {
272 key == "lightning" || key == "lno"
273 }
274
275 fn deserialize_temp(
276 &mut self, key: &str, value: Param<'_>,
277 ) -> Result<ParamKind, <Self::Value as DeserializationError>::Error> {
278 match key {
279 "lightning" => {
280 let bolt11_value =
281 String::try_from(value).map_err(|_| Error::UriParameterParsingFailed)?;
282 let invoice = bolt11_value
283 .parse::<Bolt11Invoice>()
284 .map_err(|_| Error::UriParameterParsingFailed)?;
285 self.bolt11_invoice = Some(invoice);
286 Ok(bip21::de::ParamKind::Known)
287 },
288 "lno" => {
289 let bolt12_value =
290 String::try_from(value).map_err(|_| Error::UriParameterParsingFailed)?;
291 let offer =
292 bolt12_value.parse::<Offer>().map_err(|_| Error::UriParameterParsingFailed)?;
293 self.bolt12_offer = Some(offer);
294 Ok(bip21::de::ParamKind::Known)
295 },
296 _ => Ok(bip21::de::ParamKind::Unknown),
297 }
298 }
299
300 fn finalize(self) -> Result<Self::Value, <Self::Value as DeserializationError>::Error> {
301 Ok(Extras { bolt11_invoice: self.bolt11_invoice, bolt12_offer: self.bolt12_offer })
302 }
303}
304
305impl DeserializationError for Extras {
306 type Error = Error;
307}
308
309#[cfg(test)]
310mod tests {
311 use std::str::FromStr;
312
313 use bitcoin::{Address, Network};
314
315 use super::*;
316 use crate::payment::unified_qr::Extras;
317
318 #[test]
319 fn parse_uri() {
320 let uri_test1 = "BITCOIN:TB1QRSCD05XNY6QZ63TF9GJELGVK6D3UDJFEKK62VU?amount=1&message=Test%20message&lightning=LNTB1000M1PNXWM7MDQ523JHXAPQD4JHXUMPVAJSNP4QWP9QD2JFP8DUZ46JQG5LTKVDH04YG52G6UF2YAXP8H7YZPZM3DM5PP5KUP7YT429UP9Z4ACPA60R7WETSTL66549MG05P0JN0C4L2NCC40SSP5R0LH86DJCL0NK8HZHNZHX92VVUAAVNE48Z5RVKVY5DKTRQ0DMP7S9QYYSGQCQPCXQRRAQYR59FGN2VVC5R6DS0AZMETH493ZU56H0WSVMGYCW9LEPZ032PGQNZMQ6XKVEH90Z02C0NH3J5QGDAWCS2YC2ZNP22J0ZD0PPF78N4QQQEXTYS2";
321 let expected_bolt11_invoice_1 = "LNTB1000M1PNXWM7MDQ523JHXAPQD4JHXUMPVAJSNP4QWP9QD2JFP8DUZ46JQG5LTKVDH04YG52G6UF2YAXP8H7YZPZM3DM5PP5KUP7YT429UP9Z4ACPA60R7WETSTL66549MG05P0JN0C4L2NCC40SSP5R0LH86DJCL0NK8HZHNZHX92VVUAAVNE48Z5RVKVY5DKTRQ0DMP7S9QYYSGQCQPCXQRRAQYR59FGN2VVC5R6DS0AZMETH493ZU56H0WSVMGYCW9LEPZ032PGQNZMQ6XKVEH90Z02C0NH3J5QGDAWCS2YC2ZNP22J0ZD0PPF78N4QQQEXTYS2";
322 let parsed_uri = uri_test1
323 .parse::<bip21::Uri<NetworkUnchecked, Extras>>()
324 .expect("Failed Parsing")
325 .require_network(Network::Testnet)
326 .expect("Invalid Network");
327
328 assert_eq!(
329 parsed_uri.address,
330 bitcoin::Address::from_str("TB1QRSCD05XNY6QZ63TF9GJELGVK6D3UDJFEKK62VU")
331 .unwrap()
332 .require_network(Network::Testnet)
333 .unwrap()
334 );
335
336 assert_eq!(Amount::from_sat(100_000_000), Amount::from(parsed_uri.amount.unwrap()));
337
338 if let Some(invoice) = parsed_uri.extras.bolt11_invoice {
339 assert_eq!(invoice, Bolt11Invoice::from_str(expected_bolt11_invoice_1).unwrap());
340 } else {
341 panic!("No Lightning invoice found");
342 }
343
344 let uri_with_offer = "BITCOIN:BCRT1QM0NW9S05QDPGC6F52FPKA9U6Q6VWTT5WVS30R2?amount=0.001&message=asdf&lightning=LNBCRT1M1PNGMY98DQ8V9EKGESNP4QDH5SL00QK4842UZMZVJVX2NLUZT4E6P2ZC2DLAGCU565TP42AUDYPP5XD0PRS5CRDLZVU8DNQQU08W9F4YP0XRXW06ZSHCLCHZU9X28HSSSSP5ES30JG9J4VK2CRW80YXTLRJU2M097TXMFTHR00VC5V0LGKVMURRQ9QYYSGQCQPCXQRRAQRZJQ0Q0K9CDYFSVZAJ5V3PDWYWDMHLEYCVD7TG0SVMY4AM4P6GQZJZ5XQQQQYQQX2QQQUQQQQLGQQQQQQQQFQWDQZX24PSHN68A9D4X4HD89F3XVC7DGGRDTFCA5WH4KZ546GSRTJVACA34QQ3DZ9W4JHLJD3XZRW44RA0RET6RDSRJCEZQC6AXANX6QPHZKHJK&lno=LNO1QGSQVGNWGCG35Z6EE2H3YCZRADDM72XRFUA9UVE2RLRM9DEU7XYFZRCYZPGTGRDWMGU44QPYUXLHLLMLWN4QSPQ97HSSQZSYV9EKGESSWCPK7JRAAUZ6574TSTVFJFSE20LSFWH8G9GTPFHL4RRJN23VX4TH35SRWKCNQ6S8R9ZW9HU5RXMPXVYCJVK2KY3NTEA8VXZTMWJF4NAJCCAQZQ7YZ7KDDZ600LAW2S2E7Q6XDYLPSMLMV4YAY0QXX5NC8QH05JRNUYQPQCAHK8Y5KQ8H9X624LS6A9GWFTGKYYPUZVUKKM93DWETTL8A7NE84L7SNHCSGR006EACQRQP8YWY6WPS0TS";
345 let expected_bolt11_invoice_2 = "LNBCRT1M1PNGMY98DQ8V9EKGESNP4QDH5SL00QK4842UZMZVJVX2NLUZT4E6P2ZC2DLAGCU565TP42AUDYPP5XD0PRS5CRDLZVU8DNQQU08W9F4YP0XRXW06ZSHCLCHZU9X28HSSSSP5ES30JG9J4VK2CRW80YXTLRJU2M097TXMFTHR00VC5V0LGKVMURRQ9QYYSGQCQPCXQRRAQRZJQ0Q0K9CDYFSVZAJ5V3PDWYWDMHLEYCVD7TG0SVMY4AM4P6GQZJZ5XQQQQYQQX2QQQUQQQQLGQQQQQQQQFQWDQZX24PSHN68A9D4X4HD89F3XVC7DGGRDTFCA5WH4KZ546GSRTJVACA34QQ3DZ9W4JHLJD3XZRW44RA0RET6RDSRJCEZQC6AXANX6QPHZKHJK";
346 let expected_bolt12_offer_2 = "LNO1QGSQVGNWGCG35Z6EE2H3YCZRADDM72XRFUA9UVE2RLRM9DEU7XYFZRCYZPGTGRDWMGU44QPYUXLHLLMLWN4QSPQ97HSSQZSYV9EKGESSWCPK7JRAAUZ6574TSTVFJFSE20LSFWH8G9GTPFHL4RRJN23VX4TH35SRWKCNQ6S8R9ZW9HU5RXMPXVYCJVK2KY3NTEA8VXZTMWJF4NAJCCAQZQ7YZ7KDDZ600LAW2S2E7Q6XDYLPSMLMV4YAY0QXX5NC8QH05JRNUYQPQCAHK8Y5KQ8H9X624LS6A9GWFTGKYYPUZVUKKM93DWETTL8A7NE84L7SNHCSGR006EACQRQP8YWY6WPS0TS";
347 let parsed_uri_with_offer = uri_with_offer
348 .parse::<bip21::Uri<NetworkUnchecked, Extras>>()
349 .expect("Failed Parsing")
350 .require_network(Network::Regtest)
351 .expect("Invalid Network");
352
353 assert_eq!(Amount::from_sat(100_000), Amount::from(parsed_uri_with_offer.amount.unwrap()));
354
355 assert_eq!(
356 parsed_uri_with_offer.address,
357 bitcoin::Address::from_str("BCRT1QM0NW9S05QDPGC6F52FPKA9U6Q6VWTT5WVS30R2")
358 .unwrap()
359 .require_network(Network::Regtest)
360 .unwrap()
361 );
362
363 if let Some(invoice) = parsed_uri_with_offer.extras.bolt11_invoice {
364 assert_eq!(invoice, Bolt11Invoice::from_str(expected_bolt11_invoice_2).unwrap());
365 } else {
366 panic!("No invoice found.")
367 }
368
369 if let Some(offer) = parsed_uri_with_offer.extras.bolt12_offer {
370 assert_eq!(offer, Offer::from_str(expected_bolt12_offer_2).unwrap());
371 } else {
372 panic!("No offer found.");
373 }
374
375 let zeus_test = "bitcoin:TB1QQ32G6LM2XKT0U2UGASH5DC4CFT3JTPEW65PZZ5?lightning=LNTB500U1PN89HH6PP5MA7K6DRM5SYVD05NTXMGSRNM728J7EHM8KV6VC96YNLKN7G7VDYQDQQCQZRCXQR8Q7SP5HU30L0EEXKYYPQSQYEZELZWUPT62HLJ0KV2662CALGPAML50QPXQ9QXPQYSGQDKTVFXEC8H2DG2GY3C95ETAJ0QKX50XAUCU304PPFV2SQVGFHZ6RMZWJV8MC3M0LXF3GW852C5VSK0DELK0JHLYUTYZDF7QKNAMT4PQQQN24WM&amount=0.0005";
376 let expected_bolt11_invoice_3 = "LNTB500U1PN89HH6PP5MA7K6DRM5SYVD05NTXMGSRNM728J7EHM8KV6VC96YNLKN7G7VDYQDQQCQZRCXQR8Q7SP5HU30L0EEXKYYPQSQYEZELZWUPT62HLJ0KV2662CALGPAML50QPXQ9QXPQYSGQDKTVFXEC8H2DG2GY3C95ETAJ0QKX50XAUCU304PPFV2SQVGFHZ6RMZWJV8MC3M0LXF3GW852C5VSK0DELK0JHLYUTYZDF7QKNAMT4PQQQN24WM";
377 let uri_test2 = zeus_test
378 .parse::<bip21::Uri<NetworkUnchecked, Extras>>()
379 .expect("Failed Parsing")
380 .require_network(Network::Testnet)
381 .expect("Invalid Network");
382
383 assert_eq!(
384 uri_test2.address,
385 bitcoin::Address::from_str("TB1QQ32G6LM2XKT0U2UGASH5DC4CFT3JTPEW65PZZ5")
386 .unwrap()
387 .require_network(Network::Testnet)
388 .unwrap()
389 );
390
391 if let Some(invoice) = uri_test2.extras.bolt11_invoice {
392 assert_eq!(invoice, Bolt11Invoice::from_str(expected_bolt11_invoice_3).unwrap());
393 } else {
394 panic!("No invoice found.");
395 }
396 assert_eq!(Amount::from(uri_test2.amount.unwrap()), Amount::from_sat(50000));
397
398 let muun_test = "bitcoin:bc1q6fmtam67h8wxfwtpumhazhtwyrh3uf039n058zke9xt5hr4ljzwsdcm2pj?amount=0.01&lightning=lnbc10m1pn8g2j4pp575tg4wt8jwgu2lvtk3aj6hy7mc6tnupw07wwkxcvyhtt3wlzw0zsdqqcqzzgxqyz5vqrzjqwnvuc0u4txn35cafc7w94gxvq5p3cu9dd95f7hlrh0fvs46wpvhdv6dzdeg0ww2eyqqqqryqqqqthqqpysp5fkd3k2rzvwdt2av068p58evf6eg50q0eftfhrpugaxkuyje4d25q9qrsgqqkfmnn67s5g6hadrcvf5h0l7p92rtlkwrfqdvc7uuf6lew0czxksvqhyux3zjrl3tlakwhtvezwl24zshnfumukwh0yntqsng9z6glcquvw7kc";
399 let expected_bolt11_invoice_4 = "lnbc10m1pn8g2j4pp575tg4wt8jwgu2lvtk3aj6hy7mc6tnupw07wwkxcvyhtt3wlzw0zsdqqcqzzgxqyz5vqrzjqwnvuc0u4txn35cafc7w94gxvq5p3cu9dd95f7hlrh0fvs46wpvhdv6dzdeg0ww2eyqqqqryqqqqthqqpysp5fkd3k2rzvwdt2av068p58evf6eg50q0eftfhrpugaxkuyje4d25q9qrsgqqkfmnn67s5g6hadrcvf5h0l7p92rtlkwrfqdvc7uuf6lew0czxksvqhyux3zjrl3tlakwhtvezwl24zshnfumukwh0yntqsng9z6glcquvw7kc";
400 let uri_test3 = muun_test
401 .parse::<bip21::Uri<NetworkUnchecked, Extras>>()
402 .expect("Failed Parsing")
403 .require_network(Network::Bitcoin)
404 .expect("Invalid Network");
405 assert_eq!(
406 uri_test3.address,
407 bitcoin::Address::from_str(
408 "bc1q6fmtam67h8wxfwtpumhazhtwyrh3uf039n058zke9xt5hr4ljzwsdcm2pj"
409 )
410 .unwrap()
411 .require_network(Network::Bitcoin)
412 .unwrap()
413 );
414
415 if let Some(invoice) = uri_test3.extras.bolt11_invoice {
416 assert_eq!(invoice, Bolt11Invoice::from_str(expected_bolt11_invoice_4).unwrap());
417 } else {
418 panic!("No invoice found");
419 }
420 assert_eq!(Amount::from(uri_test3.amount.unwrap()), Amount::from_sat(1_000_000));
421
422 let muun_test_no_amount = "bitcoin:bc1qwe94y974pjl9kg5afg8tmsc0nz4hct04u78hdhukxvnnphgu48hs9lx3k5?lightning=lnbc1pn8g249pp5f6ytj32ty90jhvw69enf30hwfgdhyymjewywcmfjevflg6s4z86qdqqcqzzgxqyz5vqrzjqwnvuc0u4txn35cafc7w94gxvq5p3cu9dd95f7hlrh0fvs46wpvhdfjjzh2j9f7ye5qqqqryqqqqthqqpysp5mm832athgcal3m7h35sc29j63lmgzvwc5smfjh2es65elc2ns7dq9qrsgqu2xcje2gsnjp0wn97aknyd3h58an7sjj6nhcrm40846jxphv47958c6th76whmec8ttr2wmg6sxwchvxmsc00kqrzqcga6lvsf9jtqgqy5yexa";
423 let expected_bolt11_invoice_5 = "lnbc1pn8g249pp5f6ytj32ty90jhvw69enf30hwfgdhyymjewywcmfjevflg6s4z86qdqqcqzzgxqyz5vqrzjqwnvuc0u4txn35cafc7w94gxvq5p3cu9dd95f7hlrh0fvs46wpvhdfjjzh2j9f7ye5qqqqryqqqqthqqpysp5mm832athgcal3m7h35sc29j63lmgzvwc5smfjh2es65elc2ns7dq9qrsgqu2xcje2gsnjp0wn97aknyd3h58an7sjj6nhcrm40846jxphv47958c6th76whmec8ttr2wmg6sxwchvxmsc00kqrzqcga6lvsf9jtqgqy5yexa";
424 let uri_test4 = muun_test_no_amount
425 .parse::<bip21::Uri<NetworkUnchecked, Extras>>()
426 .expect("Failed Parsing")
427 .require_network(Network::Bitcoin)
428 .expect("Invalid Network");
429 assert_eq!(
430 uri_test4.address,
431 Address::from_str("bc1qwe94y974pjl9kg5afg8tmsc0nz4hct04u78hdhukxvnnphgu48hs9lx3k5")
432 .unwrap()
433 .require_network(Network::Bitcoin)
434 .unwrap()
435 );
436 if let Some(invoice) = uri_test4.extras.bolt11_invoice {
437 assert_eq!(invoice, Bolt11Invoice::from_str(expected_bolt11_invoice_5).unwrap());
438 } else {
439 panic!("No invoice found");
440 }
441 }
442}