Skip to content
Open
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
110 changes: 94 additions & 16 deletions ldk-server-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ use ldk_server_client::error::LdkServerErrorCode::{
AuthError, InternalError, InternalServerError, InvalidRequestError, LightningError,
};
use ldk_server_client::ldk_server_protos::api::{
Bolt11ReceiveRequest, Bolt11ReceiveResponse, Bolt11SendRequest, Bolt11SendResponse,
Bolt11ReceiveRequest, Bolt11ReceiveResponse, Bolt11ReceiveVariableAmountViaJitChannelRequest,
Bolt11ReceiveVariableAmountViaJitChannelResponse, Bolt11ReceiveViaJitChannelRequest,
Bolt11ReceiveViaJitChannelResponse, Bolt11SendRequest, Bolt11SendResponse,
Bolt12ReceiveRequest, Bolt12ReceiveResponse, Bolt12SendRequest, Bolt12SendResponse,
CloseChannelRequest, CloseChannelResponse, ConnectPeerRequest, ConnectPeerResponse,
DisconnectPeerRequest, DisconnectPeerResponse, ExportPathfindingScoresRequest,
Expand Down Expand Up @@ -133,6 +135,41 @@ enum Commands {
#[arg(short, long, help = "Invoice expiry time in seconds (default: 86400)")]
expiry_secs: Option<u32>,
},
#[command(about = "Create a fixed-amount BOLT11 invoice to receive via an LSPS2 JIT channel")]
Bolt11ReceiveViaJitChannel {
#[arg(help = "Amount to request, e.g. 50sat or 50000msat")]
amount: Amount,
#[arg(short, long, help = "Description to attach along with the invoice")]
Copy link
Collaborator

Choose a reason for hiding this comment

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

can we drop short, long here so you can just feed the description directly

description: Option<String>,
#[arg(
long,
help = "SHA-256 hash of the description (hex). Use instead of description for longer text"
)]
description_hash: Option<String>,
#[arg(short, long, help = "Invoice expiry time in seconds (default: 86400)")]
expiry_secs: Option<u32>,
#[arg(
long,
help = "Maximum total fee an LSP may deduct for opening the JIT channel, e.g. 50sat or 50000msat"
)]
max_total_lsp_fee_limit: Option<Amount>,
},
#[command(
about = "Create a variable-amount BOLT11 invoice to receive via an LSPS2 JIT channel"
)]
Bolt11ReceiveVariableAmountViaJitChannel {
#[arg(short, long, help = "Description to attach along with the invoice")]
description: Option<String>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

same here

#[arg(
long,
help = "SHA-256 hash of the description (hex). Use instead of description for longer text"
)]
description_hash: Option<String>,
#[arg(short, long, help = "Invoice expiry time in seconds (default: 86400)")]
expiry_secs: Option<u32>,
#[arg(long, help = "Maximum proportional fee the LSP may deduct in ppm-msat")]
max_proportional_lsp_fee_limit_ppm_msat: Option<u64>,
},
#[command(about = "Pay a BOLT11 invoice")]
Bolt11Send {
#[arg(help = "A BOLT11 invoice for a payment within the Lightning Network")]
Expand Down Expand Up @@ -510,21 +547,8 @@ async fn main() {
},
Commands::Bolt11Receive { description, description_hash, expiry_secs, amount } => {
let amount_msat = amount.map(|a| a.to_msat());
let invoice_description = match (description, description_hash) {
(Some(desc), None) => Some(Bolt11InvoiceDescription {
kind: Some(bolt11_invoice_description::Kind::Direct(desc)),
}),
(None, Some(hash)) => Some(Bolt11InvoiceDescription {
kind: Some(bolt11_invoice_description::Kind::Hash(hash)),
}),
(Some(_), Some(_)) => {
handle_error(LdkServerError::new(
InternalError,
"Only one of description or description_hash can be set.".to_string(),
));
},
(None, None) => None,
};
let invoice_description =
parse_bolt11_invoice_description(description, description_hash);

let expiry_secs = expiry_secs.unwrap_or(DEFAULT_EXPIRY_SECS);
let request =
Expand All @@ -534,6 +558,40 @@ async fn main() {
client.bolt11_receive(request).await,
);
},
Commands::Bolt11ReceiveViaJitChannel {
amount,
description,
description_hash,
expiry_secs,
max_total_lsp_fee_limit,
} => {
let request = Bolt11ReceiveViaJitChannelRequest {
amount_msat: amount.to_msat(),
description: parse_bolt11_invoice_description(description, description_hash),
expiry_secs: expiry_secs.unwrap_or(DEFAULT_EXPIRY_SECS),
max_total_lsp_fee_limit_msat: max_total_lsp_fee_limit.map(|a| a.to_msat()),
};

handle_response_result::<_, Bolt11ReceiveViaJitChannelResponse>(
client.bolt11_receive_via_jit_channel(request).await,
);
},
Commands::Bolt11ReceiveVariableAmountViaJitChannel {
description,
description_hash,
expiry_secs,
max_proportional_lsp_fee_limit_ppm_msat,
} => {
let request = Bolt11ReceiveVariableAmountViaJitChannelRequest {
description: parse_bolt11_invoice_description(description, description_hash),
expiry_secs: expiry_secs.unwrap_or(DEFAULT_EXPIRY_SECS),
max_proportional_lsp_fee_limit_ppm_msat,
};

handle_response_result::<_, Bolt11ReceiveVariableAmountViaJitChannelResponse>(
client.bolt11_receive_variable_amount_via_jit_channel(request).await,
);
},
Commands::Bolt11Send {
invoice,
amount,
Expand Down Expand Up @@ -928,6 +986,26 @@ where
}
}

fn parse_bolt11_invoice_description(
description: Option<String>, description_hash: Option<String>,
) -> Option<Bolt11InvoiceDescription> {
match (description, description_hash) {
(Some(desc), None) => Some(Bolt11InvoiceDescription {
kind: Some(bolt11_invoice_description::Kind::Direct(desc)),
}),
(None, Some(hash)) => Some(Bolt11InvoiceDescription {
kind: Some(bolt11_invoice_description::Kind::Hash(hash)),
}),
(Some(_), Some(_)) => {
handle_error(LdkServerError::new(
InternalError,
"Only one of description or description_hash can be set.".to_string(),
));
},
(None, None) => None,
}
}

fn parse_page_token(token_str: &str) -> Result<PageToken, LdkServerError> {
let parts: Vec<&str> = token_str.split(':').collect();
if parts.len() != 2 {
Expand Down
31 changes: 29 additions & 2 deletions ldk-server-client/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ use std::time::{SystemTime, UNIX_EPOCH};
use bitcoin_hashes::hmac::{Hmac, HmacEngine};
use bitcoin_hashes::{sha256, Hash, HashEngine};
use ldk_server_protos::api::{
Bolt11ReceiveRequest, Bolt11ReceiveResponse, Bolt11SendRequest, Bolt11SendResponse,
Bolt11ReceiveRequest, Bolt11ReceiveResponse, Bolt11ReceiveVariableAmountViaJitChannelRequest,
Bolt11ReceiveVariableAmountViaJitChannelResponse, Bolt11ReceiveViaJitChannelRequest,
Bolt11ReceiveViaJitChannelResponse, Bolt11SendRequest, Bolt11SendResponse,
Bolt12ReceiveRequest, Bolt12ReceiveResponse, Bolt12SendRequest, Bolt12SendResponse,
CloseChannelRequest, CloseChannelResponse, ConnectPeerRequest, ConnectPeerResponse,
DisconnectPeerRequest, DisconnectPeerResponse, ExportPathfindingScoresRequest,
Expand All @@ -29,7 +31,8 @@ use ldk_server_protos::api::{
UpdateChannelConfigResponse, VerifySignatureRequest, VerifySignatureResponse,
};
use ldk_server_protos::endpoints::{
BOLT11_RECEIVE_PATH, BOLT11_SEND_PATH, BOLT12_RECEIVE_PATH, BOLT12_SEND_PATH,
BOLT11_RECEIVE_PATH, BOLT11_RECEIVE_VARIABLE_AMOUNT_VIA_JIT_CHANNEL_PATH,
BOLT11_RECEIVE_VIA_JIT_CHANNEL_PATH, BOLT11_SEND_PATH, BOLT12_RECEIVE_PATH, BOLT12_SEND_PATH,
CLOSE_CHANNEL_PATH, CONNECT_PEER_PATH, DISCONNECT_PEER_PATH, EXPORT_PATHFINDING_SCORES_PATH,
FORCE_CLOSE_CHANNEL_PATH, GET_BALANCES_PATH, GET_NODE_INFO_PATH, GET_PAYMENT_DETAILS_PATH,
GRAPH_GET_CHANNEL_PATH, GRAPH_GET_NODE_PATH, GRAPH_LIST_CHANNELS_PATH, GRAPH_LIST_NODES_PATH,
Expand Down Expand Up @@ -142,6 +145,30 @@ impl LdkServerClient {
self.post_request(&request, &url).await
}

/// Retrieve a new fixed-amount BOLT11 invoice for receiving via an LSPS2 JIT channel.
/// For API contract/usage, refer to docs for [`Bolt11ReceiveViaJitChannelRequest`] and
/// [`Bolt11ReceiveViaJitChannelResponse`].
pub async fn bolt11_receive_via_jit_channel(
&self, request: Bolt11ReceiveViaJitChannelRequest,
) -> Result<Bolt11ReceiveViaJitChannelResponse, LdkServerError> {
let url = format!("https://{}/{BOLT11_RECEIVE_VIA_JIT_CHANNEL_PATH}", self.base_url);
self.post_request(&request, &url).await
}

/// Retrieve a new variable-amount BOLT11 invoice for receiving via an LSPS2 JIT channel.
/// For API contract/usage, refer to docs for
/// [`Bolt11ReceiveVariableAmountViaJitChannelRequest`] and
/// [`Bolt11ReceiveVariableAmountViaJitChannelResponse`].
pub async fn bolt11_receive_variable_amount_via_jit_channel(
&self, request: Bolt11ReceiveVariableAmountViaJitChannelRequest,
) -> Result<Bolt11ReceiveVariableAmountViaJitChannelResponse, LdkServerError> {
let url = format!(
"https://{}/{BOLT11_RECEIVE_VARIABLE_AMOUNT_VIA_JIT_CHANNEL_PATH}",
self.base_url,
);
self.post_request(&request, &url).await
}

/// Send a payment for a BOLT11 invoice.
/// For API contract/usage, refer to docs for [`Bolt11SendRequest`] and [`Bolt11SendResponse`].
pub async fn bolt11_send(
Expand Down
64 changes: 64 additions & 0 deletions ldk-server-protos/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,70 @@ pub struct Bolt11ReceiveResponse {
#[prost(string, tag = "1")]
pub invoice: ::prost::alloc::string::String,
}
/// Return a BOLT11 payable invoice that can be used to request and receive a payment via an
/// LSPS2 just-in-time channel.
/// See more: <https://docs.rs/ldk-node/latest/ldk_node/payment/struct.Bolt11Payment.html#method.receive_via_jit_channel>
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Bolt11ReceiveViaJitChannelRequest {
/// The amount in millisatoshi to request.
#[prost(uint64, tag = "1")]
pub amount_msat: u64,
/// An optional description to attach along with the invoice.
/// Will be set in the description field of the encoded payment request.
#[prost(message, optional, tag = "2")]
pub description: ::core::option::Option<super::types::Bolt11InvoiceDescription>,
/// Invoice expiry time in seconds.
#[prost(uint32, tag = "3")]
pub expiry_secs: u32,
/// Optional upper bound for the total fee an LSP may deduct when opening the JIT channel.
#[prost(uint64, optional, tag = "4")]
pub max_total_lsp_fee_limit_msat: ::core::option::Option<u64>,
}
/// The response `content` for the `Bolt11ReceiveViaJitChannel` API, when HttpStatusCode is OK (200).
/// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Bolt11ReceiveViaJitChannelResponse {
/// An invoice for a payment within the Lightning Network.
#[prost(string, tag = "1")]
pub invoice: ::prost::alloc::string::String,
}
/// Return a variable-amount BOLT11 invoice that can be used to receive a payment via an LSPS2
/// just-in-time channel.
/// See more: <https://docs.rs/ldk-node/latest/ldk_node/payment/struct.Bolt11Payment.html#method.receive_variable_amount_via_jit_channel>
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Bolt11ReceiveVariableAmountViaJitChannelRequest {
/// An optional description to attach along with the invoice.
/// Will be set in the description field of the encoded payment request.
#[prost(message, optional, tag = "1")]
pub description: ::core::option::Option<super::types::Bolt11InvoiceDescription>,
/// Invoice expiry time in seconds.
#[prost(uint32, tag = "2")]
pub expiry_secs: u32,
/// Optional upper bound for the proportional fee, in parts-per-million millisatoshis, that an
/// LSP may deduct when opening the JIT channel.
#[prost(uint64, optional, tag = "3")]
pub max_proportional_lsp_fee_limit_ppm_msat: ::core::option::Option<u64>,
}
/// The response `content` for the `Bolt11ReceiveVariableAmountViaJitChannel` API, when HttpStatusCode is OK (200).
/// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Bolt11ReceiveVariableAmountViaJitChannelResponse {
/// An invoice for a payment within the Lightning Network.
#[prost(string, tag = "1")]
pub invoice: ::prost::alloc::string::String,
}
/// Send a payment for a BOLT11 invoice.
/// See more: <https://docs.rs/ldk-node/latest/ldk_node/payment/struct.Bolt11Payment.html#method.send>
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
Expand Down
3 changes: 3 additions & 0 deletions ldk-server-protos/src/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ pub const GET_BALANCES_PATH: &str = "GetBalances";
pub const ONCHAIN_RECEIVE_PATH: &str = "OnchainReceive";
pub const ONCHAIN_SEND_PATH: &str = "OnchainSend";
pub const BOLT11_RECEIVE_PATH: &str = "Bolt11Receive";
pub const BOLT11_RECEIVE_VIA_JIT_CHANNEL_PATH: &str = "Bolt11ReceiveViaJitChannel";
pub const BOLT11_RECEIVE_VARIABLE_AMOUNT_VIA_JIT_CHANNEL_PATH: &str =
"Bolt11ReceiveVariableAmountViaJitChannel";
pub const BOLT11_SEND_PATH: &str = "Bolt11Send";
pub const BOLT12_RECEIVE_PATH: &str = "Bolt12Receive";
pub const BOLT12_SEND_PATH: &str = "Bolt12Send";
Expand Down
52 changes: 52 additions & 0 deletions ldk-server-protos/src/proto/api.proto
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,58 @@ message Bolt11ReceiveResponse {
string invoice = 1;
}

// Return a BOLT11 payable invoice that can be used to request and receive a payment via an
// LSPS2 just-in-time channel.
// See more: https://docs.rs/ldk-node/latest/ldk_node/payment/struct.Bolt11Payment.html#method.receive_via_jit_channel
message Bolt11ReceiveViaJitChannelRequest {

// The amount in millisatoshi to request.
uint64 amount_msat = 1;

// An optional description to attach along with the invoice.
// Will be set in the description field of the encoded payment request.
types.Bolt11InvoiceDescription description = 2;

// Invoice expiry time in seconds.
uint32 expiry_secs = 3;

// Optional upper bound for the total fee an LSP may deduct when opening the JIT channel.
optional uint64 max_total_lsp_fee_limit_msat = 4;
}

// The response `content` for the `Bolt11ReceiveViaJitChannel` API, when HttpStatusCode is OK (200).
// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`.
message Bolt11ReceiveViaJitChannelResponse {

// An invoice for a payment within the Lightning Network.
string invoice = 1;
}

// Return a variable-amount BOLT11 invoice that can be used to receive a payment via an LSPS2
// just-in-time channel.
// See more: https://docs.rs/ldk-node/latest/ldk_node/payment/struct.Bolt11Payment.html#method.receive_variable_amount_via_jit_channel
message Bolt11ReceiveVariableAmountViaJitChannelRequest {

// An optional description to attach along with the invoice.
// Will be set in the description field of the encoded payment request.
types.Bolt11InvoiceDescription description = 1;

// Invoice expiry time in seconds.
uint32 expiry_secs = 2;

// Optional upper bound for the proportional fee, in parts-per-million millisatoshis, that an
// LSP may deduct when opening the JIT channel.
optional uint64 max_proportional_lsp_fee_limit_ppm_msat = 3;
}

// The response `content` for the `Bolt11ReceiveVariableAmountViaJitChannel` API, when HttpStatusCode is OK (200).
// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`.
message Bolt11ReceiveVariableAmountViaJitChannelResponse {

// An invoice for a payment within the Lightning Network.
string invoice = 1;
}

// Send a payment for a BOLT11 invoice.
// See more: https://docs.rs/ldk-node/latest/ldk_node/payment/struct.Bolt11Payment.html#method.send
message Bolt11SendRequest {
Expand Down
9 changes: 9 additions & 0 deletions ldk-server/ldk-server-config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ server_url = "https://mempool.space/api" # Esplora endpoint
connection_string = "" # RabbitMQ connection string
exchange_name = ""

# LSPS2 Client Support
[liquidity.lsps2_client]
# The public key of the LSPS2 LSP we source just-in-time liquidity from.
node_pubkey = "<lsp node pubkey>"
# Address to connect to the LSPS2 LSP (IPv4:port, IPv6:port, OnionV3:port, or hostname:port).
address = "127.0.0.1:9735"
Copy link
Collaborator

Choose a reason for hiding this comment

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

can we make this not a local host ip, that might confused people thinking its something they are supposed to run

# Optional token for authenticating to the LSP.
# token = ""

# Experimental LSPS2 Service Support
# CAUTION: LSPS2 support is highly experimental and for testing purposes only.
[liquidity.lsps2_service]
Expand Down
45 changes: 45 additions & 0 deletions ldk-server/src/api/bolt11_receive_via_jit_channel.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// This file is Copyright its original authors, visible in version control
// history.
//
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
// You may not use this file except in accordance with one or both of these
// licenses.

use ldk_server_protos::api::{
Bolt11ReceiveVariableAmountViaJitChannelRequest,
Bolt11ReceiveVariableAmountViaJitChannelResponse, Bolt11ReceiveViaJitChannelRequest,
Bolt11ReceiveViaJitChannelResponse,
};

use crate::api::error::LdkServerError;
use crate::service::Context;
use crate::util::proto_adapter::proto_to_bolt11_description;

pub(crate) fn handle_bolt11_receive_via_jit_channel_request(
context: Context, request: Bolt11ReceiveViaJitChannelRequest,
) -> Result<Bolt11ReceiveViaJitChannelResponse, LdkServerError> {
let description = proto_to_bolt11_description(request.description)?;
let invoice = context.node.bolt11_payment().receive_via_jit_channel(
request.amount_msat,
&description,
request.expiry_secs,
request.max_total_lsp_fee_limit_msat,
)?;

Ok(Bolt11ReceiveViaJitChannelResponse { invoice: invoice.to_string() })
}

pub(crate) fn handle_bolt11_receive_variable_amount_via_jit_channel_request(
context: Context, request: Bolt11ReceiveVariableAmountViaJitChannelRequest,
) -> Result<Bolt11ReceiveVariableAmountViaJitChannelResponse, LdkServerError> {
let description = proto_to_bolt11_description(request.description)?;
let invoice = context.node.bolt11_payment().receive_variable_amount_via_jit_channel(
&description,
request.expiry_secs,
request.max_proportional_lsp_fee_limit_ppm_msat,
)?;

Ok(Bolt11ReceiveVariableAmountViaJitChannelResponse { invoice: invoice.to_string() })
}
Loading