Skip to content

Commit

Permalink
Extend API to allow invoice creation with a description hash
Browse files Browse the repository at this point in the history
  • Loading branch information
joostjager committed Jan 21, 2025
1 parent 9953d2b commit be6be7e
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 26 deletions.
14 changes: 10 additions & 4 deletions bindings/ldk_node.udl
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ interface Node {
boolean verify_signature([ByRef]sequence<u8> msg, [ByRef]string sig, [ByRef]PublicKey pkey);
};

[Enum]
interface Bolt11InvoiceStringDescription {
Hash(string hash);
Direct(string description);
};

interface Bolt11Payment {
[Throws=NodeError]
PaymentId send([ByRef]Bolt11Invoice invoice, SendingParameters? sending_parameters);
Expand All @@ -120,13 +126,13 @@ interface Bolt11Payment {
[Throws=NodeError]
void fail_for_hash(PaymentHash payment_hash);
[Throws=NodeError]
Bolt11Invoice receive(u64 amount_msat, [ByRef]string description, u32 expiry_secs);
Bolt11Invoice receive(u64 amount_msat, [ByRef]Bolt11InvoiceStringDescription description, u32 expiry_secs);
[Throws=NodeError]
Bolt11Invoice receive_for_hash(u64 amount_msat, [ByRef]string description, u32 expiry_secs, PaymentHash payment_hash);
Bolt11Invoice receive_for_hash(u64 amount_msat, [ByRef]Bolt11InvoiceStringDescription description, u32 expiry_secs, PaymentHash payment_hash);
[Throws=NodeError]
Bolt11Invoice receive_variable_amount([ByRef]string description, u32 expiry_secs);
Bolt11Invoice receive_variable_amount([ByRef]Bolt11InvoiceStringDescription description, u32 expiry_secs);
[Throws=NodeError]
Bolt11Invoice receive_variable_amount_for_hash([ByRef]string description, u32 expiry_secs, PaymentHash payment_hash);
Bolt11Invoice receive_variable_amount_for_hash([ByRef]Bolt11InvoiceStringDescription description, u32 expiry_secs, PaymentHash payment_hash);
[Throws=NodeError]
Bolt11Invoice receive_via_jit_channel(u64 amount_msat, [ByRef]string description, u32 expiry_secs, u64? max_lsp_fee_limit_msat);
[Throws=NodeError]
Expand Down
90 changes: 77 additions & 13 deletions src/payment/bolt11.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,13 @@ use lightning::routing::router::{PaymentParameters, RouteParameters};

use lightning_types::payment::{PaymentHash, PaymentPreimage};

use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description};
use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description };

use bitcoin::hashes::sha256::Hash as Sha256;
use bitcoin::hashes::Hash;
use bitcoin::hashes::sha256::Hash as Sha256;

use std::sync::{Arc, RwLock};
use std::str::FromStr;

/// A payment handler allowing to create and pay [BOLT 11] invoices.
///
Expand Down Expand Up @@ -403,12 +404,22 @@ impl Bolt11Payment {
/// given.
///
/// The inbound payment will be automatically claimed upon arrival.
#[cfg(not(feature = "uniffi"))]
pub fn receive(
&self, amount_msat: u64, description: &str, expiry_secs: u32,
&self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32,
) -> Result<Bolt11Invoice, Error> {
self.receive_inner(Some(amount_msat), description, expiry_secs, None)
}

#[cfg(feature = "uniffi")]
pub fn receive(
&self, amount_msat: u64, description: &Bolt11InvoiceStringDescription, expiry_secs: u32,
) -> Result<Bolt11Invoice, Error> {
let invoice_description: Bolt11InvoiceDescription = description.try_into()?;
self.receive_inner(Some(amount_msat), &invoice_description, expiry_secs, None)
}


/// Returns a payable invoice that can be used to request a payment of the amount
/// given for the given payment hash.
///
Expand All @@ -423,22 +434,40 @@ impl Bolt11Payment {
/// [`PaymentClaimable`]: crate::Event::PaymentClaimable
/// [`claim_for_hash`]: Self::claim_for_hash
/// [`fail_for_hash`]: Self::fail_for_hash
#[cfg(not(feature = "uniffi"))]
pub fn receive_for_hash(
&self, amount_msat: u64, description: &str, expiry_secs: u32, payment_hash: PaymentHash,
&self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, payment_hash: PaymentHash,
) -> Result<Bolt11Invoice, Error> {
self.receive_inner(Some(amount_msat), description, expiry_secs, Some(payment_hash))
}

#[cfg(feature = "uniffi")]
pub fn receive_for_hash(
&self, amount_msat: u64, description: &Bolt11InvoiceStringDescription, expiry_secs: u32, payment_hash: PaymentHash,
) -> Result<Bolt11Invoice, Error> {
let invoice_description: Bolt11InvoiceDescription = description.try_into()?;
self.receive_inner(Some(amount_msat), &invoice_description, expiry_secs, Some(payment_hash))
}

/// Returns a payable invoice that can be used to request and receive a payment for which the
/// amount is to be determined by the user, also known as a "zero-amount" invoice.
///
/// The inbound payment will be automatically claimed upon arrival.
#[cfg(not(feature = "uniffi"))]
pub fn receive_variable_amount(
&self, description: &str, expiry_secs: u32,
&self, description: &Bolt11InvoiceDescription, expiry_secs: u32,
) -> Result<Bolt11Invoice, Error> {
self.receive_inner(None, description, expiry_secs, None)
}

#[cfg(feature = "uniffi")]
pub fn receive_variable_amount(
&self, description: &Bolt11InvoiceStringDescription, expiry_secs: u32,
) -> Result<Bolt11Invoice, Error> {
let invoice_description: Bolt11InvoiceDescription = description.try_into()?;
self.receive_inner(None, &invoice_description, expiry_secs, None)
}

/// Returns a payable invoice that can be used to request a payment for the given payment hash
/// and the amount to be determined by the user, also known as a "zero-amount" invoice.
///
Expand All @@ -453,24 +482,29 @@ impl Bolt11Payment {
/// [`PaymentClaimable`]: crate::Event::PaymentClaimable
/// [`claim_for_hash`]: Self::claim_for_hash
/// [`fail_for_hash`]: Self::fail_for_hash
#[cfg(not(feature = "uniffi"))]
pub fn receive_variable_amount_for_hash(
&self, description: &str, expiry_secs: u32, payment_hash: PaymentHash,
&self, description: &Bolt11InvoiceDescription, expiry_secs: u32, payment_hash: PaymentHash,
) -> Result<Bolt11Invoice, Error> {
self.receive_inner(None, description, expiry_secs, Some(payment_hash))
}

fn receive_inner(
&self, amount_msat: Option<u64>, description: &str, expiry_secs: u32,
manual_claim_payment_hash: Option<PaymentHash>,
#[cfg(feature = "uniffi")]
pub fn receive_variable_amount_for_hash(
&self, description: &Bolt11InvoiceStringDescription, expiry_secs: u32, payment_hash: PaymentHash,
) -> Result<Bolt11Invoice, Error> {
let invoice_description = Bolt11InvoiceDescription::Direct(
Description::new(description.to_string()).map_err(|_| Error::InvoiceCreationFailed)?,
);
let invoice_description: Bolt11InvoiceDescription = description.try_into()?;
self.receive_inner(None, &invoice_description, expiry_secs, Some(payment_hash))
}

pub(crate) fn receive_inner(
&self, amount_msat: Option<u64>, invoice_description: &Bolt11InvoiceDescription, expiry_secs: u32,
manual_claim_payment_hash: Option<PaymentHash>,
) -> Result<Bolt11Invoice, Error> {
let invoice = {
let invoice_params = Bolt11InvoiceParameters {
amount_msats: amount_msat,
description: invoice_description,
description: invoice_description.clone(),
invoice_expiry_delta_secs: Some(expiry_secs),
payment_hash: manual_claim_payment_hash,
..Default::default()
Expand Down Expand Up @@ -740,3 +774,33 @@ impl Bolt11Payment {
Ok(())
}
}

/// Represents the description of an invoice which has to be either a directly included string or
/// a hash of a description provided out of band.
pub enum Bolt11InvoiceStringDescription { // use same name (no string)
/// Full description of what the invoice is for
Direct{
/// Description of what the invoice is for
description: String
},
/// Hashed description of what the invoice is for
Hash{
/// Hash of the description of what the invoice is for
hash: String
}
}

impl TryInto<Bolt11InvoiceDescription> for &Bolt11InvoiceStringDescription {
type Error = Error;

fn try_into(self) -> Result<Bolt11InvoiceDescription, Self::Error> {
match self {
Bolt11InvoiceStringDescription::Direct{description} => {
Description::new(description.clone()).map(Bolt11InvoiceDescription::Direct).map_err(|_| Error::InvalidInvoice)
},
Bolt11InvoiceStringDescription::Hash{hash} => {
Sha256::from_str(&hash).map(lightning_invoice::Sha256).map(Bolt11InvoiceDescription::Hash).map_err(|_| Error::InvalidInvoice)
},
}
}
}
1 change: 1 addition & 0 deletions src/payment/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub use onchain::OnchainPayment;
pub use spontaneous::SpontaneousPayment;
pub use store::{LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus};
pub use unified_qr::{QrPaymentResult, UnifiedQrPayment};
pub use bolt11::Bolt11InvoiceStringDescription;

/// Represents information used to send a payment.
#[derive(Clone, Debug, PartialEq)]
Expand Down
7 changes: 5 additions & 2 deletions src/payment/unified_qr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use crate::Config;

use lightning::ln::channelmanager::PaymentId;
use lightning::offers::offer::Offer;
use lightning_invoice::Bolt11Invoice;
use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description};

use bip21::de::ParamKind;
use bip21::{DeserializationError, DeserializeParams, Param, SerializeParams};
Expand Down Expand Up @@ -99,8 +99,11 @@ impl UnifiedQrPayment {
},
};

let invoice_description = Bolt11InvoiceDescription::Direct(
Description::new(description.to_string()).map_err(|_| Error::InvoiceCreationFailed)?,
);
let bolt11_invoice =
match self.bolt11_invoice.receive(amount_msats, description, expiry_sec) {
match self.bolt11_invoice.receive_inner(Some(amount_msats), &invoice_description, expiry_sec, None) {
Ok(invoice) => Some(invoice),
Err(e) => {
log_error!(self.logger, "Failed to create invoice {}", e);
Expand Down
2 changes: 2 additions & 0 deletions src/uniffi_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ pub use bip39::Mnemonic;

pub use vss_client::headers::{VssHeaderProvider, VssHeaderProviderError};

pub use crate::payment::Bolt11InvoiceStringDescription;

use crate::UniffiCustomTypeConverter;

use crate::builder::sanitize_alias;
Expand Down
14 changes: 8 additions & 6 deletions tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use lightning::routing::gossip::NodeAlias;
use lightning::util::persist::KVStore;
use lightning::util::test_utils::TestStore;

use lightning_invoice::{Bolt11InvoiceDescription, Description};
use lightning_types::payment::{PaymentHash, PaymentPreimage};

use lightning_persister::fs_store::FilesystemStore;
Expand Down Expand Up @@ -552,7 +553,8 @@ pub(crate) fn do_channel_full_cycle<E: ElectrumApi>(

println!("\nB receive");
let invoice_amount_1_msat = 2500_000;
let invoice = node_b.bolt11_payment().receive(invoice_amount_1_msat, &"asdf", 9217).unwrap();
let invoice_description: Bolt11InvoiceDescription = Bolt11InvoiceDescription::Direct(Description::new(String::from("asdf")).unwrap());
let invoice = node_b.bolt11_payment().receive(invoice_amount_1_msat, &invoice_description, 9217).unwrap();

println!("\nA send");
let payment_id = node_a.bolt11_payment().send(&invoice, None).unwrap();
Expand Down Expand Up @@ -598,7 +600,7 @@ pub(crate) fn do_channel_full_cycle<E: ElectrumApi>(

// Test under-/overpayment
let invoice_amount_2_msat = 2500_000;
let invoice = node_b.bolt11_payment().receive(invoice_amount_2_msat, &"asdf", 9217).unwrap();
let invoice = node_b.bolt11_payment().receive(invoice_amount_2_msat, &invoice_description, 9217).unwrap();

let underpaid_amount = invoice_amount_2_msat - 1;
assert_eq!(
Expand All @@ -607,7 +609,7 @@ pub(crate) fn do_channel_full_cycle<E: ElectrumApi>(
);

println!("\nB overpaid receive");
let invoice = node_b.bolt11_payment().receive(invoice_amount_2_msat, &"asdf", 9217).unwrap();
let invoice = node_b.bolt11_payment().receive(invoice_amount_2_msat, &invoice_description, 9217).unwrap();
let overpaid_amount_msat = invoice_amount_2_msat + 100;

println!("\nA overpaid send");
Expand Down Expand Up @@ -637,7 +639,7 @@ pub(crate) fn do_channel_full_cycle<E: ElectrumApi>(
// Test "zero-amount" invoice payment
println!("\nB receive_variable_amount_payment");
let variable_amount_invoice =
node_b.bolt11_payment().receive_variable_amount(&"asdf", 9217).unwrap();
node_b.bolt11_payment().receive_variable_amount(&invoice_description, 9217).unwrap();
let determined_amount_msat = 2345_678;
assert_eq!(
Err(NodeError::InvalidInvoice),
Expand Down Expand Up @@ -676,7 +678,7 @@ pub(crate) fn do_channel_full_cycle<E: ElectrumApi>(
let manual_payment_hash = PaymentHash(Sha256::hash(&manual_preimage.0).to_byte_array());
let manual_invoice = node_b
.bolt11_payment()
.receive_for_hash(invoice_amount_3_msat, &"asdf", 9217, manual_payment_hash)
.receive_for_hash(invoice_amount_3_msat, &invoice_description, 9217, manual_payment_hash)
.unwrap();
let manual_payment_id = node_a.bolt11_payment().send(&manual_invoice, None).unwrap();

Expand Down Expand Up @@ -714,7 +716,7 @@ pub(crate) fn do_channel_full_cycle<E: ElectrumApi>(
PaymentHash(Sha256::hash(&manual_fail_preimage.0).to_byte_array());
let manual_fail_invoice = node_b
.bolt11_payment()
.receive_for_hash(invoice_amount_3_msat, &"asdf", 9217, manual_fail_payment_hash)
.receive_for_hash(invoice_amount_3_msat, &invoice_description, 9217, manual_fail_payment_hash)
.unwrap();
let manual_fail_payment_id = node_a.bolt11_payment().send(&manual_fail_invoice, None).unwrap();

Expand Down
4 changes: 3 additions & 1 deletion tests/integration_tests_rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use lightning::util::persist::KVStore;
use bitcoincore_rpc::RpcApi;

use bitcoin::Amount;
use lightning_invoice::{Bolt11InvoiceDescription, Description};

use std::sync::Arc;

Expand Down Expand Up @@ -189,7 +190,8 @@ fn multi_hop_sending() {
max_channel_saturation_power_of_half: Some(2),
};

let invoice = nodes[4].bolt11_payment().receive(2_500_000, &"asdf", 9217).unwrap();
let invoice_description = Bolt11InvoiceDescription::Direct(Description::new(String::from("asdf")).unwrap());
let invoice = nodes[4].bolt11_payment().receive(2_500_000, &invoice_description, 9217).unwrap();
nodes[0].bolt11_payment().send(&invoice, Some(sending_params)).unwrap();

expect_event!(nodes[1], PaymentForwarded);
Expand Down

0 comments on commit be6be7e

Please sign in to comment.