Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion e2e-tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ use std::time::Duration;
use corepc_node::Node;
use hex_conservative::DisplayHex;
use ldk_server_client::client::LdkServerClient;
use serde_json::Value;
use ldk_server_client::ldk_server_grpc::api::{GetNodeInfoRequest, GetNodeInfoResponse};
use ldk_server_grpc::api::{
GetBalancesRequest, ListChannelsRequest, OnchainReceiveRequest, OpenChannelRequest,
};
use serde_json::Value;

/// Wrapper around a managed bitcoind process for regtest.
pub struct TestBitcoind {
Expand Down
38 changes: 38 additions & 0 deletions e2e-tests/tests/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +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

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.

@f3r10 Looks like you missed this in your latest push.


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

Expand Down Expand Up @@ -1462,3 +1463,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);
}
7 changes: 7 additions & 0 deletions ldk-server-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,11 @@ 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,
help = "An optional hex-encoded 32-byte payment preimage. If provided, it will be used instead of generating a random one."
)]
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 +822,7 @@ async fn main() {
max_path_count,
max_channel_saturation_power_of_half,
custom_tlvs,
preimage,
} => {
let amount_msat = amount.to_msat();
Comment thread
benthecarman marked this conversation as resolved.
let max_total_routing_fee_msat = max_total_routing_fee.map(|a| a.to_msat());
Expand All @@ -841,6 +847,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
52 changes: 44 additions & 8 deletions ldk-server/src/api/spontaneous_send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
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_node::CustomTlvRecord;
use ldk_server_grpc::api::{SpontaneousSendRequest, SpontaneousSendResponse};

use crate::api::error::LdkServerError;
Expand All @@ -27,19 +30,52 @@ 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)?
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: Option<Vec<CustomTlvRecord>> = if request.custom_tlvs.is_empty() {
None
} else {
let custom_tlvs: Vec<_> =
request.custom_tlvs.iter().map(proto_to_node_custom_tlv).collect();
context.node.spontaneous_payment().send_with_custom_tlvs(
Some(request.custom_tlvs.iter().map(proto_to_node_custom_tlv).collect())
};

let payment_id = match (preimage, custom_tlvs) {
(None, None) => context.node.spontaneous_payment().send(
request.amount_msat,
node_id,
route_parameters,
)?,
(None, Some(custom_tlvs)) => context.node.spontaneous_payment().send_with_custom_tlvs(
request.amount_msat,
node_id,
route_parameters,
custom_tlvs,
)?
)?,
(Some(preimage), None) => context.node.spontaneous_payment().send_with_preimage(
request.amount_msat,
node_id,
preimage,
route_parameters,
)?,
(Some(preimage), Some(custom_tlvs)) => {
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() })
}