Skip to content

Commit e9f07c9

Browse files
tankyleogustimothy55
authored andcommitted
Rework on-chain send amounts
Replace the send-all boolean with the typed exact-or-all protobuf oneof used by channel funding. Accept "all" as the positional CLI and MCP amount while preserving exact sends. AI-assisted-by: OpenAI Codex
1 parent fba6efd commit e9f07c9

7 files changed

Lines changed: 70 additions & 58 deletions

File tree

e2e-tests/tests/e2e.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ async fn test_cli_onchain_send_all() {
376376
let balances_before = server.client().get_balances(GetBalancesRequest {}).await.unwrap();
377377

378378
let address = bitcoind.bitcoind.client.new_address().unwrap().to_string();
379-
let output = run_cli(&server, &["onchain-send", &address, "--send-all", "true"]);
379+
let output = run_cli(&server, &["onchain-send", &address, "all"]);
380380
assert!(!output["txid"].as_str().unwrap().is_empty());
381381

382382
mine_and_sync(&bitcoind, &[&server], 6).await;

ldk-server-cli/src/main.rs

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ use ldk_server_client::error::LdkServerErrorCode::{
2323
AuthError, InternalError, InternalServerError, InvalidRequestError, LightningError,
2424
};
2525
use ldk_server_client::ldk_server_grpc::api::{
26-
open_channel_request, splice_in_request, AllFunds, Bolt11ClaimForHashRequest,
27-
Bolt11ClaimForHashResponse, Bolt11FailForHashRequest, Bolt11FailForHashResponse,
28-
Bolt11ReceiveForHashRequest, Bolt11ReceiveForHashResponse, Bolt11ReceiveRequest,
29-
Bolt11ReceiveResponse, Bolt11ReceiveVariableAmountViaJitChannelRequest,
26+
onchain_send_request, open_channel_request, splice_in_request, AllFunds,
27+
Bolt11ClaimForHashRequest, Bolt11ClaimForHashResponse, Bolt11FailForHashRequest,
28+
Bolt11FailForHashResponse, Bolt11ReceiveForHashRequest, Bolt11ReceiveForHashResponse,
29+
Bolt11ReceiveRequest, Bolt11ReceiveResponse, Bolt11ReceiveVariableAmountViaJitChannelRequest,
3030
Bolt11ReceiveVariableAmountViaJitChannelResponse, Bolt11ReceiveViaJitChannelRequest,
3131
Bolt11ReceiveViaJitChannelResponse, Bolt11SendRequest, Bolt11SendResponse,
3232
Bolt11SendUnderpayingRequest, Bolt11SendUnderpayingResponse, Bolt12ReceiveRequest,
@@ -116,14 +116,9 @@ enum Commands {
116116
#[arg(help = "The address to send coins to")]
117117
address: String,
118118
#[arg(
119-
help = "The amount to send, e.g. 50sat or 50000msat, must be a whole sat amount, cannot send msats on-chain. Will respect any on-chain reserve needed for anchor channels"
119+
help = "The amount to send, e.g. 50sat or 50000msat, or 'all' to use all available on-chain funds. Exact amounts must be a whole sat amount. Will respect any on-chain reserve needed for anchor channels"
120120
)]
121-
amount: Option<Amount>,
122-
#[arg(
123-
long,
124-
help = "Send all available balance to the address while retaining on-chain reserves for anchor channels"
125-
)]
126-
send_all: Option<bool>,
121+
amount: AmountOrAll,
127122
#[arg(
128123
long,
129124
help = "Fee rate in satoshis per virtual byte. If not set, a reasonable estimate will be used"
@@ -661,15 +656,17 @@ async fn main() {
661656
client.onchain_receive(OnchainReceiveRequest {}).await,
662657
);
663658
},
664-
Commands::OnchainSend { address, amount, send_all, fee_rate_sat_per_vb } => {
665-
let amount_sats = amount.map(|a| a.to_sat().unwrap_or_else(|e| handle_error_msg(e)));
659+
Commands::OnchainSend { address, amount, fee_rate_sat_per_vb } => {
660+
let amount = match amount.to_sat().unwrap_or_else(|e| handle_error_msg(e)) {
661+
Some(amount_sats) => onchain_send_request::Amount::AmountSats(amount_sats),
662+
None => onchain_send_request::Amount::AllFunds(AllFunds {}),
663+
};
666664
handle_response_result::<_, OnchainSendResponse>(
667665
client
668666
.onchain_send(OnchainSendRequest {
669667
address,
670-
amount_sats,
671-
send_all,
672668
fee_rate_sat_per_vb,
669+
amount: Some(amount),
673670
})
674671
.await,
675672
);

ldk-server-grpc/src/api.rs

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ pub struct OnchainReceiveResponse {
109109
pub address: ::prost::alloc::string::String,
110110
}
111111
/// Send an on-chain payment to the given address.
112+
/// See more: <https://docs.rs/ldk-node/latest/ldk_node/payment/struct.OnchainPayment.html#method.send_to_address>
112113
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
113114
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
114115
#[cfg_attr(feature = "serde", serde(default))]
@@ -118,23 +119,29 @@ pub struct OnchainSendRequest {
118119
/// The address to send coins to.
119120
#[prost(string, tag = "1")]
120121
pub address: ::prost::alloc::string::String,
121-
/// The amount in satoshis to send.
122-
/// While sending the specified amount, we will respect any on-chain reserve we need to keep,
123-
/// i.e., won't allow to cut into `total_anchor_channels_reserve_sats`.
124-
/// See more: <https://docs.rs/ldk-node/latest/ldk_node/payment/struct.OnchainPayment.html#method.send_to_address>
125-
#[prost(uint64, optional, tag = "2")]
126-
pub amount_sats: ::core::option::Option<u64>,
127-
/// If set, the amount_sats field should be unset.
128-
/// It indicates that the node will send all available balance to the specified address.
129-
///
130-
/// Any on-chain reserves needed for Anchor channels will be retained.
131-
/// See more: <https://docs.rs/ldk-node/latest/ldk_node/payment/struct.OnchainPayment.html#method.send_all_to_address>
132-
#[prost(bool, optional, tag = "3")]
133-
pub send_all: ::core::option::Option<bool>,
134122
/// If `fee_rate_sat_per_vb` is set it will be used on the resulting transaction. Otherwise we'll retrieve
135123
/// a reasonable estimate from BitcoinD.
136124
#[prost(uint64, optional, tag = "4")]
137125
pub fee_rate_sat_per_vb: ::core::option::Option<u64>,
126+
/// Required. The amount to send.
127+
#[prost(oneof = "onchain_send_request::Amount", tags = "2, 3")]
128+
pub amount: ::core::option::Option<onchain_send_request::Amount>,
129+
}
130+
/// Nested message and enum types in `OnchainSendRequest`.
131+
pub mod onchain_send_request {
132+
/// Required. The amount to send.
133+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
134+
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
135+
#[allow(clippy::derive_partial_eq_without_eq)]
136+
#[derive(Clone, PartialEq, ::prost::Oneof)]
137+
pub enum Amount {
138+
/// Send the given amount of satoshis while retaining any required Anchor channel reserves.
139+
#[prost(uint64, tag = "2")]
140+
AmountSats(u64),
141+
/// Send all available on-chain funds, minus fees and any required Anchor channel reserves.
142+
#[prost(message, tag = "3")]
143+
AllFunds(super::AllFunds),
144+
}
138145
}
139146
/// The response for the `OnchainSend` RPC. On failure, a gRPC error status is returned.
140147
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]

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

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -91,23 +91,20 @@ message OnchainReceiveResponse {
9191
}
9292

9393
// Send an on-chain payment to the given address.
94+
// See more: https://docs.rs/ldk-node/latest/ldk_node/payment/struct.OnchainPayment.html#method.send_to_address
9495
message OnchainSendRequest {
9596

9697
// The address to send coins to.
9798
string address = 1;
9899

99-
// The amount in satoshis to send.
100-
// While sending the specified amount, we will respect any on-chain reserve we need to keep,
101-
// i.e., won't allow to cut into `total_anchor_channels_reserve_sats`.
102-
// See more: https://docs.rs/ldk-node/latest/ldk_node/payment/struct.OnchainPayment.html#method.send_to_address
103-
optional uint64 amount_sats = 2;
100+
// Required. The amount to send.
101+
oneof amount {
102+
// Send the given amount of satoshis while retaining any required Anchor channel reserves.
103+
uint64 amount_sats = 2;
104104

105-
// If set, the amount_sats field should be unset.
106-
// It indicates that the node will send all available balance to the specified address.
107-
//
108-
// Any on-chain reserves needed for Anchor channels will be retained.
109-
// See more: https://docs.rs/ldk-node/latest/ldk_node/payment/struct.OnchainPayment.html#method.send_all_to_address
110-
optional bool send_all = 3;
105+
// Send all available on-chain funds, minus fees and any required Anchor channel reserves.
106+
AllFunds all_funds = 3;
107+
}
111108

112109
// If `fee_rate_sat_per_vb` is set it will be used on the resulting transaction. Otherwise we'll retrieve
113110
// a reasonable estimate from BitcoinD.

ldk-server-mcp/src/tools/handlers.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ pub async fn handle_onchain_receive(
142142
}
143143

144144
pub async fn handle_onchain_send(client: &LdkServerClient, args: Value) -> Result<Value, McpError> {
145-
let request: OnchainSendRequest = parse_request(args)?;
145+
let request: OnchainSendRequest = parse_request_with_amount(args, "amount_sats")?;
146146
let response = client.onchain_send(request).await.map_err(McpError::from)?;
147147
serialize_response(response)
148148
}
@@ -449,7 +449,9 @@ pub async fn handle_graph_get_node(
449449

450450
#[cfg(test)]
451451
mod tests {
452-
use ldk_server_client::ldk_server_grpc::api::{open_channel_request, splice_in_request};
452+
use ldk_server_client::ldk_server_grpc::api::{
453+
onchain_send_request, open_channel_request, splice_in_request,
454+
};
453455

454456
use super::*;
455457

@@ -488,6 +490,20 @@ mod tests {
488490
));
489491
}
490492

493+
#[test]
494+
fn parse_request_with_amount_populates_onchain_oneof() {
495+
let request: OnchainSendRequest = parse_request_with_amount(
496+
json!({
497+
"address": "bc1qexample",
498+
"amount_sats": "all"
499+
}),
500+
"amount_sats",
501+
)
502+
.unwrap();
503+
504+
assert!(matches!(request.amount, Some(onchain_send_request::Amount::AllFunds(_))));
505+
}
506+
491507
#[test]
492508
fn parse_request_with_route_parameters_fills_missing_defaults() {
493509
let request: Bolt11SendRequest = parse_request_with_route_parameters(

ldk-server-mcp/src/tools/schema.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -163,19 +163,18 @@ pub fn onchain_send_schema() -> Value {
163163
"description": "The Bitcoin address to send coins to"
164164
},
165165
"amount_sats": {
166-
"type": "integer",
167-
"description": "The amount in satoshis to send. Respects on-chain reserve for anchor channels"
168-
},
169-
"send_all": {
170-
"type": "boolean",
171-
"description": "If true, send all available balance while retaining on-chain reserves for anchor channels (amount_sats must be unset)"
166+
"oneOf": [
167+
{"type": "integer"},
168+
{"type": "string", "const": "all"}
169+
],
170+
"description": "The amount in satoshis to send, or 'all' to use all available on-chain funds. Respects on-chain reserve for anchor channels"
172171
},
173172
"fee_rate_sat_per_vb": {
174173
"type": "integer",
175174
"description": "Fee rate in satoshis per virtual byte. If not set, a reasonable estimate will be used"
176175
}
177176
},
178-
"required": ["address"]
177+
"required": ["address", "amount_sats"]
179178
})
180179
}
181180

ldk-server/src/api/onchain_send.rs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ use std::str::FromStr;
1111
use std::sync::Arc;
1212

1313
use ldk_node::bitcoin::{Address, FeeRate};
14+
use ldk_server_grpc::api::onchain_send_request::Amount;
1415
use ldk_server_grpc::api::{OnchainSendRequest, OnchainSendResponse};
1516

1617
use crate::api::error::LdkServerError;
1718
use crate::api::error::LdkServerErrorCode::InvalidRequestError;
19+
use crate::api::require_amount;
1820
use crate::service::Context;
1921

2022
pub(crate) async fn handle_onchain_send_request(
@@ -31,19 +33,13 @@ pub(crate) async fn handle_onchain_send_request(
3133
})?;
3234

3335
let fee_rate = request.fee_rate_sat_per_vb.and_then(FeeRate::from_sat_per_vb);
34-
let txid = match (request.amount_sats, request.send_all) {
35-
(Some(amount_sats), None) => {
36+
let txid = match require_amount(request.amount)? {
37+
Amount::AmountSats(amount_sats) => {
3638
context.node.onchain_payment().send_to_address(&address, amount_sats, fee_rate)?
3739
},
38-
(None, Some(true)) => {
40+
Amount::AllFunds(_) => {
3941
context.node.onchain_payment().send_all_to_address(&address, true, fee_rate)?
4042
},
41-
_ => {
42-
return Err(LdkServerError::new(
43-
InvalidRequestError,
44-
"Must specify either `send_all` or `amount_sats`, but not both or neither",
45-
))
46-
},
4743
};
4844
let response = OnchainSendResponse { txid: txid.to_string() };
4945
Ok(response)

0 commit comments

Comments
 (0)