Skip to content

Extend API to allow invoice creation with a description hash #438

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,8 @@ class LibraryTest {
else -> return
}

val invoice = node2.bolt11Payment().receive(2500000u, "asdf", 9217u)
val description = Bolt11InvoiceDescription.Direct("asdf")
val invoice = node2.bolt11Payment().receive(2500000u, description, 9217u)

node1.bolt11Payment().send(invoice, null)

Expand Down
18 changes: 12 additions & 6 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 Bolt11InvoiceDescription {
Hash(string hash);
Direct(string description);
};

interface Bolt11Payment {
[Throws=NodeError]
PaymentId send([ByRef]Bolt11Invoice invoice, SendingParameters? sending_parameters);
Expand All @@ -120,17 +126,17 @@ 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]Bolt11InvoiceDescription 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]Bolt11InvoiceDescription description, u32 expiry_secs, PaymentHash payment_hash);
[Throws=NodeError]
Bolt11Invoice receive_variable_amount([ByRef]string description, u32 expiry_secs);
Bolt11Invoice receive_variable_amount([ByRef]Bolt11InvoiceDescription 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]Bolt11InvoiceDescription 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);
Bolt11Invoice receive_via_jit_channel(u64 amount_msat, [ByRef]Bolt11InvoiceDescription description, u32 expiry_secs, u64? max_lsp_fee_limit_msat);
[Throws=NodeError]
Bolt11Invoice receive_variable_amount_via_jit_channel([ByRef]string description, u32 expiry_secs, u64? max_proportional_lsp_fee_limit_ppm_msat);
Bolt11Invoice receive_variable_amount_via_jit_channel([ByRef]Bolt11InvoiceDescription description, u32 expiry_secs, u64? max_proportional_lsp_fee_limit_ppm_msat);
};

interface Bolt12Payment {
Expand Down
3 changes: 2 additions & 1 deletion bindings/python/src/ldk_node/test_ldk_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,8 @@ def test_channel_full_cycle(self):
print("EVENT:", channel_ready_event_2)
node_2.event_handled()

invoice = node_2.bolt11_payment().receive(2500000, "asdf", 9217)
description = Bolt11InvoiceDescription.DIRECT("asdf")
invoice = node_2.bolt11_payment().receive(2500000, description, 9217)
node_1.bolt11_payment().send(invoice, None)

payment_successful_event_1 = node_1.wait_next_event()
Expand Down
12 changes: 6 additions & 6 deletions src/liquidity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::{Config, Error};
use lightning::ln::channelmanager::MIN_FINAL_CLTV_EXPIRY_DELTA;
use lightning::ln::msgs::SocketAddress;
use lightning::routing::router::{RouteHint, RouteHintHop};
use lightning_invoice::{Bolt11Invoice, InvoiceBuilder, RoutingFees};
use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, InvoiceBuilder, RoutingFees};
use lightning_liquidity::events::Event;
use lightning_liquidity::lsps0::ser::RequestId;
use lightning_liquidity::lsps2::event::LSPS2ClientEvent;
Expand Down Expand Up @@ -196,7 +196,7 @@ where
}

pub(crate) async fn lsps2_receive_to_jit_channel(
&self, amount_msat: u64, description: &str, expiry_secs: u32,
&self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32,
max_total_lsp_fee_limit_msat: Option<u64>,
) -> Result<(Bolt11Invoice, u64), Error> {
let fee_response = self.lsps2_request_opening_fee_params().await?;
Expand Down Expand Up @@ -256,7 +256,7 @@ where
}

pub(crate) async fn lsps2_receive_variable_amount_to_jit_channel(
&self, description: &str, expiry_secs: u32,
&self, description: &Bolt11InvoiceDescription, expiry_secs: u32,
max_proportional_lsp_fee_limit_ppm_msat: Option<u64>,
) -> Result<(Bolt11Invoice, u64), Error> {
let fee_response = self.lsps2_request_opening_fee_params().await?;
Expand Down Expand Up @@ -373,8 +373,8 @@ where
}

fn lsps2_create_jit_invoice(
&self, buy_response: LSPS2BuyResponse, amount_msat: Option<u64>, description: &str,
expiry_secs: u32,
&self, buy_response: LSPS2BuyResponse, amount_msat: Option<u64>,
description: &Bolt11InvoiceDescription, expiry_secs: u32,
) -> Result<Bolt11Invoice, Error> {
let lsps2_service = self.lsps2_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?;

Expand Down Expand Up @@ -404,7 +404,7 @@ where

let currency = self.config.network.into();
let mut invoice_builder = InvoiceBuilder::new(currency)
.description(description.to_string())
.invoice_description(description.clone())
.payment_hash(payment_hash)
.payment_secret(payment_secret)
.current_timestamp()
Expand Down
157 changes: 140 additions & 17 deletions src/payment/bolt11.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use crate::config::{Config, LDK_PAYMENT_RETRY_TIMEOUT};
use crate::connection::ConnectionManager;
use crate::error::Error;
use crate::hex_utils;
use crate::liquidity::LiquiditySource;
use crate::logger::{log_error, log_info, FilesystemLogger, Logger};
use crate::payment::store::{
Expand All @@ -30,11 +31,12 @@ use lightning::routing::router::{PaymentParameters, RouteParameters};

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

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

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

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

/// A payment handler allowing to create and pay [BOLT 11] invoices.
Expand Down Expand Up @@ -403,12 +405,23 @@ 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: &lightning_invoice::Bolt11InvoiceDescription,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think rather than duplicating all of these, we should be able to follow the same approach as #434, i.e.,

a) add a local type alias

use lightning_invoice::Bolt11InvoiceDescription as LdkBolt11InvoiceDescription;

#[cfg(not(feature = "uniffi"))]
type Bolt11InvoiceDescription = LdkBolt11InvoiceDescription;
#[cfg(feature = "uniffi")]
type Bolt11InvoiceDescription = crate::uniffi_types::Bolt11InvoiceDescription;

above and have the receives use a macro like this:

macro_rules! maybe_convert_description {
	($description: expr) => {{
		#[cfg(not(feature = "uniffi"))]
		{
			$description
		}
		#[cfg(feature = "uniffi")]
		{
			&LdkBolt11InvoiceDescription::try_from($description)?
		}
	}};
}

(could also consider using it in the receive_inner/receive_via_jit_channel_inner, but the former is reused in unified_qr, which complicates things. So probably easier to do the conversion before giving the description to the _inners).

expiry_secs: u32,
) -> Result<Bolt11Invoice, Error> {
self.receive_inner(Some(amount_msat), description, expiry_secs, None)
}

#[cfg(feature = "uniffi")]
pub fn receive(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I double checked this as it confused me that it be possible to omit docs here. And indeed it doesn't work: note that if you run cargo doc --features uniffi --open, all the receive variants wouldn't have any docs on them.

For non-Uniffi we forbid missing docs on a crate-wide level (see top of lib.rs), but unfortunately we can't do this under the uniffi feature as some of the generated code doesn't have docs on it, which would have the deny(missing_docs) lint fail. This is why we didn't catch the missing docs at build time with the uniffi feature.

&self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32,
) -> Result<Bolt11Invoice, Error> {
let invoice_description =
lightning_invoice::Bolt11InvoiceDescription::try_from(description)?;
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 +436,44 @@ 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: &lightning_invoice::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: &Bolt11InvoiceDescription, expiry_secs: u32,
payment_hash: PaymentHash,
) -> Result<Bolt11Invoice, Error> {
let invoice_description =
lightning_invoice::Bolt11InvoiceDescription::try_from(description)?;
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: &lightning_invoice::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: &Bolt11InvoiceDescription, expiry_secs: u32,
) -> Result<Bolt11Invoice, Error> {
let invoice_description =
lightning_invoice::Bolt11InvoiceDescription::try_from(description)?;
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 +488,32 @@ 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: &lightning_invoice::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: &Bolt11InvoiceDescription, 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 =
lightning_invoice::Bolt11InvoiceDescription::try_from(description)?;
self.receive_inner(None, &invoice_description, expiry_secs, Some(payment_hash))
}

pub(crate) fn receive_inner(
&self, amount_msat: Option<u64>,
invoice_description: &lightning_invoice::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 @@ -530,9 +573,10 @@ impl Bolt11Payment {
/// channel to us. We'll use its cheapest offer otherwise.
///
/// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md
#[cfg(not(feature = "uniffi"))]
pub fn receive_via_jit_channel(
&self, amount_msat: u64, description: &str, expiry_secs: u32,
max_total_lsp_fee_limit_msat: Option<u64>,
&self, amount_msat: u64, description: &lightning_invoice::Bolt11InvoiceDescription,
expiry_secs: u32, max_total_lsp_fee_limit_msat: Option<u64>,
) -> Result<Bolt11Invoice, Error> {
self.receive_via_jit_channel_inner(
Some(amount_msat),
Expand All @@ -543,6 +587,22 @@ impl Bolt11Payment {
)
}

#[cfg(feature = "uniffi")]
pub fn receive_via_jit_channel(
&self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32,
max_total_lsp_fee_limit_msat: Option<u64>,
) -> Result<Bolt11Invoice, Error> {
let invoice_description =
lightning_invoice::Bolt11InvoiceDescription::try_from(description)?;
self.receive_via_jit_channel_inner(
Some(amount_msat),
&invoice_description,
expiry_secs,
max_total_lsp_fee_limit_msat,
None,
)
}

/// Returns a payable invoice that can be used to request a variable amount payment (also known
/// as "zero-amount" invoice) and receive it via a newly created just-in-time (JIT) channel.
///
Expand All @@ -554,8 +614,9 @@ impl Bolt11Payment {
/// We'll use its cheapest offer otherwise.
///
/// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md
#[cfg(not(feature = "uniffi"))]
pub fn receive_variable_amount_via_jit_channel(
&self, description: &str, expiry_secs: u32,
&self, description: &lightning_invoice::Bolt11InvoiceDescription, expiry_secs: u32,
max_proportional_lsp_fee_limit_ppm_msat: Option<u64>,
) -> Result<Bolt11Invoice, Error> {
self.receive_via_jit_channel_inner(
Expand All @@ -567,9 +628,25 @@ impl Bolt11Payment {
)
}

#[cfg(feature = "uniffi")]
pub fn receive_variable_amount_via_jit_channel(
&self, description: &Bolt11InvoiceDescription, expiry_secs: u32,
max_proportional_lsp_fee_limit_ppm_msat: Option<u64>,
) -> Result<Bolt11Invoice, Error> {
let invoice_description =
lightning_invoice::Bolt11InvoiceDescription::try_from(description)?;
self.receive_via_jit_channel_inner(
None,
&invoice_description,
expiry_secs,
None,
max_proportional_lsp_fee_limit_ppm_msat,
)
}

fn receive_via_jit_channel_inner(
&self, amount_msat: Option<u64>, description: &str, expiry_secs: u32,
max_total_lsp_fee_limit_msat: Option<u64>,
&self, amount_msat: Option<u64>, description: &lightning_invoice::Bolt11InvoiceDescription,
expiry_secs: u32, max_total_lsp_fee_limit_msat: Option<u64>,
max_proportional_lsp_fee_limit_ppm_msat: Option<u64>,
) -> Result<Bolt11Invoice, Error> {
let liquidity_source =
Expand Down Expand Up @@ -740,3 +817,49 @@ 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 Bolt11InvoiceDescription {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move this type definition to uniffi_types.rs directly.

/// Contains a full description.
Direct {
/// Description of what the invoice is for
description: String,
},
/// Contains a hash.
Hash {
/// Hash of the description of what the invoice is for
hash: String,
},
}

impl TryFrom<&Bolt11InvoiceDescription> for lightning_invoice::Bolt11InvoiceDescription {
type Error = Error;

fn try_from(value: &Bolt11InvoiceDescription) -> Result<Self, Self::Error> {
match value {
Bolt11InvoiceDescription::Direct { description } => {
Description::new(description.clone())
.map(lightning_invoice::Bolt11InvoiceDescription::Direct)
.map_err(|_| Error::InvoiceCreationFailed)
},
Bolt11InvoiceDescription::Hash { hash } => Sha256::from_str(&hash)
.map(lightning_invoice::Sha256)
.map(lightning_invoice::Bolt11InvoiceDescription::Hash)
.map_err(|_| Error::InvoiceCreationFailed),
}
}
}

impl From<lightning_invoice::Bolt11InvoiceDescription> for Bolt11InvoiceDescription {
fn from(value: lightning_invoice::Bolt11InvoiceDescription) -> Self {
match value {
lightning_invoice::Bolt11InvoiceDescription::Direct(description) => {
Bolt11InvoiceDescription::Direct { description: description.to_string() }
},
lightning_invoice::Bolt11InvoiceDescription::Hash(hash) => {
Bolt11InvoiceDescription::Hash { hash: hex_utils::to_string(hash.0.as_ref()) }
},
}
}
}
1 change: 1 addition & 0 deletions src/payment/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ mod spontaneous;
pub(crate) mod store;
mod unified_qr;

pub use bolt11::Bolt11InvoiceDescription;
pub use bolt11::Bolt11Payment;
pub use bolt12::Bolt12Payment;
pub use onchain::OnchainPayment;
Expand Down
Loading
Loading