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
39 changes: 38 additions & 1 deletion e2e-tests/tests/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ use ldk_server_client::ldk_server_grpc::events::{
use ldk_server_client::ldk_server_grpc::types::{
bolt11_invoice_description, Bolt11InvoiceDescription,
};

use ldk_server_grpc::types::payment_kind;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: can we have a new line between the imports and const

const EVENT_TIMEOUT: Duration = Duration::from_secs(15);

async fn wait_for_event(events: &mut EventStream, pred: impl Fn(&Event) -> bool) -> EventEnvelope {
Expand Down Expand Up @@ -1462,3 +1462,40 @@ async fn test_metrics_endpoint_with_auth() {
assert!(metrics.contains("ldk_server_total_anchor_channels_reserve_sats 0"));
assert!(metrics.contains("ldk_server_total_lightning_balance_sats 0"));
}

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

let mut events_b = server_b.client().subscribe_events().await.unwrap();

setup_funded_channel(&bitcoind, &server_a, &server_b, 100_000).await;

// Generate a known preimage and compute its payment hash
let preimage_bytes = [43u8; 32];
let preimage_hex = preimage_bytes.to_lower_hex_string();
let payment_hash = sha256::Hash::hash(&preimage_bytes);
let payment_hash_hex = payment_hash.to_byte_array().to_lower_hex_string();

let output = run_cli(
&server_a,
&["spontaneous-send", server_b.node_id(), "10000sat", "--preimage", &preimage_hex],
);

assert!(!output["payment_id"].as_str().unwrap().is_empty());

// The receiver must observe in PaymentReceived for checking on Spontaneous payment.
let event_b = wait_for_event(&mut events_b, |e| matches!(e, Event::PaymentReceived(_))).await;
let Some(Event::PaymentReceived(pr)) = event_b.event else {
panic!("expected PaymentReceived");
};

let payment = pr.payment.unwrap();

let Some(payment_kind::Kind::Spontaneous(spont)) = payment.kind.unwrap().kind else {
panic!("expected spontaneous kind");
};
assert_eq!(spont.hash, payment_hash_hex);
}
4 changes: 4 additions & 0 deletions ldk-server-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,8 @@ enum Commands {
help = "Custom TLV record to attach, format: <type_num>:<hex_value>. Repeatable. type_num must be >= 65536."
)]
custom_tlvs: Vec<(u64, Vec<u8>)>,
#[arg(long)]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

please add help text

preimage: Option<String>,
},
#[command(
about = "Pay a BIP 21 URI, BIP 353 Human-Readable Name, BOLT11 invoice, or BOLT12 offer"
Expand Down Expand Up @@ -817,6 +819,7 @@ async fn main() {
max_path_count,
max_channel_saturation_power_of_half,
custom_tlvs,
preimage,
} => {
let amount_msat = amount.to_msat();
let max_total_routing_fee_msat = max_total_routing_fee.map(|a| a.to_msat());
Expand All @@ -841,6 +844,7 @@ async fn main() {
node_id,
route_parameters: Some(route_parameters),
custom_tlvs: proto_custom_tlvs,
preimage,
})
.await,
);
Expand Down
4 changes: 4 additions & 0 deletions ldk-server-grpc/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,10 @@ pub struct SpontaneousSendRequest {
/// Custom TLV records to attach to the outgoing payment.
#[prost(message, repeated, tag = "4")]
pub custom_tlvs: ::prost::alloc::vec::Vec<super::types::CustomTlvRecord>,
/// An optional hex-encoded 32-byte payment preimage. If provided, it will be used instead of
/// generating a random one. The payment hash will be the SHA256 of this value.
#[prost(string, optional, tag = "5")]
pub preimage: ::core::option::Option<::prost::alloc::string::String>,
}
/// The response for the `SpontaneousSend` RPC. On failure, a gRPC error status is returned.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
Expand Down
4 changes: 4 additions & 0 deletions ldk-server-grpc/src/proto/api.proto
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,10 @@ message SpontaneousSendRequest {

// Custom TLV records to attach to the outgoing payment.
repeated types.CustomTlvRecord custom_tlvs = 4;

// An optional hex-encoded 32-byte payment preimage. If provided, it will be used instead of
// generating a random one. The payment hash will be the SHA256 of this value.
optional string preimage = 5;
}

// The response for the `SpontaneousSend` RPC. On failure, a gRPC error status is returned.
Expand Down
6 changes: 5 additions & 1 deletion ldk-server-mcp/src/tools/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,11 @@ pub fn spontaneous_send_schema() -> Value {
"type": "string",
"description": "The hex-encoded public key of the destination node"
},
"route_parameters": route_parameters_config_schema()
"route_parameters": route_parameters_config_schema(),
"preimage": {
"type": "string",
"description": "The hex-encoded 32-byte payment preimage"
}
},
"required": ["amount_msat", "node_id"]
})
Expand Down
49 changes: 40 additions & 9 deletions ldk-server/src/api/spontaneous_send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
use std::str::FromStr;
use std::sync::Arc;

use hex::FromHex;
use ldk_node::bitcoin::secp256k1::PublicKey;
use ldk_node::lightning_types::payment::PaymentPreimage;
use ldk_server_grpc::api::{SpontaneousSendRequest, SpontaneousSendResponse};

use crate::api::error::LdkServerError;
Expand All @@ -27,19 +29,48 @@ pub(crate) async fn handle_spontaneous_send_request(

let route_parameters = build_route_parameters_config_from_proto(request.route_parameters)?;

let payment_id = if request.custom_tlvs.is_empty() {
context.node.spontaneous_payment().send(request.amount_msat, node_id, route_parameters)?
} else {
let custom_tlvs: Vec<_> =
request.custom_tlvs.iter().map(proto_to_node_custom_tlv).collect();
context.node.spontaneous_payment().send_with_custom_tlvs(
let preimage = request
.preimage
.map(|p| {
<[u8; 32]>::from_hex(&p).map(PaymentPreimage).map_err(|_| {
LdkServerError::new(
InvalidRequestError,
"Invalid preimage, must be a 32-byte hex string.".to_string(),
)
})
})
.transpose()?;

let custom_tlvs: Vec<_> = request.custom_tlvs.iter().map(proto_to_node_custom_tlv).collect();

let payment_id = match (preimage, custom_tlvs.is_empty()) {
(None, true) => context.node.spontaneous_payment().send(
request.amount_msat,
node_id,
route_parameters,
)?,
(None, false) => context.node.spontaneous_payment().send_with_custom_tlvs(
request.amount_msat,
node_id,
route_parameters,
custom_tlvs,
)?
)?,
(Some(preimage), true) => context.node.spontaneous_payment().send_with_preimage(
request.amount_msat,
node_id,
preimage,
route_parameters,
)?,
(Some(preimage), false) => {
context.node.spontaneous_payment().send_with_preimage_and_custom_tlvs(
request.amount_msat,
node_id,
custom_tlvs,
preimage,
route_parameters,
)?
},
};

let response = SpontaneousSendResponse { payment_id: payment_id.to_string() };
Ok(response)
Ok(SpontaneousSendResponse { payment_id: payment_id.to_string() })
}