Skip to content

Commit

Permalink
Merge pull request lightningdevkit#327 from slanesuke/2024-07-expose-…
Browse files Browse the repository at this point in the history
…payer_note-in-PKbolt12

Expose `payer_note` in `PaymentKind::Bolt12`
  • Loading branch information
tnull authored Aug 19, 2024
2 parents 3b645b3 + 683bfb3 commit 76fb23f
Show file tree
Hide file tree
Showing 7 changed files with 171 additions and 50 deletions.
13 changes: 7 additions & 6 deletions bindings/ldk_node.udl
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -201,6 +201,7 @@ enum NodeError {
"InvalidChannelId",
"InvalidNetwork",
"InvalidUri",
"InvalidQuantity",
"DuplicatePayment",
"UnsupportedCurrency",
"InsufficientFunds",
Expand Down Expand Up @@ -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);
};

Expand Down
3 changes: 3 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.")
},
Expand Down
4 changes: 4 additions & 0 deletions src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
101 changes: 72 additions & 29 deletions src/payment/bolt12.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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<String>) -> Result<PaymentId, Error> {
///
/// If `quantity` is `Some` it represents the number of items requested.
pub fn send(
&self, offer: &Offer, quantity: Option<u64>, payer_note: Option<String>,
) -> Result<PaymentId, Error> {
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);
Expand All @@ -76,7 +80,7 @@ impl Bolt12Payment {
&offer,
quantity,
None,
payer_note,
payer_note.clone(),
payment_id,
retry_strategy,
max_total_routing_fee_msat,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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<String>, amount_msat: u64,
&self, offer: &Offer, amount_msat: u64, quantity: Option<u64>, payer_note: Option<String>,
) -> Result<PaymentId, Error> {
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);
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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<Offer, Error> {
pub fn receive(
&self, amount_msat: u64, description: &str, quantity: Option<u64>,
) -> Result<Offer, Error> {
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
Expand Down Expand Up @@ -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,
Expand All @@ -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<Refund, Error> {
pub fn initiate_refund(
&self, amount_msat: u64, expiry_secs: u32, quantity: Option<u64>,
payer_note: Option<String>,
) -> Result<Refund, Error> {
let mut random_bytes = [0u8; 32];
rand::thread_rng().fill_bytes(&mut random_bytes);
let payment_id = PaymentId(random_bytes);
Expand All @@ -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,
Expand All @@ -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,
Expand Down
25 changes: 25 additions & 0 deletions src/payment/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -212,6 +213,18 @@ pub enum PaymentKind {
secret: Option<PaymentSecret>,
/// 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<UntrustedString>,
/// The quantity of an item requested in the offer.
///
/// This will always be `None` for payments serialized with version `v0.3.0`.
quantity: Option<u64>,
},
/// A [BOLT 12] 'refund' payment, i.e., a payment for a [`Refund`].
///
Expand All @@ -224,6 +237,14 @@ pub enum PaymentKind {
preimage: Option<PaymentPreimage>,
/// The secret used by the payment.
secret: Option<PaymentSecret>,
/// The payer note for the refund payment.
///
/// This will always be `None` for payments serialized with version `v0.3.0`.
payer_note: Option<UntrustedString>,
/// 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<u64>,
},
/// A spontaneous ("keysend") payment.
Spontaneous {
Expand All @@ -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),
},
Expand All @@ -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),
};
);
Expand Down
4 changes: 2 additions & 2 deletions src/payment/unified_qr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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),
}
Expand Down
Loading

0 comments on commit 76fb23f

Please sign in to comment.