From 683bfb3b17b469f157f64f56c0ee93a43d98d1b8 Mon Sep 17 00:00:00 2001 From: Ian Slane Date: Mon, 15 Jul 2024 15:40:28 -0600 Subject: [PATCH] Add `payer_note` in `PaymentKind::Bolt12` Add support for including `payer_note` in `Bolt12Offer` and `PaymentKind::Bolt12` and updated the relevant code to handle where the new `payer_note` field was required. --- bindings/ldk_node.udl | 13 ++-- src/error.rs | 3 + src/event.rs | 4 ++ src/payment/bolt12.rs | 101 +++++++++++++++++++++++--------- src/payment/store.rs | 25 ++++++++ src/payment/unified_qr.rs | 4 +- tests/integration_tests_rust.rs | 71 ++++++++++++++++++---- 7 files changed, 171 insertions(+), 50 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 514876426..ec183e78f 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -120,17 +120,17 @@ interface Bolt11Payment { interface Bolt12Payment { [Throws=NodeError] - PaymentId send([ByRef]Offer offer, string? payer_note); + PaymentId send([ByRef]Offer offer, u64? quantity, string? payer_note); [Throws=NodeError] - PaymentId send_using_amount([ByRef]Offer offer, string? payer_note, u64 amount_msat); + PaymentId send_using_amount([ByRef]Offer offer, u64 amount_msat, u64? quantity, string? payer_note); [Throws=NodeError] - Offer receive(u64 amount_msat, [ByRef]string description); + Offer receive(u64 amount_msat, [ByRef]string description, u64? quantity); [Throws=NodeError] Offer receive_variable_amount([ByRef]string description); [Throws=NodeError] Bolt12Invoice request_refund_payment([ByRef]Refund refund); [Throws=NodeError] - Refund initiate_refund(u64 amount_msat, u32 expiry_secs); + Refund initiate_refund(u64 amount_msat, u32 expiry_secs, u64? quantity, string? payer_note); }; interface SpontaneousPayment { @@ -201,6 +201,7 @@ enum NodeError { "InvalidChannelId", "InvalidNetwork", "InvalidUri", + "InvalidQuantity", "DuplicatePayment", "UnsupportedCurrency", "InsufficientFunds", @@ -281,8 +282,8 @@ interface PaymentKind { Onchain(); Bolt11(PaymentHash hash, PaymentPreimage? preimage, PaymentSecret? secret); Bolt11Jit(PaymentHash hash, PaymentPreimage? preimage, PaymentSecret? secret, LSPFeeLimits lsp_fee_limits); - Bolt12Offer(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret, OfferId offer_id); - Bolt12Refund(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret); + Bolt12Offer(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret, OfferId offer_id, UntrustedString? payer_note, u64? quantity); + Bolt12Refund(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret, UntrustedString? payer_note, u64? quantity); Spontaneous(PaymentHash hash, PaymentPreimage? preimage); }; diff --git a/src/error.rs b/src/error.rs index 7506b013b..deaf6db31 100644 --- a/src/error.rs +++ b/src/error.rs @@ -89,6 +89,8 @@ pub enum Error { InvalidNetwork, /// The given URI is invalid. InvalidUri, + /// The given quantity is invalid. + InvalidQuantity, /// A payment with the given hash has already been initiated. DuplicatePayment, /// The provided offer was denonminated in an unsupported currency. @@ -153,6 +155,7 @@ impl fmt::Display for Error { 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::InvalidQuantity => write!(f, "The given quantity is invalid."), Self::DuplicatePayment => { write!(f, "A payment with the given hash has already been initiated.") }, diff --git a/src/event.rs b/src/event.rs index e319ab5e4..c4c5034ff 100644 --- a/src/event.rs +++ b/src/event.rs @@ -597,12 +597,16 @@ where payment_context, .. } => { + let payer_note = payment_context.invoice_request.payer_note_truncated; let offer_id = payment_context.offer_id; + let quantity = payment_context.invoice_request.quantity; let kind = PaymentKind::Bolt12Offer { hash: Some(payment_hash), preimage: payment_preimage, secret: Some(payment_secret), offer_id, + payer_note, + quantity, }; let payment = PaymentDetails::new( diff --git a/src/payment/bolt12.rs b/src/payment/bolt12.rs index 40f1fc369..577dc92ae 100644 --- a/src/payment/bolt12.rs +++ b/src/payment/bolt12.rs @@ -12,12 +12,14 @@ use crate::types::ChannelManager; use lightning::ln::channelmanager::{PaymentId, Retry}; use lightning::offers::invoice::Bolt12Invoice; -use lightning::offers::offer::{Amount, Offer}; +use lightning::offers::offer::{Amount, Offer, Quantity}; use lightning::offers::parse::Bolt12SemanticError; use lightning::offers::refund::Refund; +use lightning::util::string::UntrustedString; use rand::RngCore; +use std::num::NonZeroU64; use std::sync::{Arc, RwLock}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; @@ -47,13 +49,15 @@ impl Bolt12Payment { /// /// If `payer_note` is `Some` it will be seen by the recipient and reflected back in the invoice /// response. - pub fn send(&self, offer: &Offer, payer_note: Option) -> Result { + /// + /// If `quantity` is `Some` it represents the number of items requested. + pub fn send( + &self, offer: &Offer, quantity: Option, payer_note: Option, + ) -> Result { let rt_lock = self.runtime.read().unwrap(); if rt_lock.is_none() { return Err(Error::NotRunning); } - - let quantity = None; let mut random_bytes = [0u8; 32]; rand::thread_rng().fill_bytes(&mut random_bytes); let payment_id = PaymentId(random_bytes); @@ -76,7 +80,7 @@ impl Bolt12Payment { &offer, quantity, None, - payer_note, + payer_note.clone(), payment_id, retry_strategy, max_total_routing_fee_msat, @@ -95,6 +99,8 @@ impl Bolt12Payment { preimage: None, secret: None, offer_id: offer.id(), + payer_note: payer_note.map(UntrustedString), + quantity, }; let payment = PaymentDetails::new( payment_id, @@ -117,6 +123,8 @@ impl Bolt12Payment { preimage: None, secret: None, offer_id: offer.id(), + payer_note: payer_note.map(UntrustedString), + quantity, }; let payment = PaymentDetails::new( payment_id, @@ -143,14 +151,13 @@ impl Bolt12Payment { /// If `payer_note` is `Some` it will be seen by the recipient and reflected back in the invoice /// response. pub fn send_using_amount( - &self, offer: &Offer, payer_note: Option, amount_msat: u64, + &self, offer: &Offer, amount_msat: u64, quantity: Option, payer_note: Option, ) -> Result { let rt_lock = self.runtime.read().unwrap(); if rt_lock.is_none() { return Err(Error::NotRunning); } - let quantity = None; let mut random_bytes = [0u8; 32]; rand::thread_rng().fill_bytes(&mut random_bytes); let payment_id = PaymentId(random_bytes); @@ -177,7 +184,7 @@ impl Bolt12Payment { &offer, quantity, Some(amount_msat), - payer_note, + payer_note.clone(), payment_id, retry_strategy, max_total_routing_fee_msat, @@ -196,6 +203,8 @@ impl Bolt12Payment { preimage: None, secret: None, offer_id: offer.id(), + payer_note: payer_note.map(UntrustedString), + quantity, }; let payment = PaymentDetails::new( payment_id, @@ -218,6 +227,8 @@ impl Bolt12Payment { preimage: None, secret: None, offer_id: offer.id(), + payer_note: payer_note.map(UntrustedString), + quantity, }; let payment = PaymentDetails::new( payment_id, @@ -236,21 +247,32 @@ impl Bolt12Payment { /// Returns a payable offer that can be used to request and receive a payment of the amount /// given. - pub fn receive(&self, amount_msat: u64, description: &str) -> Result { + pub fn receive( + &self, amount_msat: u64, description: &str, quantity: Option, + ) -> Result { let offer_builder = self.channel_manager.create_offer_builder().map_err(|e| { log_error!(self.logger, "Failed to create offer builder: {:?}", e); Error::OfferCreationFailed })?; - let offer = offer_builder - .amount_msats(amount_msat) - .description(description.to_string()) - .build() - .map_err(|e| { - log_error!(self.logger, "Failed to create offer: {:?}", e); - Error::OfferCreationFailed - })?; - Ok(offer) + let mut offer = + offer_builder.amount_msats(amount_msat).description(description.to_string()); + + if let Some(qty) = quantity { + if qty == 0 { + log_error!(self.logger, "Failed to create offer: quantity can't be zero."); + return Err(Error::InvalidQuantity); + } else { + offer = offer.supported_quantity(Quantity::Bounded(NonZeroU64::new(qty).unwrap())) + }; + }; + + let finalized_offer = offer.build().map_err(|e| { + log_error!(self.logger, "Failed to create offer: {:?}", e); + Error::OfferCreationFailed + })?; + + Ok(finalized_offer) } /// Returns a payable offer that can be used to request and receive a payment for which the @@ -281,8 +303,13 @@ impl Bolt12Payment { let payment_hash = invoice.payment_hash(); let payment_id = PaymentId(payment_hash.0); - let kind = - PaymentKind::Bolt12Refund { hash: Some(payment_hash), preimage: None, secret: None }; + let kind = PaymentKind::Bolt12Refund { + hash: Some(payment_hash), + preimage: None, + secret: None, + payer_note: refund.payer_note().map(|note| UntrustedString(note.0.to_string())), + quantity: refund.quantity(), + }; let payment = PaymentDetails::new( payment_id, @@ -298,7 +325,10 @@ impl Bolt12Payment { } /// Returns a [`Refund`] object that can be used to offer a refund payment of the amount given. - pub fn initiate_refund(&self, amount_msat: u64, expiry_secs: u32) -> Result { + pub fn initiate_refund( + &self, amount_msat: u64, expiry_secs: u32, quantity: Option, + payer_note: Option, + ) -> Result { let mut random_bytes = [0u8; 32]; rand::thread_rng().fill_bytes(&mut random_bytes); let payment_id = PaymentId(random_bytes); @@ -309,7 +339,7 @@ impl Bolt12Payment { let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT); let max_total_routing_fee_msat = None; - let refund = self + let mut refund_builder = self .channel_manager .create_refund_builder( amount_msat, @@ -321,17 +351,30 @@ impl Bolt12Payment { .map_err(|e| { log_error!(self.logger, "Failed to create refund builder: {:?}", e); Error::RefundCreationFailed - })? - .build() - .map_err(|e| { - log_error!(self.logger, "Failed to create refund: {:?}", e); - Error::RefundCreationFailed })?; - log_info!(self.logger, "Offering refund of {}msat", amount_msat); + if let Some(qty) = quantity { + refund_builder = refund_builder.quantity(qty); + } + + if let Some(note) = payer_note.clone() { + refund_builder = refund_builder.payer_note(note); + } + + let refund = refund_builder.build().map_err(|e| { + log_error!(self.logger, "Failed to create refund: {:?}", e); + Error::RefundCreationFailed + })?; - let kind = PaymentKind::Bolt12Refund { hash: None, preimage: None, secret: None }; + log_info!(self.logger, "Offering refund of {}msat", amount_msat); + let kind = PaymentKind::Bolt12Refund { + hash: None, + preimage: None, + secret: None, + payer_note: payer_note.map(|note| UntrustedString(note)), + quantity, + }; let payment = PaymentDetails::new( payment_id, kind, diff --git a/src/payment/store.rs b/src/payment/store.rs index eb3ac091f..3c35043ce 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -11,6 +11,7 @@ use lightning::ln::msgs::DecodeError; use lightning::ln::{PaymentHash, PaymentPreimage, PaymentSecret}; use lightning::offers::offer::OfferId; use lightning::util::ser::{Readable, Writeable}; +use lightning::util::string::UntrustedString; use lightning::{ _init_and_read_len_prefixed_tlv_fields, impl_writeable_tlv_based, impl_writeable_tlv_based_enum, write_tlv_fields, @@ -212,6 +213,18 @@ pub enum PaymentKind { secret: Option, /// The ID of the offer this payment is for. offer_id: OfferId, + /// The payer note for the payment. + /// + /// Truncated to [`PAYER_NOTE_LIMIT`] characters. + /// + /// This will always be `None` for payments serialized with version `v0.3.0`. + /// + /// [`PAYER_NOTE_LIMIT`]: lightning::offers::invoice_request::PAYER_NOTE_LIMIT + payer_note: Option, + /// The quantity of an item requested in the offer. + /// + /// This will always be `None` for payments serialized with version `v0.3.0`. + quantity: Option, }, /// A [BOLT 12] 'refund' payment, i.e., a payment for a [`Refund`]. /// @@ -224,6 +237,14 @@ pub enum PaymentKind { preimage: Option, /// The secret used by the payment. secret: Option, + /// The payer note for the refund payment. + /// + /// This will always be `None` for payments serialized with version `v0.3.0`. + payer_note: Option, + /// The quantity of an item that the refund is for. + /// + /// This will always be `None` for payments serialized with version `v0.3.0`. + quantity: Option, }, /// A spontaneous ("keysend") payment. Spontaneous { @@ -249,7 +270,9 @@ impl_writeable_tlv_based_enum!(PaymentKind, }, (6, Bolt12Offer) => { (0, hash, option), + (1, payer_note, option), (2, preimage, option), + (3, quantity, option), (4, secret, option), (6, offer_id, required), }, @@ -259,7 +282,9 @@ impl_writeable_tlv_based_enum!(PaymentKind, }, (10, Bolt12Refund) => { (0, hash, option), + (1, payer_note, option), (2, preimage, option), + (3, quantity, option), (4, secret, option), }; ); diff --git a/src/payment/unified_qr.rs b/src/payment/unified_qr.rs index a4551eb8a..b40be5521 100644 --- a/src/payment/unified_qr.rs +++ b/src/payment/unified_qr.rs @@ -92,7 +92,7 @@ impl UnifiedQrPayment { let amount_msats = amount_sats * 1_000; - let bolt12_offer = match self.bolt12_payment.receive(amount_msats, description) { + let bolt12_offer = match self.bolt12_payment.receive(amount_msats, description, None) { Ok(offer) => Some(offer), Err(e) => { log_error!(self.logger, "Failed to create offer: {}", e); @@ -136,7 +136,7 @@ impl UnifiedQrPayment { 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) { + match self.bolt12_payment.send(&offer, None, 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), } diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 5a918762a..ec2b3d917 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -424,17 +424,31 @@ fn simple_bolt12_send_receive() { std::thread::sleep(std::time::Duration::from_secs(1)); let expected_amount_msat = 100_000_000; - let offer = node_b.bolt12_payment().receive(expected_amount_msat, "asdf").unwrap(); - let payment_id = node_a.bolt12_payment().send(&offer, None).unwrap(); + let offer = node_b.bolt12_payment().receive(expected_amount_msat, "asdf", Some(1)).unwrap(); + let expected_quantity = Some(1); + let expected_payer_note = Some("Test".to_string()); + let payment_id = node_a + .bolt12_payment() + .send(&offer, expected_quantity, expected_payer_note.clone()) + .unwrap(); expect_payment_successful_event!(node_a, Some(payment_id), None); let node_a_payments = node_a.list_payments(); assert_eq!(node_a_payments.len(), 1); match node_a_payments.first().unwrap().kind { - PaymentKind::Bolt12Offer { hash, preimage, secret: _, offer_id } => { + PaymentKind::Bolt12Offer { + hash, + preimage, + secret: _, + offer_id, + quantity: ref qty, + payer_note: ref note, + } => { assert!(hash.is_some()); assert!(preimage.is_some()); assert_eq!(offer_id, offer.id()); + assert_eq!(&expected_quantity, qty); + assert_eq!(expected_payer_note.unwrap(), note.clone().unwrap().0); //TODO: We should eventually set and assert the secret sender-side, too, but the BOLT12 //API currently doesn't allow to do that. }, @@ -448,7 +462,7 @@ fn simple_bolt12_send_receive() { let node_b_payments = node_b.list_payments(); assert_eq!(node_b_payments.len(), 1); match node_b_payments.first().unwrap().kind { - PaymentKind::Bolt12Offer { hash, preimage, secret, offer_id } => { + PaymentKind::Bolt12Offer { hash, preimage, secret, offer_id, .. } => { assert!(hash.is_some()); assert!(preimage.is_some()); assert!(secret.is_some()); @@ -464,22 +478,40 @@ fn simple_bolt12_send_receive() { let offer_amount_msat = 100_000_000; let less_than_offer_amount = offer_amount_msat - 10_000; let expected_amount_msat = offer_amount_msat + 10_000; - let offer = node_b.bolt12_payment().receive(offer_amount_msat, "asdf").unwrap(); + let offer = node_b.bolt12_payment().receive(offer_amount_msat, "asdf", Some(1)).unwrap(); + let expected_quantity = Some(1); + let expected_payer_note = Some("Test".to_string()); assert!(node_a .bolt12_payment() - .send_using_amount(&offer, None, less_than_offer_amount) + .send_using_amount(&offer, less_than_offer_amount, None, None) .is_err()); - let payment_id = - node_a.bolt12_payment().send_using_amount(&offer, None, expected_amount_msat).unwrap(); + let payment_id = node_a + .bolt12_payment() + .send_using_amount( + &offer, + expected_amount_msat, + expected_quantity, + expected_payer_note.clone(), + ) + .unwrap(); expect_payment_successful_event!(node_a, Some(payment_id), None); let node_a_payments = node_a.list_payments_with_filter(|p| p.id == payment_id); assert_eq!(node_a_payments.len(), 1); let payment_hash = match node_a_payments.first().unwrap().kind { - PaymentKind::Bolt12Offer { hash, preimage, secret: _, offer_id } => { + PaymentKind::Bolt12Offer { + hash, + preimage, + secret: _, + offer_id, + quantity: ref qty, + payer_note: ref note, + } => { assert!(hash.is_some()); assert!(preimage.is_some()); assert_eq!(offer_id, offer.id()); + assert_eq!(&expected_quantity, qty); + assert_eq!(expected_payer_note.unwrap(), note.clone().unwrap().0); //TODO: We should eventually set and assert the secret sender-side, too, but the BOLT12 //API currently doesn't allow to do that. hash.unwrap() @@ -495,7 +527,7 @@ fn simple_bolt12_send_receive() { let node_b_payments = node_b.list_payments_with_filter(|p| p.id == node_b_payment_id); assert_eq!(node_b_payments.len(), 1); match node_b_payments.first().unwrap().kind { - PaymentKind::Bolt12Offer { hash, preimage, secret, offer_id } => { + PaymentKind::Bolt12Offer { hash, preimage, secret, offer_id, .. } => { assert!(hash.is_some()); assert!(preimage.is_some()); assert!(secret.is_some()); @@ -509,7 +541,12 @@ fn simple_bolt12_send_receive() { // Now node_b refunds the amount node_a just overpaid. let overpaid_amount = expected_amount_msat - offer_amount_msat; - let refund = node_b.bolt12_payment().initiate_refund(overpaid_amount, 3600).unwrap(); + let expected_quantity = Some(1); + let expected_payer_note = Some("Test".to_string()); + let refund = node_b + .bolt12_payment() + .initiate_refund(overpaid_amount, 3600, expected_quantity, expected_payer_note.clone()) + .unwrap(); let invoice = node_a.bolt12_payment().request_refund_payment(&refund).unwrap(); expect_payment_received_event!(node_a, overpaid_amount); @@ -523,9 +560,17 @@ fn simple_bolt12_send_receive() { let node_b_payments = node_b.list_payments_with_filter(|p| p.id == node_b_payment_id); assert_eq!(node_b_payments.len(), 1); match node_b_payments.first().unwrap().kind { - PaymentKind::Bolt12Refund { hash, preimage, secret: _ } => { + PaymentKind::Bolt12Refund { + hash, + preimage, + secret: _, + quantity: ref qty, + payer_note: ref note, + } => { assert!(hash.is_some()); assert!(preimage.is_some()); + assert_eq!(&expected_quantity, qty); + assert_eq!(expected_payer_note.unwrap(), note.clone().unwrap().0) //TODO: We should eventually set and assert the secret sender-side, too, but the BOLT12 //API currently doesn't allow to do that. }, @@ -539,7 +584,7 @@ fn simple_bolt12_send_receive() { let node_a_payments = node_a.list_payments_with_filter(|p| p.id == node_a_payment_id); assert_eq!(node_a_payments.len(), 1); match node_a_payments.first().unwrap().kind { - PaymentKind::Bolt12Refund { hash, preimage, secret } => { + PaymentKind::Bolt12Refund { hash, preimage, secret, .. } => { assert!(hash.is_some()); assert!(preimage.is_some()); assert!(secret.is_some());