diff --git a/docs/api-guide.md b/docs/api-guide.md index eb182a0b..1b092c32 100644 --- a/docs/api-guide.md +++ b/docs/api-guide.md @@ -126,12 +126,14 @@ when the invoice is paid. | `Bolt11ReceiveViaJitChannel` | Create a fixed-amount invoice with JIT channel opening | | `Bolt11ReceiveVariableAmountViaJitChannel` | Create a variable-amount invoice with JIT channel opening | -### BOLT12 Offers - -| RPC | Description | -|-----------------|-------------------------------------------------------------------------| -| `Bolt12Receive` | Create a BOLT12 offer (fixed or variable amount) | -| `Bolt12Send` | Pay a BOLT12 offer (with optional quantity, payer note, routing config) | +### BOLT12 Offers and Refunds + +| RPC | Description | +|-----------------------|-------------------------------------------------------------------------| +| `Bolt12Receive` | Create a BOLT12 offer (fixed or variable amount) | +| `Bolt12Send` | Pay a BOLT12 offer (with optional quantity, payer note, routing config) | +| `Bolt12SendRefund` | Create a BOLT12 refund that this node will pay | +| `Bolt12ReceiveRefund` | Request an incoming payment for a BOLT12 refund | ### Spontaneous and Unified Send diff --git a/e2e-tests/src/lib.rs b/e2e-tests/src/lib.rs index 0dcc16e6..c63df6f0 100644 --- a/e2e-tests/src/lib.rs +++ b/e2e-tests/src/lib.rs @@ -15,14 +15,18 @@ use std::time::Duration; use corepc_node::Node; use hex_conservative::DisplayHex; -use ldk_server_client::client::LdkServerClient; +use ldk_server_client::client::{EventStream, LdkServerClient}; use ldk_server_client::ldk_server_grpc::api::{GetNodeInfoRequest, GetNodeInfoResponse}; +use ldk_server_client::ldk_server_grpc::events::event_envelope::Event; +use ldk_server_client::ldk_server_grpc::events::EventEnvelope; use ldk_server_grpc::api::{ open_channel_request, GetBalancesRequest, ListChannelsRequest, OnchainReceiveRequest, OpenChannelRequest, }; use serde_json::Value; +const EVENT_TIMEOUT: Duration = Duration::from_secs(15); + /// Wrapper around a managed bitcoind process for regtest. pub struct TestBitcoind { pub bitcoind: Node, @@ -487,6 +491,22 @@ pub async fn wait_for_file(path: &Path, timeout: Duration) { } } +/// Wait for the next event that matches the predicate. +pub async fn wait_for_event( + events: &mut EventStream, pred: impl Fn(&Event) -> bool, +) -> EventEnvelope { + tokio::time::timeout(EVENT_TIMEOUT, async { + while let Some(Ok(event)) = events.next_message().await { + if event.event.as_ref().is_some_and(&pred) { + return event; + } + } + panic!("Event stream ended without matching event"); + }) + .await + .expect("Timed out waiting for event") +} + /// Poll get_node_info until the server responds successfully. async fn wait_for_server_ready(handle: &LdkServerHandle, timeout: Duration) -> GetNodeInfoResponse { let start = std::time::Instant::now(); diff --git a/e2e-tests/tests/e2e.rs b/e2e-tests/tests/e2e.rs index 04781d62..9de61c63 100644 --- a/e2e-tests/tests/e2e.rs +++ b/e2e-tests/tests/e2e.rs @@ -13,43 +13,28 @@ use std::time::Duration; use e2e_tests::{ find_available_port, mine_and_sync, run_cli, run_cli_raw, run_cli_with_config, - setup_funded_channel, wait_for_onchain_balance, wait_for_usable_channel, LdkServerConfig, - LdkServerHandle, TestBitcoind, + setup_funded_channel, wait_for_event, wait_for_onchain_balance, wait_for_usable_channel, + LdkServerConfig, LdkServerHandle, TestBitcoind, }; use hex_conservative::{DisplayHex, FromHex}; use ldk_node::bitcoin::hashes::{sha256, Hash}; use ldk_node::lightning::ln::msgs::SocketAddress; use ldk_node::lightning::offers::offer::Offer; +use ldk_node::lightning::offers::refund::Refund; use ldk_node::lightning_invoice::Bolt11Invoice; -use ldk_server_client::client::EventStream; use ldk_server_client::ldk_server_grpc::api::{ open_channel_request, Bolt11ReceiveRequest, Bolt12ReceiveRequest, GetBalancesRequest, OnchainReceiveRequest, OpenChannelRequest, }; use ldk_server_client::ldk_server_grpc::events::event_envelope::Event; use ldk_server_client::ldk_server_grpc::events::{ - ChannelClosureInitiator, ChannelState, ChannelStateChangeReasonKind, EventEnvelope, + ChannelClosureInitiator, ChannelState, ChannelStateChangeReasonKind, }; use ldk_server_client::ldk_server_grpc::types::{ bolt11_invoice_description, Bolt11InvoiceDescription, }; use ldk_server_grpc::types::payment_kind; -const EVENT_TIMEOUT: Duration = Duration::from_secs(15); - -async fn wait_for_event(events: &mut EventStream, pred: impl Fn(&Event) -> bool) -> EventEnvelope { - tokio::time::timeout(EVENT_TIMEOUT, async { - while let Some(Ok(ev)) = events.next_message().await { - if ev.event.as_ref().is_some_and(&pred) { - return ev; - } - } - panic!("Event stream ended without matching event"); - }) - .await - .expect("Timed out waiting for event") -} - #[tokio::test] async fn test_cli_get_node_info() { let bitcoind = TestBitcoind::new(); @@ -987,6 +972,55 @@ async fn test_cli_bolt12_send() { assert!(!output["payment_id"].as_str().unwrap().is_empty()); } +#[tokio::test] +async fn test_cli_bolt12_refund() { + let bitcoind = TestBitcoind::new(); + let server_a = LdkServerHandle::start(&bitcoind).await; + let server_b = LdkServerHandle::start(&bitcoind).await; + let mut events_a = server_a.client().subscribe_events().await.unwrap(); + let mut events_b = server_b.client().subscribe_events().await.unwrap(); + setup_funded_channel(&bitcoind, &server_a, &server_b, 100_000).await; + + // Give B outbound liquidity for the refund payment. + let offer = server_b + .client() + .bolt12_receive(Bolt12ReceiveRequest { + description: "refund funding payment".to_string(), + amount_msat: Some(10_000_000), + expiry_secs: None, + quantity: None, + }) + .await + .unwrap(); + run_cli(&server_a, &["bolt12-send", &offer.offer]); + wait_for_event(&mut events_a, |e| matches!(e, Event::PaymentSuccessful(_))).await; + wait_for_event(&mut events_b, |e| matches!(e, Event::PaymentReceived(_))).await; + + let output = run_cli( + &server_b, + &["bolt12-send-refund", "5000sat", "--quantity", "1", "--payer-note", "test refund"], + ); + let refund_str = output["refund"].as_str().unwrap(); + assert!(refund_str.starts_with("lnr"), "Expected lnr prefix, got: {refund_str}"); + let refund = Refund::from_str(refund_str).unwrap(); + assert_eq!(refund.amount_msats(), 5_000_000); + assert_eq!(refund.quantity(), Some(1)); + assert_eq!(refund.payer_note().unwrap().to_string(), "test refund"); + + let output = run_cli(&server_a, &["bolt12-receive-refund", refund_str]); + let payment_hash = output["payment_hash"].as_str().unwrap(); + let event_a = wait_for_event(&mut events_a, |e| matches!(e, Event::PaymentReceived(_))).await; + let Some(Event::PaymentReceived(payment_received)) = event_a.event else { + panic!("expected PaymentReceived"); + }; + let payment = payment_received.payment.unwrap(); + let Some(payment_kind::Kind::Bolt12Refund(refund)) = payment.kind.unwrap().kind else { + panic!("expected BOLT12 refund kind"); + }; + assert_eq!(refund.hash.as_deref(), Some(payment_hash)); + wait_for_event(&mut events_b, |e| matches!(e, Event::PaymentSuccessful(_))).await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_cli_spontaneous_send() { let bitcoind = TestBitcoind::new(); diff --git a/e2e-tests/tests/mcp.rs b/e2e-tests/tests/mcp.rs index 6fe137e8..3ae00766 100644 --- a/e2e-tests/tests/mcp.rs +++ b/e2e-tests/tests/mcp.rs @@ -7,12 +7,21 @@ // You may not use this file except in accordance with one or both of these // licenses. -use e2e_tests::{LdkServerHandle, McpHandle, TestBitcoind}; +use std::str::FromStr; + +use e2e_tests::{setup_funded_channel, wait_for_event, LdkServerHandle, McpHandle, TestBitcoind}; +use ldk_node::lightning::offers::refund::Refund; use ldk_server_client::ldk_server_grpc::api::Bolt11ReceiveRequest; +use ldk_server_client::ldk_server_grpc::events::event_envelope::Event; use ldk_server_client::ldk_server_grpc::types::{ - bolt11_invoice_description, Bolt11InvoiceDescription, + bolt11_invoice_description, payment_kind, Bolt11InvoiceDescription, }; -use serde_json::json; +use serde_json::{json, Value}; + +fn tool_result_json(response: &Value) -> Value { + let text = response["result"]["content"][0]["text"].as_str().unwrap(); + serde_json::from_str(text).unwrap() +} #[tokio::test] async fn test_mcp_initialize_and_list_tools() { @@ -49,17 +58,14 @@ async fn test_mcp_live_tool_calls() { "name": "get_node_info", "arguments": {} })); - let node_info_text = node_info["result"]["content"][0]["text"].as_str().unwrap(); - let node_info_json: serde_json::Value = serde_json::from_str(node_info_text).unwrap(); + let node_info_json = tool_result_json(&node_info); assert_eq!(node_info_json["node_id"], server.node_id()); let onchain_receive = mcp.call(2, "tools/call", json!({ "name": "onchain_receive", "arguments": {} })); - let onchain_receive_text = onchain_receive["result"]["content"][0]["text"].as_str().unwrap(); - let onchain_receive_json: serde_json::Value = - serde_json::from_str(onchain_receive_text).unwrap(); + let onchain_receive_json = tool_result_json(&onchain_receive); assert!(onchain_receive_json["address"].as_str().unwrap().starts_with("bcrt1")); let invoice = server @@ -78,10 +84,63 @@ async fn test_mcp_live_tool_calls() { "name": "decode_invoice", "arguments": { "invoice": invoice.invoice } })); - let decode_invoice_text = decode_invoice["result"]["content"][0]["text"].as_str().unwrap(); - let decode_invoice_json: serde_json::Value = - serde_json::from_str(decode_invoice_text).unwrap(); + let decode_invoice_json = tool_result_json(&decode_invoice); assert_eq!(decode_invoice_json["destination"], server.node_id()); assert_eq!(decode_invoice_json["description"], "mcp decode"); assert_eq!(decode_invoice_json["amount_msat"], 50_000_000u64); } + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_mcp_bolt12_refund() { + let bitcoind = TestBitcoind::new(); + let server_a = LdkServerHandle::start(&bitcoind).await; + let server_b = LdkServerHandle::start(&bitcoind).await; + let mut events_a = server_a.client().subscribe_events().await.unwrap(); + let mut events_b = server_b.client().subscribe_events().await.unwrap(); + + setup_funded_channel(&bitcoind, &server_b, &server_a, 100_000).await; + + let mut mcp_a = McpHandle::start(&server_a); + let mut mcp_b = McpHandle::start(&server_b); + let send_refund = mcp_b.call( + 1, + "tools/call", + json!({ + "name": "bolt12_send_refund", + "arguments": { + "amount_msat": 5_000_000, + "quantity": 1, + "payer_note": "mcp refund" + } + }), + ); + let send_refund = tool_result_json(&send_refund); + let refund_str = send_refund["refund"].as_str().unwrap(); + let refund = Refund::from_str(refund_str).unwrap(); + assert_eq!(refund.amount_msats(), 5_000_000); + assert_eq!(refund.quantity(), Some(1)); + assert_eq!(refund.payer_note().unwrap().to_string(), "mcp refund"); + + let receive_refund = mcp_a.call( + 1, + "tools/call", + json!({ + "name": "bolt12_receive_refund", + "arguments": { "refund": refund_str } + }), + ); + let receive_refund = tool_result_json(&receive_refund); + let payment_hash = receive_refund["payment_hash"].as_str().unwrap(); + + let event_a = + wait_for_event(&mut events_a, |event| matches!(event, Event::PaymentReceived(_))).await; + let Some(Event::PaymentReceived(payment_received)) = event_a.event else { + panic!("expected PaymentReceived"); + }; + let payment = payment_received.payment.unwrap(); + let Some(payment_kind::Kind::Bolt12Refund(refund)) = payment.kind.unwrap().kind else { + panic!("expected BOLT12 refund kind"); + }; + assert_eq!(refund.hash.as_deref(), Some(payment_hash)); + wait_for_event(&mut events_b, |event| matches!(event, Event::PaymentSuccessful(_))).await; +} diff --git a/ldk-server-cli/src/main.rs b/ldk-server-cli/src/main.rs index 224e1d42..fb16d747 100644 --- a/ldk-server-cli/src/main.rs +++ b/ldk-server-cli/src/main.rs @@ -29,13 +29,14 @@ use ldk_server_client::ldk_server_grpc::api::{ Bolt11ReceiveRequest, Bolt11ReceiveResponse, Bolt11ReceiveVariableAmountViaJitChannelRequest, Bolt11ReceiveVariableAmountViaJitChannelResponse, Bolt11ReceiveViaJitChannelRequest, Bolt11ReceiveViaJitChannelResponse, Bolt11SendRequest, Bolt11SendResponse, - 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, + Bolt11SendUnderpayingRequest, Bolt11SendUnderpayingResponse, Bolt12ReceiveRefundRequest, + Bolt12ReceiveRefundResponse, Bolt12ReceiveRequest, Bolt12ReceiveResponse, + Bolt12SendRefundRequest, Bolt12SendRefundResponse, 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, @@ -319,6 +320,43 @@ enum Commands { )] max_channel_saturation_power_of_half: Option, }, + #[command(about = "Create a BOLT12 refund")] + Bolt12SendRefund { + #[arg(help = "Amount to refund, e.g. 50sat or 50000msat")] + amount: Amount, + #[arg(long, default_value_t = DEFAULT_EXPIRY_SECS, help = "Refund expiry time in seconds")] + expiry_secs: u32, + #[arg(short, long, help = "Number of items being refunded")] + quantity: Option, + #[arg( + short, + long, + help = "Note to include for the recipient. Will be reflected back in the invoice" + )] + payer_note: Option, + #[arg( + long, + help = "Maximum total routing fee, e.g. 50sat or 50000msat. Defaults to 1% of the payment amount + 50 sats" + )] + max_total_routing_fee: Option, + #[arg(long, help = "Maximum total CLTV delta we accept for the route (default: 1008)")] + max_total_cltv_expiry_delta: Option, + #[arg( + long, + help = "Maximum number of paths that may be used by MPP payments (default: 10)" + )] + max_path_count: Option, + #[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, + }, + #[command(about = "Request payment for a BOLT12 refund")] + Bolt12ReceiveRefund { + #[arg(help = "A BOLT12 refund from the node that will send the payment")] + refund: String, + }, #[command(about = "Send a spontaneous payment (keysend) to a node")] SpontaneousSend { #[arg(help = "The hex-encoded public key of the node to send the payment to")] @@ -872,6 +910,43 @@ async fn main() { .await, ); }, + Commands::Bolt12SendRefund { + amount, + expiry_secs, + quantity, + payer_note, + max_total_routing_fee, + max_total_cltv_expiry_delta, + max_path_count, + max_channel_saturation_power_of_half, + } => { + 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::<_, Bolt12SendRefundResponse>( + client + .bolt12_send_refund(Bolt12SendRefundRequest { + amount_msat: amount.to_msat(), + expiry_secs, + quantity, + payer_note, + route_parameters: Some(route_parameters), + }) + .await, + ); + }, + Commands::Bolt12ReceiveRefund { refund } => { + handle_response_result::<_, Bolt12ReceiveRefundResponse>( + client.bolt12_receive_refund(Bolt12ReceiveRefundRequest { refund }).await, + ); + }, Commands::SpontaneousSend { node_id, amount, diff --git a/ldk-server-client/src/client.rs b/ldk-server-client/src/client.rs index 6d0d50d0..18962c52 100644 --- a/ldk-server-client/src/client.rs +++ b/ldk-server-client/src/client.rs @@ -21,21 +21,22 @@ use ldk_server_grpc::api::{ Bolt11ReceiveRequest, Bolt11ReceiveResponse, Bolt11ReceiveVariableAmountViaJitChannelRequest, Bolt11ReceiveVariableAmountViaJitChannelResponse, Bolt11ReceiveViaJitChannelRequest, Bolt11ReceiveViaJitChannelResponse, Bolt11SendRequest, Bolt11SendResponse, - 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, + Bolt11SendUnderpayingRequest, Bolt11SendUnderpayingResponse, Bolt12ReceiveRefundRequest, + Bolt12ReceiveRefundResponse, Bolt12ReceiveRequest, Bolt12ReceiveResponse, + Bolt12SendRefundRequest, Bolt12SendRefundResponse, 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, @@ -44,15 +45,15 @@ 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, 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, + BOLT12_RECEIVE_PATH, BOLT12_RECEIVE_REFUND_PATH, BOLT12_SEND_PATH, BOLT12_SEND_REFUND_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::{ @@ -269,6 +270,20 @@ impl LdkServerClient { self.grpc_unary(&request, BOLT12_SEND_PATH).await } + /// Create a BOLT12 refund. + pub async fn bolt12_send_refund( + &self, request: Bolt12SendRefundRequest, + ) -> Result { + self.grpc_unary(&request, BOLT12_SEND_REFUND_PATH).await + } + + /// Request payment for a BOLT12 refund. + pub async fn bolt12_receive_refund( + &self, request: Bolt12ReceiveRefundRequest, + ) -> Result { + self.grpc_unary(&request, BOLT12_RECEIVE_REFUND_PATH).await + } + /// Creates a new outbound channel. pub async fn open_channel( &self, request: OpenChannelRequest, diff --git a/ldk-server-grpc/src/api.rs b/ldk-server-grpc/src/api.rs index 5187fa40..a2d44a10 100644 --- a/ldk-server-grpc/src/api.rs +++ b/ldk-server-grpc/src/api.rs @@ -492,6 +492,67 @@ pub struct Bolt12SendResponse { #[prost(string, tag = "1")] pub payment_id: ::prost::alloc::string::String, } +/// Returns a BOLT12 refund for the given amount. The refund recipient can use it to request +/// payment from this node. +/// See more: +/// - +#[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 Bolt12SendRefundRequest { + /// The amount in millisatoshis to refund. + #[prost(uint64, tag = "1")] + pub amount_msat: u64, + /// Refund expiry time in seconds. A value of zero is rejected. + #[prost(uint32, tag = "2")] + pub expiry_secs: u32, + /// If set, it represents the number of items being refunded. + #[prost(uint64, optional, tag = "3")] + pub quantity: ::core::option::Option, + /// If set, it will be seen by the recipient and reflected back in the invoice. + #[prost(string, optional, tag = "4")] + pub payer_note: ::core::option::Option<::prost::alloc::string::String>, + /// Configuration options for payment routing and pathfinding. + #[prost(message, optional, tag = "5")] + pub route_parameters: ::core::option::Option, +} +/// The response for the `Bolt12SendRefund` 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 Bolt12SendRefundResponse { + /// A BOLT12 refund that the recipient can use to request the refund payment. + #[prost(string, tag = "1")] + pub refund: ::prost::alloc::string::String, +} +/// Requests payment for a BOLT12 refund from another node. +/// See more: +/// - +#[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 Bolt12ReceiveRefundRequest { + /// A BOLT12 refund from the node that will send the payment. + #[prost(string, tag = "1")] + pub refund: ::prost::alloc::string::String, +} +/// The response for the `Bolt12ReceiveRefund` 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 Bolt12ReceiveRefundResponse { + /// The payment hash for the incoming refund payment in hex-encoded form. + #[prost(string, tag = "1")] + pub payment_hash: ::prost::alloc::string::String, +} /// Send a spontaneous payment, also known as "keysend", to a node. /// See more: #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] diff --git a/ldk-server-grpc/src/endpoints.rs b/ldk-server-grpc/src/endpoints.rs index cc4492be..e9fe3f16 100644 --- a/ldk-server-grpc/src/endpoints.rs +++ b/ldk-server-grpc/src/endpoints.rs @@ -25,6 +25,8 @@ 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 BOLT12_SEND_REFUND_PATH: &str = "Bolt12SendRefund"; +pub const BOLT12_RECEIVE_REFUND_PATH: &str = "Bolt12ReceiveRefund"; pub const OPEN_CHANNEL_PATH: &str = "OpenChannel"; pub const SPLICE_IN_PATH: &str = "SpliceIn"; pub const SPLICE_OUT_PATH: &str = "SpliceOut"; diff --git a/ldk-server-grpc/src/proto/api.proto b/ldk-server-grpc/src/proto/api.proto index 4c982a4f..ead1015b 100644 --- a/ldk-server-grpc/src/proto/api.proto +++ b/ldk-server-grpc/src/proto/api.proto @@ -379,6 +379,51 @@ message Bolt12SendResponse { string payment_id = 1; } +// Returns a BOLT12 refund for the given amount. The refund recipient can use it to request +// payment from this node. +// See more: +// - https://docs.rs/ldk-node/latest/ldk_node/payment/struct.Bolt12Payment.html#method.initiate_refund +message Bolt12SendRefundRequest { + + // The amount in millisatoshis to refund. + uint64 amount_msat = 1; + + // Refund expiry time in seconds. A value of zero is rejected. + uint32 expiry_secs = 2; + + // If set, it represents the number of items being refunded. + optional uint64 quantity = 3; + + // If set, it will be seen by the recipient and reflected back in the invoice. + optional string payer_note = 4; + + // Configuration options for payment routing and pathfinding. + optional types.RouteParametersConfig route_parameters = 5; +} + +// The response for the `Bolt12SendRefund` RPC. On failure, a gRPC error status is returned. +message Bolt12SendRefundResponse { + + // A BOLT12 refund that the recipient can use to request the refund payment. + string refund = 1; +} + +// Requests payment for a BOLT12 refund from another node. +// See more: +// - https://docs.rs/ldk-node/latest/ldk_node/payment/struct.Bolt12Payment.html#method.request_refund_payment +message Bolt12ReceiveRefundRequest { + + // A BOLT12 refund from the node that will send the payment. + string refund = 1; +} + +// The response for the `Bolt12ReceiveRefund` RPC. On failure, a gRPC error status is returned. +message Bolt12ReceiveRefundResponse { + + // The payment hash for the incoming refund payment in hex-encoded form. + string payment_hash = 1; +} + // Send a spontaneous payment, also known as "keysend", to a node. // See more: https://docs.rs/ldk-node/latest/ldk_node/payment/struct.SpontaneousPayment.html#method.send message SpontaneousSendRequest { @@ -973,6 +1018,10 @@ service LightningNode { rpc Bolt12Receive(Bolt12ReceiveRequest) returns (Bolt12ReceiveResponse); // Send a payment for a BOLT12 offer. rpc Bolt12Send(Bolt12SendRequest) returns (Bolt12SendResponse); + // Return a BOLT12 refund that this node will pay. + rpc Bolt12SendRefund(Bolt12SendRefundRequest) returns (Bolt12SendRefundResponse); + // Request an incoming payment for a BOLT12 refund. + rpc Bolt12ReceiveRefund(Bolt12ReceiveRefundRequest) returns (Bolt12ReceiveRefundResponse); // Send a spontaneous payment (keysend). rpc SpontaneousSend(SpontaneousSendRequest) returns (SpontaneousSendResponse); // Create a new outbound channel. diff --git a/ldk-server-mcp/src/tools/handlers.rs b/ldk-server-mcp/src/tools/handlers.rs index 7d3a42c2..5c9cf16a 100644 --- a/ldk-server-mcp/src/tools/handlers.rs +++ b/ldk-server-mcp/src/tools/handlers.rs @@ -13,11 +13,11 @@ use ldk_server_client::ldk_server_grpc::api::{ Bolt11ClaimForHashRequest, Bolt11FailForHashRequest, Bolt11ReceiveForHashRequest, Bolt11ReceiveRequest, Bolt11ReceiveVariableAmountViaJitChannelRequest, Bolt11ReceiveViaJitChannelRequest, Bolt11SendRequest, Bolt11SendUnderpayingRequest, - Bolt12ReceiveRequest, Bolt12SendRequest, CloseChannelRequest, ConnectPeerRequest, - DecodeInvoiceRequest, DecodeOfferRequest, DisconnectPeerRequest, - ExportPathfindingScoresRequest, ForceCloseChannelRequest, GetBalancesRequest, - GetNodeInfoRequest, GetPaymentDetailsRequest, GraphGetChannelRequest, GraphGetNodeRequest, - GraphListChannelsRequest, GraphListNodesRequest, ListChannelsRequest, + Bolt12ReceiveRefundRequest, Bolt12ReceiveRequest, Bolt12SendRefundRequest, Bolt12SendRequest, + CloseChannelRequest, ConnectPeerRequest, DecodeInvoiceRequest, DecodeOfferRequest, + DisconnectPeerRequest, ExportPathfindingScoresRequest, ForceCloseChannelRequest, + GetBalancesRequest, GetNodeInfoRequest, GetPaymentDetailsRequest, GraphGetChannelRequest, + GraphGetNodeRequest, GraphListChannelsRequest, GraphListNodesRequest, ListChannelsRequest, ListForwardedPaymentsRequest, ListPaymentsRequest, ListPeersRequest, OnchainReceiveRequest, OnchainSendRequest, OpenChannelRequest, SignMessageRequest, SpliceInRequest, SpliceOutRequest, SpontaneousSendRequest, UnifiedSendRequest, UpdateChannelConfigRequest, VerifySignatureRequest, @@ -247,6 +247,28 @@ pub async fn handle_bolt12_send(client: &LdkServerClient, args: Value) -> Result serialize_response(response) } +pub async fn handle_bolt12_send_refund( + client: &LdkServerClient, args: Value, +) -> Result { + let mut request: Bolt12SendRefundRequest = + parse_request_with_route_parameters(args, |request: &mut Bolt12SendRefundRequest| { + &mut request.route_parameters + })?; + if request.expiry_secs == 0 { + request.expiry_secs = DEFAULT_EXPIRY_SECS; + } + let response = client.bolt12_send_refund(request).await.map_err(McpError::from)?; + serialize_response(response) +} + +pub async fn handle_bolt12_receive_refund( + client: &LdkServerClient, args: Value, +) -> Result { + let request: Bolt12ReceiveRefundRequest = parse_request(args)?; + let response = client.bolt12_receive_refund(request).await.map_err(McpError::from)?; + serialize_response(response) +} + pub async fn handle_spontaneous_send( client: &LdkServerClient, args: Value, ) -> Result { diff --git a/ldk-server-mcp/src/tools/mod.rs b/ldk-server-mcp/src/tools/mod.rs index b86b159d..a60d6734 100644 --- a/ldk-server-mcp/src/tools/mod.rs +++ b/ldk-server-mcp/src/tools/mod.rs @@ -157,6 +157,18 @@ pub fn build_tool_registry() -> ToolRegistry { schema::bolt12_send_schema, |client, args| Box::pin(handlers::handle_bolt12_send(client, args)), ), + tool_spec( + "bolt12_send_refund", + "Create a BOLT12 refund that this node will pay", + schema::bolt12_send_refund_schema, + |client, args| Box::pin(handlers::handle_bolt12_send_refund(client, args)), + ), + tool_spec( + "bolt12_receive_refund", + "Request an incoming payment for a BOLT12 refund", + schema::bolt12_receive_refund_schema, + |client, args| Box::pin(handlers::handle_bolt12_receive_refund(client, args)), + ), tool_spec( "spontaneous_send", "Send a spontaneous (keysend) payment to a Lightning node", diff --git a/ldk-server-mcp/src/tools/schema.rs b/ldk-server-mcp/src/tools/schema.rs index e8aed08d..e2fb9ec4 100644 --- a/ldk-server-mcp/src/tools/schema.rs +++ b/ldk-server-mcp/src/tools/schema.rs @@ -379,6 +379,45 @@ pub fn bolt12_send_schema() -> Value { }) } +pub fn bolt12_send_refund_schema() -> Value { + json!({ + "type": "object", + "properties": { + "amount_msat": { + "type": "integer", + "description": "Amount in millisatoshis to refund" + }, + "expiry_secs": { + "type": "integer", + "description": "Refund expiry time in seconds (defaults to 86400 if omitted or 0)" + }, + "quantity": { + "type": "integer", + "description": "Number of items being refunded" + }, + "payer_note": { + "type": "string", + "description": "Note to include for the refund recipient" + }, + "route_parameters": route_parameters_config_schema() + }, + "required": ["amount_msat"] + }) +} + +pub fn bolt12_receive_refund_schema() -> Value { + json!({ + "type": "object", + "properties": { + "refund": { + "type": "string", + "description": "A BOLT12 refund from the node that will send the payment" + } + }, + "required": ["refund"] + }) +} + pub fn spontaneous_send_schema() -> Value { json!({ "type": "object", diff --git a/ldk-server-mcp/tests/integration.rs b/ldk-server-mcp/tests/integration.rs index 5b636285..fb229f23 100644 --- a/ldk-server-mcp/tests/integration.rs +++ b/ldk-server-mcp/tests/integration.rs @@ -11,7 +11,7 @@ use std::io::{BufRead, BufReader, Write}; use serde_json::{json, Value}; -const NUM_TOOLS: usize = 38; +const NUM_TOOLS: usize = 40; const EXPECTED_TOOLS: [&str; NUM_TOOLS] = [ "bolt11_claim_for_hash", "bolt11_fail_for_hash", @@ -22,7 +22,9 @@ const EXPECTED_TOOLS: [&str; NUM_TOOLS] = [ "bolt11_send", "bolt11_send_underpaying", "bolt12_receive", + "bolt12_receive_refund", "bolt12_send", + "bolt12_send_refund", "close_channel", "connect_peer", "decode_invoice", @@ -332,6 +334,16 @@ fn test_bolt11_send_underpaying_unreachable() { ); } +#[test] +fn test_bolt12_send_refund_unreachable() { + assert_unreachable_tool("bolt12_send_refund", json!({ "amount_msat": 1000 })); +} + +#[test] +fn test_bolt12_receive_refund_unreachable() { + assert_unreachable_tool("bolt12_receive_refund", json!({ "refund": "lnr1example" })); +} + #[test] fn test_decode_offer_unreachable() { assert_unreachable_tool("decode_offer", json!({ "offer": "lno1example" })); diff --git a/ldk-server/src/api/bolt12_refund.rs b/ldk-server/src/api/bolt12_refund.rs new file mode 100644 index 00000000..6d092902 --- /dev/null +++ b/ldk-server/src/api/bolt12_refund.rs @@ -0,0 +1,71 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +use std::str::FromStr; +use std::sync::Arc; + +use ldk_node::lightning::offers::refund::Refund; +use ldk_server_grpc::api::{ + Bolt12ReceiveRefundRequest, Bolt12ReceiveRefundResponse, Bolt12SendRefundRequest, + Bolt12SendRefundResponse, +}; + +use crate::api::build_route_parameters_config_from_proto; +use crate::api::error::{LdkServerError, LdkServerErrorCode}; +use crate::service::Context; + +fn validate_refund_expiry(expiry_secs: u32) -> Result<(), LdkServerError> { + if expiry_secs == 0 { + return Err(LdkServerError::new( + LdkServerErrorCode::InvalidRequestError, + "Refund expiry must be greater than zero seconds", + )); + } + Ok(()) +} + +pub(crate) async fn handle_bolt12_send_refund_request( + context: Arc, request: Bolt12SendRefundRequest, +) -> Result { + validate_refund_expiry(request.expiry_secs)?; + let route_parameters = build_route_parameters_config_from_proto(request.route_parameters)?; + let refund = context.node.bolt12_payment().initiate_refund( + request.amount_msat, + request.expiry_secs, + request.quantity, + request.payer_note, + route_parameters, + )?; + + Ok(Bolt12SendRefundResponse { refund: refund.to_string() }) +} + +pub(crate) async fn handle_bolt12_receive_refund_request( + context: Arc, request: Bolt12ReceiveRefundRequest, +) -> Result { + let refund = + Refund::from_str(&request.refund).map_err(|_| ldk_node::NodeError::InvalidRefund)?; + let invoice = context.node.bolt12_payment().request_refund_payment(&refund)?; + let payment_hash = invoice.payment_hash().to_string(); + + Ok(Bolt12ReceiveRefundResponse { payment_hash }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn refund_expiry_must_be_positive() { + let error = validate_refund_expiry(0).unwrap_err(); + assert_eq!(error.error_code, LdkServerErrorCode::InvalidRequestError); + assert_eq!(error.message, "Refund expiry must be greater than zero seconds"); + assert!(validate_refund_expiry(1).is_ok()); + } +} diff --git a/ldk-server/src/api/mod.rs b/ldk-server/src/api/mod.rs index 15ff0f13..15f7ff06 100644 --- a/ldk-server/src/api/mod.rs +++ b/ldk-server/src/api/mod.rs @@ -23,6 +23,7 @@ pub(crate) mod bolt11_receive_for_hash; pub(crate) mod bolt11_receive_via_jit_channel; pub(crate) mod bolt11_send; pub(crate) mod bolt12_receive; +pub(crate) mod bolt12_refund; pub(crate) mod bolt12_send; pub(crate) mod close_channel; pub(crate) mod connect_peer; diff --git a/ldk-server/src/service.rs b/ldk-server/src/service.rs index 83c6fb7e..ed14458e 100644 --- a/ldk-server/src/service.rs +++ b/ldk-server/src/service.rs @@ -22,15 +22,15 @@ 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, 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, 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, + BOLT12_RECEIVE_PATH, BOLT12_RECEIVE_REFUND_PATH, BOLT12_SEND_PATH, BOLT12_SEND_REFUND_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, + 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::{ @@ -52,6 +52,9 @@ use crate::api::bolt11_receive_via_jit_channel::{ }; use crate::api::bolt11_send::{handle_bolt11_send_request, handle_bolt11_send_underpaying_request}; use crate::api::bolt12_receive::handle_bolt12_receive_request; +use crate::api::bolt12_refund::{ + handle_bolt12_receive_refund_request, handle_bolt12_send_refund_request, +}; use crate::api::bolt12_send::handle_bolt12_send_request; use crate::api::close_channel::{handle_close_channel_request, handle_force_close_channel_request}; use crate::api::connect_peer::handle_connect_peer; @@ -329,6 +332,13 @@ impl Service> for NodeService { BOLT12_SEND_PATH => { handle_grpc_unary(context, body_bytes, handle_bolt12_send_request).await }, + BOLT12_SEND_REFUND_PATH => { + handle_grpc_unary(context, body_bytes, handle_bolt12_send_refund_request).await + }, + BOLT12_RECEIVE_REFUND_PATH => { + handle_grpc_unary(context, body_bytes, handle_bolt12_receive_refund_request) + .await + }, OPEN_CHANNEL_PATH => { handle_grpc_unary(context, body_bytes, handle_open_channel).await },