From 77a0bbede4f063ee50ca2aa36fb019e79d3f8707 Mon Sep 17 00:00:00 2001 From: Ian Slane <105389948+slanesuke@users.noreply.github.com> Date: Thu, 18 Jul 2024 08:42:12 -0600 Subject: [PATCH] Add BIP21 Unified QR Code Support (#302) * Add `UnifiedQrPayment` module for BIP21 URIs Firstly, I thought I staged and made commits for `unified_qr.rs` so sorry if this is out of order! But in `unified_qr.rs` I - I introduced the `UnifiedQrPayment` struct to handle creating and paying BIP21 URIs - `receive` generates a URI with an on-chain address and BOLT11 invoice and returns the URI as a string - `send` will parse a given URI string and attempt to send the BOLT12 offer, BOLT11 invoice, then if those fail the fallback on-chain address will be paid to. - Then I included tests for URI generation and URI parsing - Also has logging and error handling for payment operations * Add `unified_qr_payment` payment handler to `Node` - implement unified_qr_payment method to create the Unified QR payment handler - Includes conditional UniFFI features and updates docs with BIP21 and BOLT11 links * Add support for `unified_qr` in payment mod - Included unified_qr in payment module - Added `PaymentResult` and `UnifiedQrPayment` from unified_qr for public use * Add bip21 crate to handle BIP21 URIs * Add `UnifiedQrPayment` and `PaymentResult` to `ldk_node.udl` - Introduced `UnifiedQrPayment` method to `Node` interface - Add `UnifiedQrPayment` interface with `receieve and `send` methods - Add `PaymentResult` interface (enum) with `Onchain`, `Bolt11` and `Bolt12` fields These changes add support for our UniFFI bindings and enable the use of `unified_qr_payment` payment handler in Swift, and Kotlin. * Update `Error` enum with URI related errors - Add `UriParameterFailed` and `InvalidUri` fields to the `Error` enum - Added related error messages in the Display impl for the new fields * Add `PaymentResult` import for UniFFI bindings - Added `PaymentResult` so the .udl could access the enum - Added comment to explain the need to import any re-exported items to enure they're accessible in UniFFI. (becasue rustc says to add them in `lib.rs` * Add Unified QR `send`/`receive` integration tests - Added `unified_qr_send_receive` test to verify the `UnifedQrPayment` functionality - Added logic to handle paying a `BOLT12` offer, `BOLT11` invoice, and if those fail `On-chain` tx from a URI. - Validated each payments successful event - Ensured the off-chain and on-chain balacnes reflected the payment attempts * Update PR with optimizations and nit fixups The changes include: - Fixed a handful of nits for better readability in docs and simple grammar errors and made various name changes that affected the committed files. - Added a helper function in unified_qr.rs called capitalize_qr_params to format the lightning param in the receive method - Removed the optional message in the receive method and made it a required &str - Adjusted UDL formatting to use tabs instead of spaces These changes were made to improve code quality and maintainability based on the review feedback * 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 * Update BOLT12 offer to use `lno` key 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 * Refactor for clarity and improve error handling This commit fixes a handful of minor comments/nits that include: - Updates to set the `bip21` crates default-features to false, to minimize dependencies. - Enable the `std` feature since we use/benefit from it. - In `receive` return `InvoiceCreationFailed` or `OfferCreationFailed` when creating an invoice or offer. Rather than silently logging the error. - Also in `receive` we first check if an amount is specified, and if not, return an error and abort. - Pass in `Config` to `UnifiedQrPayment` struct to use the users config network. - In `send` instead of checking each network for the `NetworkChecked` URI, we pass in the `Config::Network`. - Simplifed param parsing in `deserialize_temp` by directly finding the key and parsing the corresponding value. - General documentation fixes. - In parsing tests, moved longer invoice/offer strings into. variables that start with expected_ for clarity. * Fix docs for clarity Cleaned up the docs so they are easier to understand for the user. Also changed the message param in receive to description. --- Cargo.toml | 1 + bindings/ldk_node.udl | 17 ++ src/error.rs | 6 + src/lib.rs | 39 ++- src/payment/mod.rs | 2 + src/payment/unified_qr.rs | 421 ++++++++++++++++++++++++++++++++ src/uniffi_types.rs | 6 + tests/integration_tests_rust.rs | 158 +++++++++++- 8 files changed, 645 insertions(+), 5 deletions(-) create mode 100644 src/payment/unified_qr.rs 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); +}