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
15 changes: 11 additions & 4 deletions docs/api-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,17 @@ All RPCs are unary (single request, single response) unless noted otherwise.

### BOLT11 Payments

| RPC | Description |
|-----------------|-------------------------------------------------------------------|
| `Bolt11Receive` | Create an invoice (fixed or variable amount) with automatic claim |
| `Bolt11Send` | Pay a BOLT11 invoice (with optional routing config) |
| RPC | Description |
|-------------------------|-------------------------------------------------------------------|
| `Bolt11Receive` | Create an invoice (fixed or variable amount) with automatic claim |
| `Bolt11Send` | Pay a BOLT11 invoice (with optional routing config) |
| `Bolt11SendUnderpaying` | Send part of the amount for a BOLT11 invoice |

> [!NOTE]
> `Bolt11SendUnderpaying` sends one part of a multi-part payment (MPP) for a BOLT11
> invoice. Other nodes must send compatible partial payments for the same invoice until
> the combined amount equals the invoice amount. Without those payments, the receiver
> holds the incomplete MPP payment and eventually fails it.

### BOLT11 Hodl Invoices

Expand Down
52 changes: 52 additions & 0 deletions e2e-tests/tests/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,58 @@ async fn test_cli_bolt11_send() {
assert!(matches!(&event_b.event, Some(Event::PaymentReceived(_))));
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_cli_bolt11_send_underpaying_split_payment() {
let bitcoind = TestBitcoind::new();
let server_a = LdkServerHandle::start(&bitcoind).await;
let server_b = LdkServerHandle::start(&bitcoind).await;
let server_c = LdkServerHandle::start(&bitcoind).await;

// Subscribe to events on all three nodes before any payment is sent.
let mut events_a = server_a.client().subscribe_events().await.unwrap();
let mut events_b = server_b.client().subscribe_events().await.unwrap();
let mut events_c = server_c.client().subscribe_events().await.unwrap();

// Each payer gets its own direct channel into the receiver. The channels are sized well
// above the 50,000 sat HTLCs because LDK limits a channel's maximum HTLC size to a fraction
// of its capacity.
setup_funded_channel(&bitcoind, &server_a, &server_c, 300_000).await;
setup_funded_channel(&bitcoind, &server_b, &server_c, 300_000).await;

// Create one invoice for the full amount that the two payers will jointly cover.
let invoice_resp = server_c
.client()
.bolt11_receive(Bolt11ReceiveRequest {
amount_msat: Some(100_000_000),
description: Some(Bolt11InvoiceDescription {
kind: Some(bolt11_invoice_description::Kind::Direct(
"split payment test".to_string(),
)),
}),
expiry_secs: 3600,
})
.await
.unwrap();

// Both payers independently send half of the invoice amount.
let output_a =
run_cli(&server_a, &["bolt11-send-underpaying", &invoice_resp.invoice, "50000sat"]);
let output_b =
run_cli(&server_b, &["bolt11-send-underpaying", &invoice_resp.invoice, "50000sat"]);
assert!(!output_a["payment_id"].as_str().unwrap().is_empty());
assert!(!output_b["payment_id"].as_str().unwrap().is_empty());

// The receiver completes the payment only after both partial HTLCs arrive.
let event_c = wait_for_event(&mut events_c, |e| matches!(e, Event::PaymentReceived(_))).await;
assert!(matches!(&event_c.event, Some(Event::PaymentReceived(_))));

// Both payers complete their part of the payment successfully.
let event_a = wait_for_event(&mut events_a, |e| matches!(e, Event::PaymentSuccessful(_))).await;
assert!(matches!(&event_a.event, Some(Event::PaymentSuccessful(_))));
let event_b = wait_for_event(&mut events_b, |e| matches!(e, Event::PaymentSuccessful(_))).await;
assert!(matches!(&event_b.event, Some(Event::PaymentSuccessful(_))));
}

#[tokio::test]
async fn test_cli_pay() {
let bitcoind = TestBitcoind::new();
Expand Down
69 changes: 63 additions & 6 deletions ldk-server-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@ use ldk_server_client::ldk_server_grpc::api::{
Bolt11ReceiveRequest, Bolt11ReceiveResponse, Bolt11ReceiveVariableAmountViaJitChannelRequest,
Bolt11ReceiveVariableAmountViaJitChannelResponse, Bolt11ReceiveViaJitChannelRequest,
Bolt11ReceiveViaJitChannelResponse, Bolt11SendRequest, Bolt11SendResponse,
Bolt12ReceiveRequest, Bolt12ReceiveResponse, Bolt12SendRequest, Bolt12SendResponse,
CloseChannelRequest, CloseChannelResponse, ConnectPeerRequest, ConnectPeerResponse,
DecodeInvoiceRequest, DecodeInvoiceResponse, DecodeOfferRequest, DecodeOfferResponse,
DisconnectPeerRequest, DisconnectPeerResponse, ExportPathfindingScoresRequest,
ForceCloseChannelRequest, ForceCloseChannelResponse, GetBalancesRequest, GetBalancesResponse,
GetNodeInfoRequest, GetNodeInfoResponse, GetPaymentDetailsRequest, GetPaymentDetailsResponse,
Bolt11SendUnderpayingRequest, Bolt11SendUnderpayingResponse, Bolt12ReceiveRequest,
Bolt12ReceiveResponse, Bolt12SendRequest, Bolt12SendResponse, CloseChannelRequest,
CloseChannelResponse, ConnectPeerRequest, ConnectPeerResponse, DecodeInvoiceRequest,
DecodeInvoiceResponse, DecodeOfferRequest, DecodeOfferResponse, DisconnectPeerRequest,
DisconnectPeerResponse, ExportPathfindingScoresRequest, ForceCloseChannelRequest,
ForceCloseChannelResponse, GetBalancesRequest, GetBalancesResponse, GetNodeInfoRequest,
GetNodeInfoResponse, GetPaymentDetailsRequest, GetPaymentDetailsResponse,
GraphGetChannelRequest, GraphGetChannelResponse, GraphGetNodeRequest, GraphGetNodeResponse,
GraphListChannelsRequest, GraphListChannelsResponse, GraphListNodesRequest,
GraphListNodesResponse, ListChannelsRequest, ListChannelsResponse,
Expand Down Expand Up @@ -246,6 +247,34 @@ enum Commands {
)]
max_channel_saturation_power_of_half: Option<u32>,
},
#[command(
about = "Send part of a BOLT11 invoice. Other nodes must send partial payments for the same invoice until the combined amount equals the invoice amount"
)]
Bolt11SendUnderpaying {
#[arg(help = "A BOLT11 invoice for a payment within the Lightning Network")]
invoice: String,
#[arg(
help = "Amount from this payer, for example 50sat or 50000msat. Must be less than the invoice amount"
)]
amount: Amount,
#[arg(
long,
help = "Maximum total routing fee, e.g. 50sat or 50000msat. Defaults to 1% of payment + 50 sats"
)]
max_total_routing_fee: Option<Amount>,
#[arg(long, help = "Maximum total CLTV delta we accept for the route (default: 1008)")]
max_total_cltv_expiry_delta: Option<u32>,
#[arg(
long,
help = "Maximum number of paths that may be used by MPP payments (default: 10)"
)]
max_path_count: Option<u32>,
#[arg(
long,
help = "Maximum share of a channel's total capacity to send over a channel, as a power of 1/2 (default: 2)"
)]
max_channel_saturation_power_of_half: Option<u32>,
},
#[command(about = "Return a BOLT12 offer for receiving payments")]
Bolt12Receive {
#[arg(help = "Description to attach along with the offer")]
Expand Down Expand Up @@ -765,6 +794,34 @@ async fn main() {
.await,
);
},
Commands::Bolt11SendUnderpaying {
invoice,
amount,
max_total_routing_fee,
max_total_cltv_expiry_delta,
max_path_count,
max_channel_saturation_power_of_half,
} => {
let amount_msat = amount.to_msat();
let max_total_routing_fee_msat = max_total_routing_fee.map(|a| a.to_msat());
let route_parameters = RouteParametersConfig {
max_total_routing_fee_msat,
max_total_cltv_expiry_delta: max_total_cltv_expiry_delta
.unwrap_or(DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA),
max_path_count: max_path_count.unwrap_or(DEFAULT_MAX_PATH_COUNT),
max_channel_saturation_power_of_half: max_channel_saturation_power_of_half
.unwrap_or(DEFAULT_MAX_CHANNEL_SATURATION_POWER_OF_HALF),
};
handle_response_result::<_, Bolt11SendUnderpayingResponse>(
client
.bolt11_send_underpaying(Bolt11SendUnderpayingRequest {
invoice,
amount_msat,
route_parameters: Some(route_parameters),
})
.await,
);
},
Commands::Bolt12Receive { description, amount, expiry_secs, quantity } => {
let amount_msat = amount.map(|a| a.to_msat());
handle_response_result::<_, Bolt12ReceiveResponse>(
Expand Down
59 changes: 36 additions & 23 deletions ldk-server-client/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,36 +21,38 @@ use ldk_server_grpc::api::{
Bolt11ReceiveRequest, Bolt11ReceiveResponse, Bolt11ReceiveVariableAmountViaJitChannelRequest,
Bolt11ReceiveVariableAmountViaJitChannelResponse, Bolt11ReceiveViaJitChannelRequest,
Bolt11ReceiveViaJitChannelResponse, Bolt11SendRequest, Bolt11SendResponse,
Bolt12ReceiveRequest, Bolt12ReceiveResponse, Bolt12SendRequest, Bolt12SendResponse,
CloseChannelRequest, CloseChannelResponse, ConnectPeerRequest, ConnectPeerResponse,
DecodeInvoiceRequest, DecodeInvoiceResponse, DecodeOfferRequest, DecodeOfferResponse,
DisconnectPeerRequest, DisconnectPeerResponse, ExportPathfindingScoresRequest,
ExportPathfindingScoresResponse, ForceCloseChannelRequest, ForceCloseChannelResponse,
GetBalancesRequest, GetBalancesResponse, GetNodeInfoRequest, GetNodeInfoResponse,
GetPaymentDetailsRequest, GetPaymentDetailsResponse, GraphGetChannelRequest,
GraphGetChannelResponse, GraphGetNodeRequest, GraphGetNodeResponse, GraphListChannelsRequest,
GraphListChannelsResponse, GraphListNodesRequest, GraphListNodesResponse, ListChannelsRequest,
ListChannelsResponse, ListForwardedPaymentsRequest, ListForwardedPaymentsResponse,
ListPaymentsRequest, ListPaymentsResponse, ListPeersRequest, ListPeersResponse,
OnchainReceiveRequest, OnchainReceiveResponse, OnchainSendRequest, OnchainSendResponse,
OpenChannelRequest, OpenChannelResponse, SignMessageRequest, SignMessageResponse,
SpliceInRequest, SpliceInResponse, SpliceOutRequest, SpliceOutResponse, SpontaneousSendRequest,
Bolt11SendUnderpayingRequest, Bolt11SendUnderpayingResponse, Bolt12ReceiveRequest,
Bolt12ReceiveResponse, Bolt12SendRequest, Bolt12SendResponse, CloseChannelRequest,
CloseChannelResponse, ConnectPeerRequest, ConnectPeerResponse, DecodeInvoiceRequest,
DecodeInvoiceResponse, DecodeOfferRequest, DecodeOfferResponse, DisconnectPeerRequest,
DisconnectPeerResponse, ExportPathfindingScoresRequest, ExportPathfindingScoresResponse,
ForceCloseChannelRequest, ForceCloseChannelResponse, GetBalancesRequest, GetBalancesResponse,
GetNodeInfoRequest, GetNodeInfoResponse, GetPaymentDetailsRequest, GetPaymentDetailsResponse,
GraphGetChannelRequest, GraphGetChannelResponse, GraphGetNodeRequest, GraphGetNodeResponse,
GraphListChannelsRequest, GraphListChannelsResponse, GraphListNodesRequest,
GraphListNodesResponse, ListChannelsRequest, ListChannelsResponse,
ListForwardedPaymentsRequest, ListForwardedPaymentsResponse, ListPaymentsRequest,
ListPaymentsResponse, ListPeersRequest, ListPeersResponse, OnchainReceiveRequest,
OnchainReceiveResponse, OnchainSendRequest, OnchainSendResponse, OpenChannelRequest,
OpenChannelResponse, SignMessageRequest, SignMessageResponse, SpliceInRequest,
SpliceInResponse, SpliceOutRequest, SpliceOutResponse, SpontaneousSendRequest,
SpontaneousSendResponse, SubscribeEventsRequest, UnifiedSendRequest, UnifiedSendResponse,
UpdateChannelConfigRequest, UpdateChannelConfigResponse, VerifySignatureRequest,
VerifySignatureResponse,
};
use ldk_server_grpc::endpoints::{
BOLT11_CLAIM_FOR_HASH_PATH, BOLT11_FAIL_FOR_HASH_PATH, BOLT11_RECEIVE_FOR_HASH_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, DECODE_INVOICE_PATH, DECODE_OFFER_PATH,
DISCONNECT_PEER_PATH, EXPORT_PATHFINDING_SCORES_PATH, FORCE_CLOSE_CHANNEL_PATH,
GET_BALANCES_PATH, GET_METRICS_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,
GRPC_SERVICE_PREFIX, LIST_CHANNELS_PATH, LIST_FORWARDED_PAYMENTS_PATH, LIST_PAYMENTS_PATH,
LIST_PEERS_PATH, ONCHAIN_RECEIVE_PATH, ONCHAIN_SEND_PATH, OPEN_CHANNEL_PATH, SIGN_MESSAGE_PATH,
SPLICE_IN_PATH, SPLICE_OUT_PATH, SPONTANEOUS_SEND_PATH, SUBSCRIBE_EVENTS_PATH,
UNIFIED_SEND_PATH, UPDATE_CHANNEL_CONFIG_PATH, VERIFY_SIGNATURE_PATH,
BOLT11_RECEIVE_VIA_JIT_CHANNEL_PATH, BOLT11_SEND_PATH, BOLT11_SEND_UNDERPAYING_PATH,
BOLT12_RECEIVE_PATH, BOLT12_SEND_PATH, CLOSE_CHANNEL_PATH, CONNECT_PEER_PATH,
DECODE_INVOICE_PATH, DECODE_OFFER_PATH, DISCONNECT_PEER_PATH, EXPORT_PATHFINDING_SCORES_PATH,
FORCE_CLOSE_CHANNEL_PATH, GET_BALANCES_PATH, GET_METRICS_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, GRPC_SERVICE_PREFIX, LIST_CHANNELS_PATH,
LIST_FORWARDED_PAYMENTS_PATH, LIST_PAYMENTS_PATH, LIST_PEERS_PATH, ONCHAIN_RECEIVE_PATH,
ONCHAIN_SEND_PATH, OPEN_CHANNEL_PATH, SIGN_MESSAGE_PATH, SPLICE_IN_PATH, SPLICE_OUT_PATH,
SPONTANEOUS_SEND_PATH, SUBSCRIBE_EVENTS_PATH, UNIFIED_SEND_PATH, UPDATE_CHANNEL_CONFIG_PATH,
VERIFY_SIGNATURE_PATH,
};
use ldk_server_grpc::events::EventEnvelope;
use ldk_server_grpc::grpc::{
Expand Down Expand Up @@ -242,6 +244,17 @@ impl LdkServerClient {
self.grpc_unary(&request, BOLT11_SEND_PATH).await
}

/// Send part of the amount for a BOLT11 invoice.
///
/// Other nodes must send partial payments for the same invoice until the combined amount equals
/// the invoice amount. Without those payments, the receiver holds the incomplete MPP payment
/// and eventually fails it.
pub async fn bolt11_send_underpaying(
&self, request: Bolt11SendUnderpayingRequest,
) -> Result<Bolt11SendUnderpayingResponse, LdkServerError> {
self.grpc_unary(&request, BOLT11_SEND_UNDERPAYING_PATH).await
}

/// Retrieve a new BOLT12 offer.
pub async fn bolt12_receive(
&self, request: Bolt12ReceiveRequest,
Expand Down
31 changes: 31 additions & 0 deletions ldk-server-grpc/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,37 @@ pub struct Bolt11SendResponse {
#[prost(string, tag = "1")]
pub payment_id: ::prost::alloc::string::String,
}
/// Send part of the amount for a BOLT11 invoice.
/// Other nodes must send partial payments for the same invoice until the combined amount equals the invoice amount.
/// Without those payments, the receiver holds the incomplete MPP payment and eventually fails it.
/// See more: <https://docs.rs/ldk-node/latest/ldk_node/payment/struct.Bolt11Payment.html#method.send_using_amount_underpaying>
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[cfg_attr(feature = "serde", serde(default))]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Bolt11SendUnderpayingRequest {
/// An invoice for a payment within the Lightning Network.
#[prost(string, tag = "1")]
pub invoice: ::prost::alloc::string::String,
/// Amount in millisatoshis from this payer. Must be less than the amount required by the invoice.
#[prost(uint64, tag = "2")]
pub amount_msat: u64,
/// Configuration options for payment routing and pathfinding.
#[prost(message, optional, tag = "3")]
pub route_parameters: ::core::option::Option<super::types::RouteParametersConfig>,
}
/// The response for the `Bolt11SendUnderpaying` RPC. On failure, a gRPC error status is returned.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[cfg_attr(feature = "serde", serde(default))]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Bolt11SendUnderpayingResponse {
/// An identifier used to uniquely identify a payment in hex-encoded form.
#[prost(string, tag = "1")]
pub payment_id: ::prost::alloc::string::String,
}
/// Returns a BOLT12 offer for the given amount, if specified.
///
/// See more:
Expand Down
1 change: 1 addition & 0 deletions ldk-server-grpc/src/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub const BOLT11_RECEIVE_VIA_JIT_CHANNEL_PATH: &str = "Bolt11ReceiveViaJitChanne
pub const BOLT11_RECEIVE_VARIABLE_AMOUNT_VIA_JIT_CHANNEL_PATH: &str =
"Bolt11ReceiveVariableAmountViaJitChannel";
pub const BOLT11_SEND_PATH: &str = "Bolt11Send";
pub const BOLT11_SEND_UNDERPAYING_PATH: &str = "Bolt11SendUnderpaying";
pub const BOLT12_RECEIVE_PATH: &str = "Bolt12Receive";
pub const BOLT12_SEND_PATH: &str = "Bolt12Send";
pub const OPEN_CHANNEL_PATH: &str = "OpenChannel";
Expand Down
27 changes: 27 additions & 0 deletions ldk-server-grpc/src/proto/api.proto
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,30 @@ message Bolt11SendResponse {
string payment_id = 1;
}

// Send part of the amount for a BOLT11 invoice.
// Other nodes must send partial payments for the same invoice until the combined amount equals the invoice amount.
// Without those payments, the receiver holds the incomplete MPP payment and eventually fails it.
// See more: https://docs.rs/ldk-node/latest/ldk_node/payment/struct.Bolt11Payment.html#method.send_using_amount_underpaying
message Bolt11SendUnderpayingRequest {

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

// Amount in millisatoshis from this payer. Must be less than the amount required by the invoice.
uint64 amount_msat = 2;

// Configuration options for payment routing and pathfinding.
optional types.RouteParametersConfig route_parameters = 3;

}

// The response for the `Bolt11SendUnderpaying` RPC. On failure, a gRPC error status is returned.
message Bolt11SendUnderpayingResponse {

// An identifier used to uniquely identify a payment in hex-encoded form.
string payment_id = 1;
}

// Returns a BOLT12 offer for the given amount, if specified.
//
// See more:
Expand Down Expand Up @@ -928,6 +952,9 @@ service LightningNode {
rpc Bolt11ReceiveVariableAmountViaJitChannel(Bolt11ReceiveVariableAmountViaJitChannelRequest) returns (Bolt11ReceiveVariableAmountViaJitChannelResponse);
// Send a payment for a BOLT11 invoice.
rpc Bolt11Send(Bolt11SendRequest) returns (Bolt11SendResponse);
// Send part of the amount for a BOLT11 invoice.
// Other nodes must send partial payments for the same invoice until the combined amount equals the invoice amount.
rpc Bolt11SendUnderpaying(Bolt11SendUnderpayingRequest) returns (Bolt11SendUnderpayingResponse);
// Return a BOLT12 offer.
rpc Bolt12Receive(Bolt12ReceiveRequest) returns (Bolt12ReceiveResponse);
// Send a payment for a BOLT12 offer.
Expand Down
Loading