Skip to content
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
37 changes: 37 additions & 0 deletions e2e-tests/tests/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,43 @@ async fn test_cli_bolt11_send() {
);
}

#[tokio::test]
async fn test_cli_pay() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I guess if we tested BIP 353, it might be too flaky with the way github CI is?

let bitcoind = TestBitcoind::new();
let server_a = LdkServerHandle::start(&bitcoind).await;
let server_b = LdkServerHandle::start(&bitcoind).await;
setup_funded_channel(&bitcoind, &server_a, &server_b, 100_000).await;

// Pay a BOLT11 invoice via unified `pay` command
let invoice_resp = server_b
.client()
.bolt11_receive(Bolt11ReceiveRequest {
amount_msat: Some(10_000_000),
description: Some(Bolt11InvoiceDescription {
kind: Some(bolt11_invoice_description::Kind::Direct("test".to_string())),
}),
expiry_secs: 3600,
})
.await
.unwrap();
let output = run_cli(&server_a, &["pay", &invoice_resp.invoice]);
assert!(output.get("bolt11_payment_id").is_some());

// Pay a BOLT12 offer via unified `pay` command
let offer_resp = server_b
.client()
.bolt12_receive(Bolt12ReceiveRequest {
description: "test offer".to_string(),
amount_msat: None,
expiry_secs: None,
quantity: None,
})
.await
.unwrap();
let output = run_cli(&server_a, &["pay", &offer_resp.offer, "10000sat"]);
assert!(output.get("bolt12_payment_id").is_some());
}

#[tokio::test]
async fn test_cli_bolt12_send() {
let bitcoind = TestBitcoind::new();
Expand Down
58 changes: 56 additions & 2 deletions ldk-server-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ use ldk_server_client::ldk_server_protos::api::{
OnchainReceiveRequest, OnchainReceiveResponse, OnchainSendRequest, OnchainSendResponse,
OpenChannelRequest, OpenChannelResponse, SignMessageRequest, SignMessageResponse,
SpliceInRequest, SpliceInResponse, SpliceOutRequest, SpliceOutResponse, SpontaneousSendRequest,
SpontaneousSendResponse, UpdateChannelConfigRequest, UpdateChannelConfigResponse,
VerifySignatureRequest, VerifySignatureResponse,
SpontaneousSendResponse, UnifiedSendRequest, UnifiedSendResponse, UpdateChannelConfigRequest,
UpdateChannelConfigResponse, VerifySignatureRequest, VerifySignatureResponse,
};
use ldk_server_client::ldk_server_protos::types::{
bolt11_invoice_description, Bolt11InvoiceDescription, ChannelConfig, PageToken,
Expand Down Expand Up @@ -275,6 +275,32 @@ enum Commands {
)]
max_channel_saturation_power_of_half: Option<u32>,
},
#[command(
about = "Pay a BIP 21 URI, BIP 353 Human-Readable Name, BOLT11 invoice, or BOLT12 offer"
)]
Pay {
#[arg(help = "A BIP 21 URI, BIP 353 Human-Readable Name, BOLT11 invoice, or BOLT12 offer")]
uri: String,
#[arg(help = "Amount to send, e.g. 50sat or 50000msat. Required for variable-amount URIs")]
amount: Option<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 = "Cooperatively close the channel specified by the given channel ID")]
CloseChannel {
#[arg(help = "The local user_channel_id of this channel")]
Expand Down Expand Up @@ -750,6 +776,34 @@ async fn main() {
.await,
);
},
Commands::Pay {
uri,
amount,
max_total_routing_fee,
max_total_cltv_expiry_delta,
max_path_count,
max_channel_saturation_power_of_half,
} => {
let amount_msat = amount.map(|a| a.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::<_, UnifiedSendResponse>(
client
.unified_send(UnifiedSendRequest {
uri,
amount_msat,
route_parameters: Some(route_parameters),
})
.await,
);
},
Commands::CloseChannel { user_channel_id, counterparty_node_id } => {
handle_response_result::<_, CloseChannelResponse>(
client
Expand Down
16 changes: 13 additions & 3 deletions ldk-server-client/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ use ldk_server_protos::api::{
OnchainReceiveRequest, OnchainReceiveResponse, OnchainSendRequest, OnchainSendResponse,
OpenChannelRequest, OpenChannelResponse, SignMessageRequest, SignMessageResponse,
SpliceInRequest, SpliceInResponse, SpliceOutRequest, SpliceOutResponse, SpontaneousSendRequest,
SpontaneousSendResponse, UpdateChannelConfigRequest, UpdateChannelConfigResponse,
VerifySignatureRequest, VerifySignatureResponse,
SpontaneousSendResponse, UnifiedSendRequest, UnifiedSendResponse, UpdateChannelConfigRequest,
UpdateChannelConfigResponse, VerifySignatureRequest, VerifySignatureResponse,
};
use ldk_server_protos::endpoints::{
BOLT11_CLAIM_FOR_HASH_PATH, BOLT11_FAIL_FOR_HASH_PATH, BOLT11_RECEIVE_FOR_HASH_PATH,
Expand All @@ -39,7 +39,8 @@ use ldk_server_protos::endpoints::{
GRAPH_GET_CHANNEL_PATH, GRAPH_GET_NODE_PATH, GRAPH_LIST_CHANNELS_PATH, GRAPH_LIST_NODES_PATH,
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, UPDATE_CHANNEL_CONFIG_PATH, VERIFY_SIGNATURE_PATH,
SPLICE_OUT_PATH, SPONTANEOUS_SEND_PATH, UNIFIED_SEND_PATH, UPDATE_CHANNEL_CONFIG_PATH,
VERIFY_SIGNATURE_PATH,
};
use ldk_server_protos::error::{ErrorCode, ErrorResponse};
use prost::Message;
Expand Down Expand Up @@ -327,6 +328,15 @@ impl LdkServerClient {
self.post_request(&request, &url).await
}

/// Send a payment given a BIP 21 URI or BIP 353 Human-Readable Name.
/// For API contract/usage, refer to docs for [`UnifiedSendRequest`] and [`UnifiedSendResponse`].
pub async fn unified_send(
&self, request: UnifiedSendRequest,
) -> Result<UnifiedSendResponse, LdkServerError> {
let url = format!("https://{}/{UNIFIED_SEND_PATH}", self.base_url);
self.post_request(&request, &url).await
}

/// Sign a message with the node's secret key.
/// For API contract/usage, refer to docs for [`SignMessageRequest`] and [`SignMessageResponse`].
pub async fn sign_message(
Expand Down
4 changes: 4 additions & 0 deletions ldk-server-protos/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ fn generate_protos() {
"types.ClaimableAwaitingConfirmations.source",
"#[cfg_attr(feature = \"serde\", serde(serialize_with = \"crate::serde_utils::serialize_balance_source\"))]",
)
.field_attribute(
"api.UnifiedSendResponse.payment_result",
"#[cfg_attr(feature = \"serde\", serde(flatten))]",
)
.compile_protos(
&[
"src/proto/api.proto",
Expand Down
50 changes: 50 additions & 0 deletions ldk-server-protos/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,56 @@ pub struct GraphListNodesResponse {
#[prost(string, repeated, tag = "1")]
pub node_ids: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
}
/// Send a payment given a BIP 21 URI or BIP 353 Human-Readable Name.
///
/// This method parses the provided URI string and attempts to send the payment. If the URI
/// has an offer and/or invoice, it will try to pay the offer first followed by the invoice.
/// If they both fail, the on-chain payment will be paid.
/// See more: <https://docs.rs/ldk-node/latest/ldk_node/payment/struct.UnifiedPayment.html#method.send>
#[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 UnifiedSendRequest {
/// A BIP 21 URI or BIP 353 Human-Readable Name to pay.
#[prost(string, tag = "1")]
pub uri: ::prost::alloc::string::String,
/// The amount in millisatoshis to send. Required for "zero-amount" or variable-amount URIs.
#[prost(uint64, optional, tag = "2")]
pub amount_msat: ::core::option::Option<u64>,
/// Configuration options for payment routing and pathfinding.
#[prost(message, optional, tag = "3")]
pub route_parameters: ::core::option::Option<super::types::RouteParametersConfig>,
}
/// The response `content` for the `UnifiedSend` 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 UnifiedSendResponse {
#[prost(oneof = "unified_send_response::PaymentResult", tags = "1, 2, 3")]
#[cfg_attr(feature = "serde", serde(flatten))]
pub payment_result: ::core::option::Option<unified_send_response::PaymentResult>,
}
/// Nested message and enum types in `UnifiedSendResponse`.
pub mod unified_send_response {
#[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::Oneof)]
pub enum PaymentResult {
/// An on-chain payment was made. Contains the transaction ID.
#[prost(string, tag = "1")]
Txid(::prost::alloc::string::String),
/// A BOLT11 payment was made. Contains the payment ID in hex-encoded form.
#[prost(string, tag = "2")]
Bolt11PaymentId(::prost::alloc::string::String),
/// A BOLT12 payment was made. Contains the payment ID in hex-encoded form.
#[prost(string, tag = "3")]
Bolt12PaymentId(::prost::alloc::string::String),
}
}
/// Returns information on a node with the given ID from the network graph.
/// See more: <https://docs.rs/ldk-node/latest/ldk_node/graph/struct.NetworkGraph.html#method.node>
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
Expand Down
1 change: 1 addition & 0 deletions ldk-server-protos/src/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ pub const SPONTANEOUS_SEND_PATH: &str = "SpontaneousSend";
pub const SIGN_MESSAGE_PATH: &str = "SignMessage";
pub const VERIFY_SIGNATURE_PATH: &str = "VerifySignature";
pub const EXPORT_PATHFINDING_SCORES_PATH: &str = "ExportPathfindingScores";
pub const UNIFIED_SEND_PATH: &str = "UnifiedSend";
pub const GRAPH_LIST_CHANNELS_PATH: &str = "GraphListChannels";
pub const GRAPH_GET_CHANNEL_PATH: &str = "GraphGetChannel";
pub const GRAPH_LIST_NODES_PATH: &str = "GraphListNodes";
Expand Down
35 changes: 35 additions & 0 deletions ldk-server-protos/src/proto/api.proto
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,41 @@ message GraphListNodesResponse {
repeated string node_ids = 1;
}

// Send a payment given a BIP 21 URI or BIP 353 Human-Readable Name.
//
// This method parses the provided URI string and attempts to send the payment. If the URI
// has an offer and/or invoice, it will try to pay the offer first followed by the invoice.
// If they both fail, the on-chain payment will be paid.
// See more: https://docs.rs/ldk-node/latest/ldk_node/payment/struct.UnifiedPayment.html#method.send
message UnifiedSendRequest {

// A BIP 21 URI or BIP 353 Human-Readable Name to pay.
string uri = 1;

// The amount in millisatoshis to send. Required for "zero-amount" or variable-amount URIs.
optional uint64 amount_msat = 2;

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

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

oneof payment_result {

// An on-chain payment was made. Contains the transaction ID.
string txid = 1;

// A BOLT11 payment was made. Contains the payment ID in hex-encoded form.
string bolt11_payment_id = 2;

// A BOLT12 payment was made. Contains the payment ID in hex-encoded form.
string bolt12_payment_id = 3;
}
}

// Returns information on a node with the given ID from the network graph.
// See more: https://docs.rs/ldk-node/latest/ldk_node/graph/struct.NetworkGraph.html#method.node
message GraphGetNodeRequest {
Expand Down
1 change: 1 addition & 0 deletions ldk-server/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ pub(crate) mod open_channel;
pub(crate) mod sign_message;
pub(crate) mod splice_channel;
pub(crate) mod spontaneous_send;
pub(crate) mod unified_send;
pub(crate) mod update_channel_config;
pub(crate) mod verify_signature;

Expand Down
43 changes: 43 additions & 0 deletions ldk-server/src/api/unified_send.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// 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_node::payment::UnifiedPaymentResult;
use ldk_server_protos::api::unified_send_response::PaymentResult;
use ldk_server_protos::api::{UnifiedSendRequest, UnifiedSendResponse};
use tokio::runtime::Handle;

use crate::api::build_route_parameters_config_from_proto;
use crate::api::error::LdkServerError;
use crate::service::Context;

pub(crate) fn handle_unified_send_request(
context: Context, request: UnifiedSendRequest,
) -> Result<UnifiedSendResponse, LdkServerError> {
let route_parameters = build_route_parameters_config_from_proto(request.route_parameters)?;

let result = tokio::task::block_in_place(|| {
Handle::current().block_on(context.node.unified_payment().send(
&request.uri,
request.amount_msat,
route_parameters,
))
})?;

let payment_result = match result {
UnifiedPaymentResult::Onchain { txid } => PaymentResult::Txid(txid.to_string()),
UnifiedPaymentResult::Bolt11 { payment_id } => {
PaymentResult::Bolt11PaymentId(payment_id.to_string())
},
UnifiedPaymentResult::Bolt12 { payment_id } => {
PaymentResult::Bolt12PaymentId(payment_id.to_string())
},
};

Ok(UnifiedSendResponse { payment_result: Some(payment_result) })
}
11 changes: 10 additions & 1 deletion ldk-server/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ use ldk_server_protos::endpoints::{
GRAPH_GET_CHANNEL_PATH, GRAPH_GET_NODE_PATH, GRAPH_LIST_CHANNELS_PATH, GRAPH_LIST_NODES_PATH,
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, UPDATE_CHANNEL_CONFIG_PATH, VERIFY_SIGNATURE_PATH,
SPLICE_OUT_PATH, SPONTANEOUS_SEND_PATH, UNIFIED_SEND_PATH, UPDATE_CHANNEL_CONFIG_PATH,
VERIFY_SIGNATURE_PATH,
};
use prost::Message;

Expand Down Expand Up @@ -60,6 +61,7 @@ use crate::api::open_channel::handle_open_channel;
use crate::api::sign_message::handle_sign_message_request;
use crate::api::splice_channel::{handle_splice_in_request, handle_splice_out_request};
use crate::api::spontaneous_send::handle_spontaneous_send_request;
use crate::api::unified_send::handle_unified_send_request;
use crate::api::update_channel_config::handle_update_channel_config_request;
use crate::api::verify_signature::handle_verify_signature_request;
use crate::io::persist::paginated_kv_store::PaginatedKVStore;
Expand Down Expand Up @@ -350,6 +352,13 @@ impl Service<Request<Incoming>> for NodeService {
api_key,
handle_spontaneous_send_request,
)),
UNIFIED_SEND_PATH => Box::pin(handle_request(
context,
req,
auth_params,
api_key,
handle_unified_send_request,
)),
SIGN_MESSAGE_PATH => Box::pin(handle_request(
context,
req,
Expand Down
Loading