Skip to content

Commit f09a262

Browse files
Merge pull request #161 from benthecarman/unified
Add unified payment send support
2 parents 7f28543 + bbdd922 commit f09a262

10 files changed

Lines changed: 250 additions & 6 deletions

File tree

e2e-tests/tests/e2e.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,43 @@ async fn test_cli_bolt11_send() {
323323
);
324324
}
325325

326+
#[tokio::test]
327+
async fn test_cli_pay() {
328+
let bitcoind = TestBitcoind::new();
329+
let server_a = LdkServerHandle::start(&bitcoind).await;
330+
let server_b = LdkServerHandle::start(&bitcoind).await;
331+
setup_funded_channel(&bitcoind, &server_a, &server_b, 100_000).await;
332+
333+
// Pay a BOLT11 invoice via unified `pay` command
334+
let invoice_resp = server_b
335+
.client()
336+
.bolt11_receive(Bolt11ReceiveRequest {
337+
amount_msat: Some(10_000_000),
338+
description: Some(Bolt11InvoiceDescription {
339+
kind: Some(bolt11_invoice_description::Kind::Direct("test".to_string())),
340+
}),
341+
expiry_secs: 3600,
342+
})
343+
.await
344+
.unwrap();
345+
let output = run_cli(&server_a, &["pay", &invoice_resp.invoice]);
346+
assert!(output.get("bolt11_payment_id").is_some());
347+
348+
// Pay a BOLT12 offer via unified `pay` command
349+
let offer_resp = server_b
350+
.client()
351+
.bolt12_receive(Bolt12ReceiveRequest {
352+
description: "test offer".to_string(),
353+
amount_msat: None,
354+
expiry_secs: None,
355+
quantity: None,
356+
})
357+
.await
358+
.unwrap();
359+
let output = run_cli(&server_a, &["pay", &offer_resp.offer, "10000sat"]);
360+
assert!(output.get("bolt12_payment_id").is_some());
361+
}
362+
326363
#[tokio::test]
327364
async fn test_cli_bolt12_send() {
328365
let bitcoind = TestBitcoind::new();

ldk-server-cli/src/main.rs

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ use ldk_server_client::ldk_server_protos::api::{
3939
OnchainReceiveRequest, OnchainReceiveResponse, OnchainSendRequest, OnchainSendResponse,
4040
OpenChannelRequest, OpenChannelResponse, SignMessageRequest, SignMessageResponse,
4141
SpliceInRequest, SpliceInResponse, SpliceOutRequest, SpliceOutResponse, SpontaneousSendRequest,
42-
SpontaneousSendResponse, UpdateChannelConfigRequest, UpdateChannelConfigResponse,
43-
VerifySignatureRequest, VerifySignatureResponse,
42+
SpontaneousSendResponse, UnifiedSendRequest, UnifiedSendResponse, UpdateChannelConfigRequest,
43+
UpdateChannelConfigResponse, VerifySignatureRequest, VerifySignatureResponse,
4444
};
4545
use ldk_server_client::ldk_server_protos::types::{
4646
bolt11_invoice_description, Bolt11InvoiceDescription, ChannelConfig, PageToken,
@@ -312,6 +312,32 @@ enum Commands {
312312
)]
313313
max_channel_saturation_power_of_half: Option<u32>,
314314
},
315+
#[command(
316+
about = "Pay a BIP 21 URI, BIP 353 Human-Readable Name, BOLT11 invoice, or BOLT12 offer"
317+
)]
318+
Pay {
319+
#[arg(help = "A BIP 21 URI, BIP 353 Human-Readable Name, BOLT11 invoice, or BOLT12 offer")]
320+
uri: String,
321+
#[arg(help = "Amount to send, e.g. 50sat or 50000msat. Required for variable-amount URIs")]
322+
amount: Option<Amount>,
323+
#[arg(
324+
long,
325+
help = "Maximum total routing fee, e.g. 50sat or 50000msat. Defaults to 1% of payment + 50 sats"
326+
)]
327+
max_total_routing_fee: Option<Amount>,
328+
#[arg(long, help = "Maximum total CLTV delta we accept for the route (default: 1008)")]
329+
max_total_cltv_expiry_delta: Option<u32>,
330+
#[arg(
331+
long,
332+
help = "Maximum number of paths that may be used by MPP payments (default: 10)"
333+
)]
334+
max_path_count: Option<u32>,
335+
#[arg(
336+
long,
337+
help = "Maximum share of a channel's total capacity to send over a channel, as a power of 1/2 (default: 2)"
338+
)]
339+
max_channel_saturation_power_of_half: Option<u32>,
340+
},
315341
#[command(about = "Cooperatively close the channel specified by the given channel ID")]
316342
CloseChannel {
317343
#[arg(help = "The local user_channel_id of this channel")]
@@ -808,6 +834,34 @@ async fn main() {
808834
.await,
809835
);
810836
},
837+
Commands::Pay {
838+
uri,
839+
amount,
840+
max_total_routing_fee,
841+
max_total_cltv_expiry_delta,
842+
max_path_count,
843+
max_channel_saturation_power_of_half,
844+
} => {
845+
let amount_msat = amount.map(|a| a.to_msat());
846+
let max_total_routing_fee_msat = max_total_routing_fee.map(|a| a.to_msat());
847+
let route_parameters = RouteParametersConfig {
848+
max_total_routing_fee_msat,
849+
max_total_cltv_expiry_delta: max_total_cltv_expiry_delta
850+
.unwrap_or(DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA),
851+
max_path_count: max_path_count.unwrap_or(DEFAULT_MAX_PATH_COUNT),
852+
max_channel_saturation_power_of_half: max_channel_saturation_power_of_half
853+
.unwrap_or(DEFAULT_MAX_CHANNEL_SATURATION_POWER_OF_HALF),
854+
};
855+
handle_response_result::<_, UnifiedSendResponse>(
856+
client
857+
.unified_send(UnifiedSendRequest {
858+
uri,
859+
amount_msat,
860+
route_parameters: Some(route_parameters),
861+
})
862+
.await,
863+
);
864+
},
811865
Commands::CloseChannel { user_channel_id, counterparty_node_id } => {
812866
handle_response_result::<_, CloseChannelResponse>(
813867
client

ldk-server-client/src/client.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ use ldk_server_protos::api::{
3030
OnchainReceiveRequest, OnchainReceiveResponse, OnchainSendRequest, OnchainSendResponse,
3131
OpenChannelRequest, OpenChannelResponse, SignMessageRequest, SignMessageResponse,
3232
SpliceInRequest, SpliceInResponse, SpliceOutRequest, SpliceOutResponse, SpontaneousSendRequest,
33-
SpontaneousSendResponse, UpdateChannelConfigRequest, UpdateChannelConfigResponse,
34-
VerifySignatureRequest, VerifySignatureResponse,
33+
SpontaneousSendResponse, UnifiedSendRequest, UnifiedSendResponse, UpdateChannelConfigRequest,
34+
UpdateChannelConfigResponse, VerifySignatureRequest, VerifySignatureResponse,
3535
};
3636
use ldk_server_protos::endpoints::{
3737
BOLT11_CLAIM_FOR_HASH_PATH, BOLT11_FAIL_FOR_HASH_PATH, BOLT11_RECEIVE_FOR_HASH_PATH,
@@ -42,7 +42,8 @@ use ldk_server_protos::endpoints::{
4242
GRAPH_GET_CHANNEL_PATH, GRAPH_GET_NODE_PATH, GRAPH_LIST_CHANNELS_PATH, GRAPH_LIST_NODES_PATH,
4343
LIST_CHANNELS_PATH, LIST_FORWARDED_PAYMENTS_PATH, LIST_PAYMENTS_PATH, LIST_PEERS_PATH,
4444
ONCHAIN_RECEIVE_PATH, ONCHAIN_SEND_PATH, OPEN_CHANNEL_PATH, SIGN_MESSAGE_PATH, SPLICE_IN_PATH,
45-
SPLICE_OUT_PATH, SPONTANEOUS_SEND_PATH, UPDATE_CHANNEL_CONFIG_PATH, VERIFY_SIGNATURE_PATH,
45+
SPLICE_OUT_PATH, SPONTANEOUS_SEND_PATH, UNIFIED_SEND_PATH, UPDATE_CHANNEL_CONFIG_PATH,
46+
VERIFY_SIGNATURE_PATH,
4647
};
4748
use ldk_server_protos::error::{ErrorCode, ErrorResponse};
4849
use prost::Message;
@@ -354,6 +355,15 @@ impl LdkServerClient {
354355
self.post_request(&request, &url).await
355356
}
356357

358+
/// Send a payment given a BIP 21 URI or BIP 353 Human-Readable Name.
359+
/// For API contract/usage, refer to docs for [`UnifiedSendRequest`] and [`UnifiedSendResponse`].
360+
pub async fn unified_send(
361+
&self, request: UnifiedSendRequest,
362+
) -> Result<UnifiedSendResponse, LdkServerError> {
363+
let url = format!("https://{}/{UNIFIED_SEND_PATH}", self.base_url);
364+
self.post_request(&request, &url).await
365+
}
366+
357367
/// Sign a message with the node's secret key.
358368
/// For API contract/usage, refer to docs for [`SignMessageRequest`] and [`SignMessageResponse`].
359369
pub async fn sign_message(

ldk-server-protos/build.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ fn generate_protos() {
6969
"types.ClaimableAwaitingConfirmations.source",
7070
"#[cfg_attr(feature = \"serde\", serde(serialize_with = \"crate::serde_utils::serialize_balance_source\"))]",
7171
)
72+
.field_attribute(
73+
"api.UnifiedSendResponse.payment_result",
74+
"#[cfg_attr(feature = \"serde\", serde(flatten))]",
75+
)
7276
.compile_protos(
7377
&[
7478
"src/proto/api.proto",

ldk-server-protos/src/api.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -995,6 +995,56 @@ pub struct GraphListNodesResponse {
995995
#[prost(string, repeated, tag = "1")]
996996
pub node_ids: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
997997
}
998+
/// Send a payment given a BIP 21 URI or BIP 353 Human-Readable Name.
999+
///
1000+
/// This method parses the provided URI string and attempts to send the payment. If the URI
1001+
/// has an offer and/or invoice, it will try to pay the offer first followed by the invoice.
1002+
/// If they both fail, the on-chain payment will be paid.
1003+
/// See more: <https://docs.rs/ldk-node/latest/ldk_node/payment/struct.UnifiedPayment.html#method.send>
1004+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1005+
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
1006+
#[allow(clippy::derive_partial_eq_without_eq)]
1007+
#[derive(Clone, PartialEq, ::prost::Message)]
1008+
pub struct UnifiedSendRequest {
1009+
/// A BIP 21 URI or BIP 353 Human-Readable Name to pay.
1010+
#[prost(string, tag = "1")]
1011+
pub uri: ::prost::alloc::string::String,
1012+
/// The amount in millisatoshis to send. Required for "zero-amount" or variable-amount URIs.
1013+
#[prost(uint64, optional, tag = "2")]
1014+
pub amount_msat: ::core::option::Option<u64>,
1015+
/// Configuration options for payment routing and pathfinding.
1016+
#[prost(message, optional, tag = "3")]
1017+
pub route_parameters: ::core::option::Option<super::types::RouteParametersConfig>,
1018+
}
1019+
/// The response `content` for the `UnifiedSend` API, when HttpStatusCode is OK (200).
1020+
/// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`.
1021+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1022+
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
1023+
#[allow(clippy::derive_partial_eq_without_eq)]
1024+
#[derive(Clone, PartialEq, ::prost::Message)]
1025+
pub struct UnifiedSendResponse {
1026+
#[prost(oneof = "unified_send_response::PaymentResult", tags = "1, 2, 3")]
1027+
#[cfg_attr(feature = "serde", serde(flatten))]
1028+
pub payment_result: ::core::option::Option<unified_send_response::PaymentResult>,
1029+
}
1030+
/// Nested message and enum types in `UnifiedSendResponse`.
1031+
pub mod unified_send_response {
1032+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1033+
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
1034+
#[allow(clippy::derive_partial_eq_without_eq)]
1035+
#[derive(Clone, PartialEq, ::prost::Oneof)]
1036+
pub enum PaymentResult {
1037+
/// An on-chain payment was made. Contains the transaction ID.
1038+
#[prost(string, tag = "1")]
1039+
Txid(::prost::alloc::string::String),
1040+
/// A BOLT11 payment was made. Contains the payment ID in hex-encoded form.
1041+
#[prost(string, tag = "2")]
1042+
Bolt11PaymentId(::prost::alloc::string::String),
1043+
/// A BOLT12 payment was made. Contains the payment ID in hex-encoded form.
1044+
#[prost(string, tag = "3")]
1045+
Bolt12PaymentId(::prost::alloc::string::String),
1046+
}
1047+
}
9981048
/// Returns information on a node with the given ID from the network graph.
9991049
/// See more: <https://docs.rs/ldk-node/latest/ldk_node/graph/struct.NetworkGraph.html#method.node>
10001050
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]

ldk-server-protos/src/endpoints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ pub const SPONTANEOUS_SEND_PATH: &str = "SpontaneousSend";
3838
pub const SIGN_MESSAGE_PATH: &str = "SignMessage";
3939
pub const VERIFY_SIGNATURE_PATH: &str = "VerifySignature";
4040
pub const EXPORT_PATHFINDING_SCORES_PATH: &str = "ExportPathfindingScores";
41+
pub const UNIFIED_SEND_PATH: &str = "UnifiedSend";
4142
pub const GRAPH_LIST_CHANNELS_PATH: &str = "GraphListChannels";
4243
pub const GRAPH_GET_CHANNEL_PATH: &str = "GraphGetChannel";
4344
pub const GRAPH_LIST_NODES_PATH: &str = "GraphListNodes";

ldk-server-protos/src/proto/api.proto

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -774,6 +774,41 @@ message GraphListNodesResponse {
774774
repeated string node_ids = 1;
775775
}
776776

777+
// Send a payment given a BIP 21 URI or BIP 353 Human-Readable Name.
778+
//
779+
// This method parses the provided URI string and attempts to send the payment. If the URI
780+
// has an offer and/or invoice, it will try to pay the offer first followed by the invoice.
781+
// If they both fail, the on-chain payment will be paid.
782+
// See more: https://docs.rs/ldk-node/latest/ldk_node/payment/struct.UnifiedPayment.html#method.send
783+
message UnifiedSendRequest {
784+
785+
// A BIP 21 URI or BIP 353 Human-Readable Name to pay.
786+
string uri = 1;
787+
788+
// The amount in millisatoshis to send. Required for "zero-amount" or variable-amount URIs.
789+
optional uint64 amount_msat = 2;
790+
791+
// Configuration options for payment routing and pathfinding.
792+
optional types.RouteParametersConfig route_parameters = 3;
793+
}
794+
795+
// The response `content` for the `UnifiedSend` API, when HttpStatusCode is OK (200).
796+
// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`.
797+
message UnifiedSendResponse {
798+
799+
oneof payment_result {
800+
801+
// An on-chain payment was made. Contains the transaction ID.
802+
string txid = 1;
803+
804+
// A BOLT11 payment was made. Contains the payment ID in hex-encoded form.
805+
string bolt11_payment_id = 2;
806+
807+
// A BOLT12 payment was made. Contains the payment ID in hex-encoded form.
808+
string bolt12_payment_id = 3;
809+
}
810+
}
811+
777812
// Returns information on a node with the given ID from the network graph.
778813
// See more: https://docs.rs/ldk-node/latest/ldk_node/graph/struct.NetworkGraph.html#method.node
779814
message GraphGetNodeRequest {

ldk-server/src/api/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ pub(crate) mod open_channel;
4444
pub(crate) mod sign_message;
4545
pub(crate) mod splice_channel;
4646
pub(crate) mod spontaneous_send;
47+
pub(crate) mod unified_send;
4748
pub(crate) mod update_channel_config;
4849
pub(crate) mod verify_signature;
4950

ldk-server/src/api/unified_send.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// This file is Copyright its original authors, visible in version control
2+
// history.
3+
//
4+
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
5+
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
6+
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
7+
// You may not use this file except in accordance with one or both of these
8+
// licenses.
9+
10+
use ldk_node::payment::UnifiedPaymentResult;
11+
use ldk_server_protos::api::unified_send_response::PaymentResult;
12+
use ldk_server_protos::api::{UnifiedSendRequest, UnifiedSendResponse};
13+
use tokio::runtime::Handle;
14+
15+
use crate::api::build_route_parameters_config_from_proto;
16+
use crate::api::error::LdkServerError;
17+
use crate::service::Context;
18+
19+
pub(crate) fn handle_unified_send_request(
20+
context: Context, request: UnifiedSendRequest,
21+
) -> Result<UnifiedSendResponse, LdkServerError> {
22+
let route_parameters = build_route_parameters_config_from_proto(request.route_parameters)?;
23+
24+
let result = tokio::task::block_in_place(|| {
25+
Handle::current().block_on(context.node.unified_payment().send(
26+
&request.uri,
27+
request.amount_msat,
28+
route_parameters,
29+
))
30+
})?;
31+
32+
let payment_result = match result {
33+
UnifiedPaymentResult::Onchain { txid } => PaymentResult::Txid(txid.to_string()),
34+
UnifiedPaymentResult::Bolt11 { payment_id } => {
35+
PaymentResult::Bolt11PaymentId(payment_id.to_string())
36+
},
37+
UnifiedPaymentResult::Bolt12 { payment_id } => {
38+
PaymentResult::Bolt12PaymentId(payment_id.to_string())
39+
},
40+
};
41+
42+
Ok(UnifiedSendResponse { payment_result: Some(payment_result) })
43+
}

ldk-server/src/service.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ use ldk_server_protos::endpoints::{
2727
GRAPH_GET_CHANNEL_PATH, GRAPH_GET_NODE_PATH, GRAPH_LIST_CHANNELS_PATH, GRAPH_LIST_NODES_PATH,
2828
LIST_CHANNELS_PATH, LIST_FORWARDED_PAYMENTS_PATH, LIST_PAYMENTS_PATH, LIST_PEERS_PATH,
2929
ONCHAIN_RECEIVE_PATH, ONCHAIN_SEND_PATH, OPEN_CHANNEL_PATH, SIGN_MESSAGE_PATH, SPLICE_IN_PATH,
30-
SPLICE_OUT_PATH, SPONTANEOUS_SEND_PATH, UPDATE_CHANNEL_CONFIG_PATH, VERIFY_SIGNATURE_PATH,
30+
SPLICE_OUT_PATH, SPONTANEOUS_SEND_PATH, UNIFIED_SEND_PATH, UPDATE_CHANNEL_CONFIG_PATH,
31+
VERIFY_SIGNATURE_PATH,
3132
};
3233
use prost::Message;
3334

@@ -65,6 +66,7 @@ use crate::api::open_channel::handle_open_channel;
6566
use crate::api::sign_message::handle_sign_message_request;
6667
use crate::api::splice_channel::{handle_splice_in_request, handle_splice_out_request};
6768
use crate::api::spontaneous_send::handle_spontaneous_send_request;
69+
use crate::api::unified_send::handle_unified_send_request;
6870
use crate::api::update_channel_config::handle_update_channel_config_request;
6971
use crate::api::verify_signature::handle_verify_signature_request;
7072
use crate::io::persist::paginated_kv_store::PaginatedKVStore;
@@ -369,6 +371,13 @@ impl Service<Request<Incoming>> for NodeService {
369371
api_key,
370372
handle_spontaneous_send_request,
371373
)),
374+
UNIFIED_SEND_PATH => Box::pin(handle_request(
375+
context,
376+
req,
377+
auth_params,
378+
api_key,
379+
handle_unified_send_request,
380+
)),
372381
SIGN_MESSAGE_PATH => Box::pin(handle_request(
373382
context,
374383
req,

0 commit comments

Comments
 (0)