diff --git a/Cargo.toml b/Cargo.toml index d4a87b2a2..206f5f2dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,7 @@ reqwest = { version = "0.11", default-features = false, features = ["json", "rus rusqlite = { version = "0.28.0", features = ["bundled"] } bitcoin = "0.30.2" bip39 = "2.0.0" +bip21 = { version = "0.3.1", features = ["std"], default-features = false } rand = "0.8.5" chrono = { version = "0.4", default-features = false, features = ["clock"] } diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 2723db573..aedf9f6ab 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -63,6 +63,7 @@ interface Node { Bolt12Payment bolt12_payment(); SpontaneousPayment spontaneous_payment(); OnchainPayment onchain_payment(); + UnifiedQrPayment unified_qr_payment(); [Throws=NodeError] void connect(PublicKey node_id, SocketAddress address, boolean persist); [Throws=NodeError] @@ -148,6 +149,13 @@ interface OnchainPayment { Txid send_all_to_address([ByRef]Address address); }; +interface UnifiedQrPayment { + [Throws=NodeError] + string receive(u64 amount_sats, [ByRef]string message, u32 expiry_sec); + [Throws=NodeError] + QrPaymentResult send([ByRef]string uri_str); +}; + [Error] enum NodeError { "AlreadyRunning", @@ -175,6 +183,7 @@ enum NodeError { "GossipUpdateFailed", "GossipUpdateTimeout", "LiquidityRequestFailed", + "UriParameterParsingFailed", "InvalidAddress", "InvalidSocketAddress", "InvalidPublicKey", @@ -191,6 +200,7 @@ enum NodeError { "InvalidRefund", "InvalidChannelId", "InvalidNetwork", + "InvalidUri", "DuplicatePayment", "UnsupportedCurrency", "InsufficientFunds", @@ -276,6 +286,13 @@ interface PaymentKind { Spontaneous(PaymentHash hash, PaymentPreimage? preimage); }; +[Enum] +interface QrPaymentResult { + Onchain(Txid txid); + Bolt11(PaymentId payment_id); + Bolt12(PaymentId payment_id); +}; + enum PaymentDirection { "Inbound", "Outbound", diff --git a/src/error.rs b/src/error.rs index a8671d9a7..7506b013b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -53,6 +53,8 @@ pub enum Error { GossipUpdateTimeout, /// A liquidity request operation failed. LiquidityRequestFailed, + /// Parsing a URI parameter has failed. + UriParameterParsingFailed, /// The given address is invalid. InvalidAddress, /// The given network address is invalid. @@ -85,6 +87,8 @@ pub enum Error { InvalidChannelId, /// The given network is invalid. InvalidNetwork, + /// The given URI is invalid. + InvalidUri, /// A payment with the given hash has already been initiated. DuplicatePayment, /// The provided offer was denonminated in an unsupported currency. @@ -131,6 +135,7 @@ impl fmt::Display for Error { Self::GossipUpdateFailed => write!(f, "Failed to update gossip data."), Self::GossipUpdateTimeout => write!(f, "Updating gossip data timed out."), Self::LiquidityRequestFailed => write!(f, "Failed to request inbound liquidity."), + Self::UriParameterParsingFailed => write!(f, "Failed to parse a URI parameter."), Self::InvalidAddress => write!(f, "The given address is invalid."), Self::InvalidSocketAddress => write!(f, "The given network address is invalid."), Self::InvalidPublicKey => write!(f, "The given public key is invalid."), @@ -147,6 +152,7 @@ impl fmt::Display for Error { Self::InvalidRefund => write!(f, "The given refund is invalid."), Self::InvalidChannelId => write!(f, "The given channel ID is invalid."), Self::InvalidNetwork => write!(f, "The given network is invalid."), + Self::InvalidUri => write!(f, "The given URI is invalid."), Self::DuplicatePayment => { write!(f, "A payment with the given hash has already been initiated.") }, diff --git a/src/lib.rs b/src/lib.rs index 1c137d355..206fe52d8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -133,7 +133,10 @@ use gossip::GossipSource; use graph::NetworkGraph; use liquidity::LiquiditySource; use payment::store::PaymentStore; -use payment::{Bolt11Payment, Bolt12Payment, OnchainPayment, PaymentDetails, SpontaneousPayment}; +use payment::{ + Bolt11Payment, Bolt12Payment, OnchainPayment, PaymentDetails, SpontaneousPayment, + UnifiedQrPayment, +}; use peer_store::{PeerInfo, PeerStore}; use types::{ Broadcaster, BumpTransactionEventHandler, ChainMonitor, ChannelManager, DynStore, FeeEstimator, @@ -1072,6 +1075,40 @@ impl Node { )) } + /// Returns a payment handler allowing to create [BIP 21] URIs with an on-chain, [BOLT 11], + /// and [BOLT 12] payment options. + /// + /// [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 + /// [BIP 21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki + #[cfg(not(feature = "uniffi"))] + pub fn unified_qr_payment(&self) -> UnifiedQrPayment { + UnifiedQrPayment::new( + self.onchain_payment().into(), + self.bolt11_payment().into(), + self.bolt12_payment().into(), + Arc::clone(&self.config), + Arc::clone(&self.logger), + ) + } + + /// Returns a payment handler allowing to create [BIP 21] URIs with an on-chain, [BOLT 11], + /// and [BOLT 12] payment options. + /// + /// [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 + /// [BIP 21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki + #[cfg(feature = "uniffi")] + pub fn unified_qr_payment(&self) -> Arc { + Arc::new(UnifiedQrPayment::new( + self.onchain_payment(), + self.bolt11_payment(), + self.bolt12_payment(), + Arc::clone(&self.config), + Arc::clone(&self.logger), + )) + } + /// Retrieve a list of known channels. pub fn list_channels(&self) -> Vec { self.channel_manager.list_channels().into_iter().map(|c| c.into()).collect() diff --git a/src/payment/mod.rs b/src/payment/mod.rs index 1862bf2df..ac4fc5663 100644 --- a/src/payment/mod.rs +++ b/src/payment/mod.rs @@ -5,9 +5,11 @@ mod bolt12; mod onchain; mod spontaneous; pub(crate) mod store; +mod unified_qr; pub use bolt11::Bolt11Payment; pub use bolt12::Bolt12Payment; pub use onchain::OnchainPayment; pub use spontaneous::SpontaneousPayment; pub use store::{LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus}; +pub use unified_qr::{QrPaymentResult, UnifiedQrPayment}; diff --git a/src/payment/unified_qr.rs b/src/payment/unified_qr.rs new file mode 100644 index 000000000..a4551eb8a --- /dev/null +++ b/src/payment/unified_qr.rs @@ -0,0 +1,421 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Holds a payment handler allowing to create [BIP 21] URIs with an on-chain, [BOLT 11], and [BOLT 12] payment +//! options. +//! +//! [BIP 21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki +//! [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 +use crate::error::Error; +use crate::logger::{log_error, FilesystemLogger, Logger}; +use crate::payment::{Bolt11Payment, Bolt12Payment, OnchainPayment}; +use crate::Config; + +use lightning::ln::channelmanager::PaymentId; +use lightning::offers::offer::Offer; +use lightning_invoice::Bolt11Invoice; + +use bip21::de::ParamKind; +use bip21::{DeserializationError, DeserializeParams, Param, SerializeParams}; +use bitcoin::address::{NetworkChecked, NetworkUnchecked}; +use bitcoin::{Amount, Txid}; + +use std::sync::Arc; +use std::vec::IntoIter; + +type Uri<'a> = bip21::Uri<'a, NetworkChecked, Extras>; + +#[derive(Debug, Clone)] +struct Extras { + bolt11_invoice: Option, + bolt12_offer: Option, +} + +/// A payment handler allowing to create [BIP 21] URIs with an on-chain, [BOLT 11], and [BOLT 12] payment +/// option. +/// +/// Should be retrieved by calling [`Node::unified_qr_payment`] +/// +/// [BIP 21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki +/// [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 +/// [`Node::unified_qr_payment`]: crate::Node::unified_qr_payment +pub struct UnifiedQrPayment { + onchain_payment: Arc, + bolt11_invoice: Arc, + bolt12_payment: Arc, + config: Arc, + logger: Arc, +} + +impl UnifiedQrPayment { + pub(crate) fn new( + onchain_payment: Arc, bolt11_invoice: Arc, + bolt12_payment: Arc, config: Arc, logger: Arc, + ) -> Self { + Self { onchain_payment, bolt11_invoice, bolt12_payment, config, logger } + } + + /// 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 + /// can always pay using the provided on-chain address, while newer wallets will + /// typically opt to use the provided BOLT11 invoice or BOLT12 offer. + /// + /// # Parameters + /// - `amount_sats`: The amount to be received, specified in satoshis. + /// - `description`: A description or note associated with the payment. + /// This message is visible to the payer and can provide context or details about the payment. + /// - `expiry_sec`: The expiration time for the payment, specified in seconds. + /// + /// Returns a payable URI that can be used to request and receive a payment of the amount + /// given. In case of an error, the function returns `Error::WalletOperationFailed`for on-chain + /// address issues, `Error::InvoiceCreationFailed` for BOLT11 invoice issues, or + /// `Error::OfferCreationFailed` for BOLT12 offer issues. + /// + /// 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, description: &str, expiry_sec: u32, + ) -> Result { + 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, description) { + Ok(offer) => Some(offer), + Err(e) => { + log_error!(self.logger, "Failed to create offer: {}", e); + return Err(Error::OfferCreationFailed); + }, + }; + + let bolt11_invoice = + match self.bolt11_invoice.receive(amount_msats, description, expiry_sec) { + Ok(invoice) => Some(invoice), + Err(e) => { + log_error!(self.logger, "Failed to create invoice {}", e); + return Err(Error::InvoiceCreationFailed); + }, + }; + + let extras = Extras { bolt11_invoice, bolt12_offer }; + + let mut uri = Uri::with_extras(onchain_address, extras); + uri.amount = Some(Amount::from_sat(amount_sats)); + uri.message = Some(description.into()); + + Ok(format_uri(uri)) + } + + /// Sends a payment given a [BIP 21] 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 paid. + /// + /// Returns a `QrPaymentResult` indicating the outcome of the payment. If an error + /// occurs, an `Error` is returned detailing the issue encountered. + /// + /// [BIP 21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki + pub fn send(&self, uri_str: &str) -> Result { + let uri: bip21::Uri = + uri_str.parse().map_err(|_| Error::InvalidUri)?; + + let uri_network_checked = + uri.clone().require_network(self.config.network).map_err(|_| Error::InvalidNetwork)?; + + if let Some(offer) = uri_network_checked.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 send BOLT12 offer: {:?}. This is part of a unified QR code payment. Falling back to the BOLT11 invoice.", e), + } + } + + if let Some(invoice) = uri_network_checked.extras.bolt11_invoice { + match self.bolt11_invoice.send(&invoice) { + Ok(payment_id) => return Ok(QrPaymentResult::Bolt11 { payment_id }), + 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), + } + } + + let amount = match uri_network_checked.amount { + Some(amount) => amount, + None => { + log_error!(self.logger, "No amount specified in the URI. Aborting the payment."); + return Err(Error::InvalidAmount); + }, + }; + + let txid = + self.onchain_payment.send_to_address(&uri_network_checked.address, amount.to_sat())?; + + Ok(QrPaymentResult::Onchain { txid }) + } +} + +/// 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 { + /// An on-chain payment. + Onchain { + /// The transaction ID (txid) of the on-chain payment. + txid: Txid, + }, + /// 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, + }, + /// 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 format_uri(uri: bip21::Uri) -> String { + let mut uri = format!("{:#}", uri); + + 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 +} + +impl<'a> SerializeParams for &'a Extras { + type Key = &'static str; + type Value = String; + type Iterator = IntoIter<(Self::Key, Self::Value)>; + + fn serialize_params(self) -> Self::Iterator { + let mut params = Vec::new(); + + 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(("lno", bolt12_offer.to_string())); + } + + params.into_iter() + } +} + +impl<'a> DeserializeParams<'a> for Extras { + type DeserializationState = DeserializationState; +} + +#[derive(Default)] +struct DeserializationState { + bolt11_invoice: Option, + bolt12_offer: Option, +} + +impl<'a> bip21::de::DeserializationState<'a> for DeserializationState { + type Value = Extras; + + fn is_param_known(&self, key: &str) -> bool { + key == "lightning" || key == "lno" + } + + fn deserialize_temp( + &mut self, key: &str, value: Param<'_>, + ) -> Result::Error> { + match key { + "lightning" => { + let bolt11_value = + String::try_from(value).map_err(|_| Error::UriParameterParsingFailed)?; + if let Ok(invoice) = bolt11_value.parse::() { + self.bolt11_invoice = Some(invoice); + Ok(bip21::de::ParamKind::Known) + } else { + Ok(bip21::de::ParamKind::Unknown) + } + }, + "lno" => { + let bolt12_value = + String::try_from(value).map_err(|_| Error::UriParameterParsingFailed)?; + if let Ok(offer) = bolt12_value.parse::() { + self.bolt12_offer = Some(offer); + Ok(bip21::de::ParamKind::Known) + } else { + Ok(bip21::de::ParamKind::Unknown) + } + }, + _ => Ok(bip21::de::ParamKind::Unknown), + } + } + + fn finalize(self) -> Result::Error> { + Ok(Extras { bolt11_invoice: self.bolt11_invoice, bolt12_offer: self.bolt12_offer }) + } +} + +impl DeserializationError for Extras { + type Error = Error; +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::payment::unified_qr::Extras; + use bitcoin::{Address, Network}; + use std::str::FromStr; + + #[test] + fn parse_uri() { + let uri_test1 = "BITCOIN:TB1QRSCD05XNY6QZ63TF9GJELGVK6D3UDJFEKK62VU?amount=1&message=Test%20message&lightning=LNTB1000M1PNXWM7MDQ523JHXAPQD4JHXUMPVAJSNP4QWP9QD2JFP8DUZ46JQG5LTKVDH04YG52G6UF2YAXP8H7YZPZM3DM5PP5KUP7YT429UP9Z4ACPA60R7WETSTL66549MG05P0JN0C4L2NCC40SSP5R0LH86DJCL0NK8HZHNZHX92VVUAAVNE48Z5RVKVY5DKTRQ0DMP7S9QYYSGQCQPCXQRRAQYR59FGN2VVC5R6DS0AZMETH493ZU56H0WSVMGYCW9LEPZ032PGQNZMQ6XKVEH90Z02C0NH3J5QGDAWCS2YC2ZNP22J0ZD0PPF78N4QQQEXTYS2"; + let expected_bolt11_invoice_1 = "LNTB1000M1PNXWM7MDQ523JHXAPQD4JHXUMPVAJSNP4QWP9QD2JFP8DUZ46JQG5LTKVDH04YG52G6UF2YAXP8H7YZPZM3DM5PP5KUP7YT429UP9Z4ACPA60R7WETSTL66549MG05P0JN0C4L2NCC40SSP5R0LH86DJCL0NK8HZHNZHX92VVUAAVNE48Z5RVKVY5DKTRQ0DMP7S9QYYSGQCQPCXQRRAQYR59FGN2VVC5R6DS0AZMETH493ZU56H0WSVMGYCW9LEPZ032PGQNZMQ6XKVEH90Z02C0NH3J5QGDAWCS2YC2ZNP22J0ZD0PPF78N4QQQEXTYS2"; + let parsed_uri = uri_test1 + .parse::>() + .expect("Failed Parsing") + .require_network(Network::Testnet) + .expect("Invalid Network"); + + assert_eq!( + parsed_uri.address, + bitcoin::Address::from_str("TB1QRSCD05XNY6QZ63TF9GJELGVK6D3UDJFEKK62VU") + .unwrap() + .require_network(Network::Testnet) + .unwrap() + ); + + assert_eq!(Amount::from_sat(100_000_000), Amount::from(parsed_uri.amount.unwrap())); + + if let Some(invoice) = parsed_uri.extras.bolt11_invoice { + assert_eq!(invoice, Bolt11Invoice::from_str(expected_bolt11_invoice_1).unwrap()); + } else { + panic!("No Lightning invoice found"); + } + + let uri_with_offer = "BITCOIN:BCRT1QM0NW9S05QDPGC6F52FPKA9U6Q6VWTT5WVS30R2?amount=0.001&message=asdf&lightning=LNBCRT1M1PNGMY98DQ8V9EKGESNP4QDH5SL00QK4842UZMZVJVX2NLUZT4E6P2ZC2DLAGCU565TP42AUDYPP5XD0PRS5CRDLZVU8DNQQU08W9F4YP0XRXW06ZSHCLCHZU9X28HSSSSP5ES30JG9J4VK2CRW80YXTLRJU2M097TXMFTHR00VC5V0LGKVMURRQ9QYYSGQCQPCXQRRAQRZJQ0Q0K9CDYFSVZAJ5V3PDWYWDMHLEYCVD7TG0SVMY4AM4P6GQZJZ5XQQQQYQQX2QQQUQQQQLGQQQQQQQQFQWDQZX24PSHN68A9D4X4HD89F3XVC7DGGRDTFCA5WH4KZ546GSRTJVACA34QQ3DZ9W4JHLJD3XZRW44RA0RET6RDSRJCEZQC6AXANX6QPHZKHJK&lno=LNO1QGSQVGNWGCG35Z6EE2H3YCZRADDM72XRFUA9UVE2RLRM9DEU7XYFZRCYZPGTGRDWMGU44QPYUXLHLLMLWN4QSPQ97HSSQZSYV9EKGESSWCPK7JRAAUZ6574TSTVFJFSE20LSFWH8G9GTPFHL4RRJN23VX4TH35SRWKCNQ6S8R9ZW9HU5RXMPXVYCJVK2KY3NTEA8VXZTMWJF4NAJCCAQZQ7YZ7KDDZ600LAW2S2E7Q6XDYLPSMLMV4YAY0QXX5NC8QH05JRNUYQPQCAHK8Y5KQ8H9X624LS6A9GWFTGKYYPUZVUKKM93DWETTL8A7NE84L7SNHCSGR006EACQRQP8YWY6WPS0TS"; + let expected_bolt11_invoice_2 = "LNBCRT1M1PNGMY98DQ8V9EKGESNP4QDH5SL00QK4842UZMZVJVX2NLUZT4E6P2ZC2DLAGCU565TP42AUDYPP5XD0PRS5CRDLZVU8DNQQU08W9F4YP0XRXW06ZSHCLCHZU9X28HSSSSP5ES30JG9J4VK2CRW80YXTLRJU2M097TXMFTHR00VC5V0LGKVMURRQ9QYYSGQCQPCXQRRAQRZJQ0Q0K9CDYFSVZAJ5V3PDWYWDMHLEYCVD7TG0SVMY4AM4P6GQZJZ5XQQQQYQQX2QQQUQQQQLGQQQQQQQQFQWDQZX24PSHN68A9D4X4HD89F3XVC7DGGRDTFCA5WH4KZ546GSRTJVACA34QQ3DZ9W4JHLJD3XZRW44RA0RET6RDSRJCEZQC6AXANX6QPHZKHJK"; + let expected_bolt12_offer_2 = "LNO1QGSQVGNWGCG35Z6EE2H3YCZRADDM72XRFUA9UVE2RLRM9DEU7XYFZRCYZPGTGRDWMGU44QPYUXLHLLMLWN4QSPQ97HSSQZSYV9EKGESSWCPK7JRAAUZ6574TSTVFJFSE20LSFWH8G9GTPFHL4RRJN23VX4TH35SRWKCNQ6S8R9ZW9HU5RXMPXVYCJVK2KY3NTEA8VXZTMWJF4NAJCCAQZQ7YZ7KDDZ600LAW2S2E7Q6XDYLPSMLMV4YAY0QXX5NC8QH05JRNUYQPQCAHK8Y5KQ8H9X624LS6A9GWFTGKYYPUZVUKKM93DWETTL8A7NE84L7SNHCSGR006EACQRQP8YWY6WPS0TS"; + let parsed_uri_with_offer = uri_with_offer + .parse::>() + .expect("Failed Parsing") + .require_network(Network::Regtest) + .expect("Invalid Network"); + + assert_eq!(Amount::from_sat(100_000), Amount::from(parsed_uri_with_offer.amount.unwrap())); + + 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(expected_bolt11_invoice_2).unwrap()); + } else { + panic!("No invoice found.") + } + + if let Some(offer) = parsed_uri_with_offer.extras.bolt12_offer { + assert_eq!(offer, Offer::from_str(expected_bolt12_offer_2).unwrap()); + } else { + panic!("No offer found."); + } + + let zeus_test = "bitcoin:TB1QQ32G6LM2XKT0U2UGASH5DC4CFT3JTPEW65PZZ5?lightning=LNTB500U1PN89HH6PP5MA7K6DRM5SYVD05NTXMGSRNM728J7EHM8KV6VC96YNLKN7G7VDYQDQQCQZRCXQR8Q7SP5HU30L0EEXKYYPQSQYEZELZWUPT62HLJ0KV2662CALGPAML50QPXQ9QXPQYSGQDKTVFXEC8H2DG2GY3C95ETAJ0QKX50XAUCU304PPFV2SQVGFHZ6RMZWJV8MC3M0LXF3GW852C5VSK0DELK0JHLYUTYZDF7QKNAMT4PQQQN24WM&amount=0.0005"; + let expected_bolt11_invoice_3 = "LNTB500U1PN89HH6PP5MA7K6DRM5SYVD05NTXMGSRNM728J7EHM8KV6VC96YNLKN7G7VDYQDQQCQZRCXQR8Q7SP5HU30L0EEXKYYPQSQYEZELZWUPT62HLJ0KV2662CALGPAML50QPXQ9QXPQYSGQDKTVFXEC8H2DG2GY3C95ETAJ0QKX50XAUCU304PPFV2SQVGFHZ6RMZWJV8MC3M0LXF3GW852C5VSK0DELK0JHLYUTYZDF7QKNAMT4PQQQN24WM"; + let uri_test2 = zeus_test + .parse::>() + .expect("Failed Parsing") + .require_network(Network::Testnet) + .expect("Invalid Network"); + + assert_eq!( + uri_test2.address, + bitcoin::Address::from_str("TB1QQ32G6LM2XKT0U2UGASH5DC4CFT3JTPEW65PZZ5") + .unwrap() + .require_network(Network::Testnet) + .unwrap() + ); + + if let Some(invoice) = uri_test2.extras.bolt11_invoice { + assert_eq!(invoice, Bolt11Invoice::from_str(expected_bolt11_invoice_3).unwrap()); + } else { + panic!("No invoice found."); + } + assert_eq!(Amount::from(uri_test2.amount.unwrap()), Amount::from_sat(50000)); + + let muun_test = "bitcoin:bc1q6fmtam67h8wxfwtpumhazhtwyrh3uf039n058zke9xt5hr4ljzwsdcm2pj?amount=0.01&lightning=lnbc10m1pn8g2j4pp575tg4wt8jwgu2lvtk3aj6hy7mc6tnupw07wwkxcvyhtt3wlzw0zsdqqcqzzgxqyz5vqrzjqwnvuc0u4txn35cafc7w94gxvq5p3cu9dd95f7hlrh0fvs46wpvhdv6dzdeg0ww2eyqqqqryqqqqthqqpysp5fkd3k2rzvwdt2av068p58evf6eg50q0eftfhrpugaxkuyje4d25q9qrsgqqkfmnn67s5g6hadrcvf5h0l7p92rtlkwrfqdvc7uuf6lew0czxksvqhyux3zjrl3tlakwhtvezwl24zshnfumukwh0yntqsng9z6glcquvw7kc"; + let expected_bolt11_invoice_4 = "lnbc10m1pn8g2j4pp575tg4wt8jwgu2lvtk3aj6hy7mc6tnupw07wwkxcvyhtt3wlzw0zsdqqcqzzgxqyz5vqrzjqwnvuc0u4txn35cafc7w94gxvq5p3cu9dd95f7hlrh0fvs46wpvhdv6dzdeg0ww2eyqqqqryqqqqthqqpysp5fkd3k2rzvwdt2av068p58evf6eg50q0eftfhrpugaxkuyje4d25q9qrsgqqkfmnn67s5g6hadrcvf5h0l7p92rtlkwrfqdvc7uuf6lew0czxksvqhyux3zjrl3tlakwhtvezwl24zshnfumukwh0yntqsng9z6glcquvw7kc"; + let uri_test3 = muun_test + .parse::>() + .expect("Failed Parsing") + .require_network(Network::Bitcoin) + .expect("Invalid Network"); + assert_eq!( + uri_test3.address, + bitcoin::Address::from_str( + "bc1q6fmtam67h8wxfwtpumhazhtwyrh3uf039n058zke9xt5hr4ljzwsdcm2pj" + ) + .unwrap() + .require_network(Network::Bitcoin) + .unwrap() + ); + + if let Some(invoice) = uri_test3.extras.bolt11_invoice { + assert_eq!(invoice, Bolt11Invoice::from_str(expected_bolt11_invoice_4).unwrap()); + } else { + panic!("No invoice found"); + } + assert_eq!(Amount::from(uri_test3.amount.unwrap()), Amount::from_sat(1_000_000)); + + let muun_test_no_amount = "bitcoin:bc1qwe94y974pjl9kg5afg8tmsc0nz4hct04u78hdhukxvnnphgu48hs9lx3k5?lightning=lnbc1pn8g249pp5f6ytj32ty90jhvw69enf30hwfgdhyymjewywcmfjevflg6s4z86qdqqcqzzgxqyz5vqrzjqwnvuc0u4txn35cafc7w94gxvq5p3cu9dd95f7hlrh0fvs46wpvhdfjjzh2j9f7ye5qqqqryqqqqthqqpysp5mm832athgcal3m7h35sc29j63lmgzvwc5smfjh2es65elc2ns7dq9qrsgqu2xcje2gsnjp0wn97aknyd3h58an7sjj6nhcrm40846jxphv47958c6th76whmec8ttr2wmg6sxwchvxmsc00kqrzqcga6lvsf9jtqgqy5yexa"; + let expected_bolt11_invoice_5 = "lnbc1pn8g249pp5f6ytj32ty90jhvw69enf30hwfgdhyymjewywcmfjevflg6s4z86qdqqcqzzgxqyz5vqrzjqwnvuc0u4txn35cafc7w94gxvq5p3cu9dd95f7hlrh0fvs46wpvhdfjjzh2j9f7ye5qqqqryqqqqthqqpysp5mm832athgcal3m7h35sc29j63lmgzvwc5smfjh2es65elc2ns7dq9qrsgqu2xcje2gsnjp0wn97aknyd3h58an7sjj6nhcrm40846jxphv47958c6th76whmec8ttr2wmg6sxwchvxmsc00kqrzqcga6lvsf9jtqgqy5yexa"; + let uri_test4 = muun_test_no_amount + .parse::>() + .expect("Failed Parsing") + .require_network(Network::Bitcoin) + .expect("Invalid Network"); + assert_eq!( + uri_test4.address, + Address::from_str("bc1qwe94y974pjl9kg5afg8tmsc0nz4hct04u78hdhukxvnnphgu48hs9lx3k5") + .unwrap() + .require_network(Network::Bitcoin) + .unwrap() + ); + if let Some(invoice) = uri_test4.extras.bolt11_invoice { + assert_eq!(invoice, Bolt11Invoice::from_str(expected_bolt11_invoice_5).unwrap()); + } else { + panic!("No invoice found"); + } + } +} diff --git a/src/uniffi_types.rs b/src/uniffi_types.rs index 9dd7e5699..7c2142091 100644 --- a/src/uniffi_types.rs +++ b/src/uniffi_types.rs @@ -1,5 +1,11 @@ +// Importing these items ensures they are accessible in the uniffi bindings +// without introducing unused import warnings in lib.rs. +// +// Make sure to add any re-exported items that need to be used in uniffi below. + pub use crate::graph::{ChannelInfo, ChannelUpdateInfo, NodeAnnouncementInfo, NodeInfo}; pub use crate::payment::store::{LSPFeeLimits, PaymentDirection, PaymentKind, PaymentStatus}; +pub use crate::payment::QrPaymentResult; pub use lightning::events::{ClosureReason, PaymentFailureReason}; pub use lightning::ln::{ChannelId, PaymentHash, PaymentPreimage, PaymentSecret}; diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 37ddeb9a7..5a918762a 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -1,13 +1,13 @@ mod common; use common::{ - do_channel_full_cycle, expect_event, expect_payment_received_event, + do_channel_full_cycle, expect_channel_ready_event, expect_event, expect_payment_received_event, expect_payment_successful_event, generate_blocks_and_wait, open_channel, premine_and_distribute_funds, random_config, setup_bitcoind_and_electrsd, setup_builder, setup_node, setup_two_nodes, wait_for_tx, TestSyncStore, }; -use ldk_node::payment::PaymentKind; +use ldk_node::payment::{PaymentKind, QrPaymentResult}; use ldk_node::{Builder, Event, NodeError}; use lightning::ln::channelmanager::PaymentId; @@ -17,8 +17,6 @@ use bitcoin::{Amount, Network}; use std::sync::Arc; -use crate::common::expect_channel_ready_event; - #[test] fn channel_full_cycle() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); @@ -552,3 +550,155 @@ 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:")); + assert!(uri.contains("lightning=")); + assert!(uri.contains("lno=")); + }, + Err(e) => panic!("Failed to generate URI: {:?}", e), + } +} + +#[test] +fn unified_qr_send_receive() { + 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()); + + // Sleep until we broadcast a node announcement. + while node_b.status().latest_node_announcement_broadcast_timestamp.is_none() { + std::thread::sleep(std::time::Duration::from_millis(10)); + } + + // Sleep one more sec to make sure the node announcement propagates. + std::thread::sleep(std::time::Duration::from_secs(1)); + + 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 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 + }, + Ok(QrPaymentResult::Bolt11 { payment_id: _ }) => { + panic!("Expected Bolt12 payment but got Bolt11"); + }, + Ok(QrPaymentResult::Onchain { txid: _ }) => { + panic!("Expected Bolt12 payment but get On-chain transaction"); + }, + Err(e) => { + panic!("Expected Bolt12 payment but got error: {:?}", e); + }, + }; + + expect_payment_successful_event!(node_a, Some(offer_payment_id), None); + + // 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_str_with_invalid_offer) { + Ok(QrPaymentResult::Bolt12 { payment_id: _ }) => { + panic!("Expected Bolt11 payment but got Bolt12"); + }, + Ok(QrPaymentResult::Bolt11 { payment_id }) => { + println!("\nBolt11 payment sent successfully with PaymentID: {:?}", payment_id); + payment_id + }, + Ok(QrPaymentResult::Onchain { txid: _ }) => { + panic!("Expected Bolt11 payment but got on-chain transaction"); + }, + Err(e) => { + panic!("Expected Bolt11 payment but got error: {:?}", e); + }, + }; + expect_payment_successful_event!(node_a, Some(invoice_payment_id), None); + + let expect_onchain_amount_sats = 800_000; + let onchain_uqr_payment = + node_b.unified_qr_payment().receive(expect_onchain_amount_sats, "asdf", 4_000).unwrap(); + + // 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") + }, + Ok(QrPaymentResult::Bolt11 { payment_id: _ }) => { + panic!("Expected on-chain payment but got Bolt11"); + }, + Ok(QrPaymentResult::Onchain { txid }) => { + println!("\nOn-chain transaction successful with Txid: {}", txid); + txid + }, + Err(e) => { + panic!("Expected on-chain payment but got error: {:?}", e); + }, + }; + + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); + wait_for_tx(&electrsd.client, txid); + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + assert_eq!(node_b.list_balances().total_onchain_balance_sats, 800_000); + assert_eq!(node_b.list_balances().total_lightning_balance_sats, 200_000); +}