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 all commits
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
56 changes: 39 additions & 17 deletions src/payment/bolt11.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,32 @@ use lightning::routing::router::{PaymentParameters, RouteParameters};

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

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

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

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

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

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

/// A payment handler allowing to create and pay [BOLT 11] invoices.
///
/// Should be retrieved by calling [`Node::bolt11_payment`].
Expand Down Expand Up @@ -404,8 +423,9 @@ impl Bolt11Payment {
///
/// The inbound payment will be automatically claimed upon arrival.
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: &str, expiry_secs: u32,
&self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32,
) -> Result<Bolt11Invoice, Error> {
let description = maybe_convert_description!(description);
self.receive_inner(Some(amount_msat), description, expiry_secs, None)
}

Expand All @@ -424,8 +444,10 @@ impl Bolt11Payment {
/// [`claim_for_hash`]: Self::claim_for_hash
/// [`fail_for_hash`]: Self::fail_for_hash
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> {
let description = maybe_convert_description!(description);
self.receive_inner(Some(amount_msat), description, expiry_secs, Some(payment_hash))
}

Expand All @@ -434,8 +456,9 @@ impl Bolt11Payment {
///
/// The inbound payment will be automatically claimed upon arrival.
pub fn receive_variable_amount(
&self, description: &str, expiry_secs: u32,
&self, description: &Bolt11InvoiceDescription, expiry_secs: u32,
) -> Result<Bolt11Invoice, Error> {
let description = maybe_convert_description!(description);
self.receive_inner(None, description, expiry_secs, None)
}

Expand All @@ -454,23 +477,20 @@ impl Bolt11Payment {
/// [`claim_for_hash`]: Self::claim_for_hash
/// [`fail_for_hash`]: Self::fail_for_hash
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> {
let description = maybe_convert_description!(description);
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>,
pub(crate) fn receive_inner(
&self, amount_msat: Option<u64>, invoice_description: &LdkBolt11InvoiceDescription,
expiry_secs: u32, manual_claim_payment_hash: Option<PaymentHash>,
) -> Result<Bolt11Invoice, Error> {
let invoice_description = Bolt11InvoiceDescription::Direct(
Description::new(description.to_string()).map_err(|_| Error::InvoiceCreationFailed)?,
);

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 @@ -531,9 +551,10 @@ impl Bolt11Payment {
///
/// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md
pub fn receive_via_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, Error> {
let description = maybe_convert_description!(description);
self.receive_via_jit_channel_inner(
Some(amount_msat),
description,
Expand All @@ -555,9 +576,10 @@ impl Bolt11Payment {
///
/// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md
pub fn receive_variable_amount_via_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, Error> {
let description = maybe_convert_description!(description);
self.receive_via_jit_channel_inner(
None,
description,
Expand All @@ -568,8 +590,8 @@ impl Bolt11Payment {
}

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: &LdkBolt11InvoiceDescription,
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
25 changes: 16 additions & 9 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,14 +99,21 @@ impl UnifiedQrPayment {
},
};

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 invoice_description = Bolt11InvoiceDescription::Direct(
Description::new(description.to_string()).map_err(|_| Error::InvoiceCreationFailed)?,
);
let bolt11_invoice = 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);
return Err(Error::InvoiceCreationFailed);
},
};

let extras = Extras { bolt11_invoice, bolt12_offer };

Expand Down
48 changes: 47 additions & 1 deletion src/uniffi_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ pub use lightning::util::string::UntrustedString;

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

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

pub use bitcoin::{Address, BlockHash, Network, OutPoint, Txid};

Expand Down Expand Up @@ -345,3 +345,49 @@ impl UniffiCustomTypeConverter for NodeAlias {
obj.to_string()
}
}

/// 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 {
/// 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()) }
},
}
}
}
Loading
Loading