Skip to content

Commit c417e77

Browse files
committed
Expose with-all channel funding variants
Accept "all" in the CLI and MCP funding amount fields for channel opening and splice-in. Represent each choice in a typed protobuf oneof. Dispatch announced, unannounced, and zero-reserve channel opens and splice-in to the corresponding ldk-node with-all methods. AI-assisted-by: OpenAI Codex
1 parent 1af5168 commit c417e77

11 files changed

Lines changed: 324 additions & 76 deletions

File tree

e2e-tests/src/lib.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ use hex_conservative::DisplayHex;
1818
use ldk_server_client::client::LdkServerClient;
1919
use ldk_server_client::ldk_server_grpc::api::{GetNodeInfoRequest, GetNodeInfoResponse};
2020
use ldk_server_grpc::api::{
21-
GetBalancesRequest, ListChannelsRequest, OnchainReceiveRequest, OpenChannelRequest,
21+
open_channel_request, GetBalancesRequest, ListChannelsRequest, OnchainReceiveRequest,
22+
OpenChannelRequest,
2223
};
2324
use serde_json::Value;
2425

@@ -737,7 +738,9 @@ pub async fn setup_funded_channel(
737738
.open_channel(OpenChannelRequest {
738739
node_pubkey: server_b.node_id().to_string(),
739740
address: format!("127.0.0.1:{}", server_b.p2p_port),
740-
channel_amount_sats,
741+
amount: Some(open_channel_request::Amount::ChannelAmountSats(
742+
channel_amount_sats,
743+
)),
741744
push_to_counterparty_msat: None,
742745
channel_config: None,
743746
announce_channel: true,

e2e-tests/tests/e2e.rs

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ use ldk_node::lightning::offers::offer::Offer;
2323
use ldk_node::lightning_invoice::Bolt11Invoice;
2424
use ldk_server_client::client::EventStream;
2525
use ldk_server_client::ldk_server_grpc::api::{
26-
Bolt11ReceiveRequest, Bolt12ReceiveRequest, GetBalancesRequest, OnchainReceiveRequest,
27-
OpenChannelRequest,
26+
open_channel_request, Bolt11ReceiveRequest, Bolt12ReceiveRequest, GetBalancesRequest,
27+
OnchainReceiveRequest, OpenChannelRequest,
2828
};
2929
use ldk_server_client::ldk_server_grpc::events::event_envelope::Event;
3030
use ldk_server_client::ldk_server_grpc::events::{
@@ -435,8 +435,7 @@ async fn test_cli_list_peers() {
435435

436436
// === CLI tests: Group 4 — Two-node with channel ===
437437

438-
#[tokio::test]
439-
async fn test_cli_open_channel() {
438+
async fn open_channel_via_cli(channel_amount: &str) {
440439
let bitcoind = TestBitcoind::new();
441440
let server_a = LdkServerHandle::start(&bitcoind).await;
442441
let server_b = LdkServerHandle::start(&bitcoind).await;
@@ -454,11 +453,27 @@ async fn test_cli_open_channel() {
454453
let addr = format!("127.0.0.1:{}", server_b.p2p_port);
455454
let output = run_cli(
456455
&server_a,
457-
&["open-channel", server_b.node_id(), &addr, "100000sat", "--announce-channel"],
456+
&[
457+
"open-channel",
458+
server_b.node_id(),
459+
&addr,
460+
channel_amount,
461+
"--announce-channel",
462+
],
458463
);
459464
assert!(!output["user_channel_id"].as_str().unwrap().is_empty());
460465
}
461466

467+
#[tokio::test]
468+
async fn test_cli_open_channel() {
469+
open_channel_via_cli("100000sat").await;
470+
}
471+
472+
#[tokio::test]
473+
async fn test_cli_open_channel_with_all() {
474+
open_channel_via_cli("all").await;
475+
}
476+
462477
#[tokio::test]
463478
async fn test_subscribe_events_channel_state_lifecycle_pending_ready_closed() {
464479
let bitcoind = TestBitcoind::new();
@@ -481,7 +496,9 @@ async fn test_subscribe_events_channel_state_lifecycle_pending_ready_closed() {
481496
.open_channel(OpenChannelRequest {
482497
node_pubkey: server_b.node_id().to_string(),
483498
address: format!("127.0.0.1:{}", server_b.p2p_port),
484-
channel_amount_sats: 100_000,
499+
amount: Some(open_channel_request::Amount::ChannelAmountSats(
500+
100_000,
501+
)),
485502
push_to_counterparty_msat: None,
486503
channel_config: None,
487504
announce_channel: true,
@@ -645,7 +662,9 @@ async fn test_subscribe_events_channel_state_lifecycle_pending_ready_force_close
645662
.open_channel(OpenChannelRequest {
646663
node_pubkey: server_b.node_id().to_string(),
647664
address: format!("127.0.0.1:{}", server_b.p2p_port),
648-
channel_amount_sats: 100_000,
665+
amount: Some(open_channel_request::Amount::ChannelAmountSats(
666+
100_000,
667+
)),
649668
push_to_counterparty_msat: None,
650669
channel_config: None,
651670
announce_channel: true,
@@ -1119,18 +1138,29 @@ async fn test_cli_force_close_channel() {
11191138
assert!(channels_output["channels"].as_array().unwrap().is_empty());
11201139
}
11211140

1122-
#[tokio::test]
1123-
async fn test_cli_splice_in() {
1141+
async fn splice_in_via_cli(splice_amount: &str) {
11241142
let bitcoind = TestBitcoind::new();
11251143
let server_a = LdkServerHandle::start(&bitcoind).await;
11261144
let server_b = LdkServerHandle::start(&bitcoind).await;
11271145
let user_channel_id = setup_funded_channel(&bitcoind, &server_a, &server_b, 100_000).await;
11281146

1129-
let output =
1130-
run_cli(&server_a, &["splice-in", &user_channel_id, server_b.node_id(), "50000sat"]);
1147+
let output = run_cli(
1148+
&server_a,
1149+
&["splice-in", &user_channel_id, server_b.node_id(), splice_amount],
1150+
);
11311151
assert!(output.is_object());
11321152
}
11331153

1154+
#[tokio::test]
1155+
async fn test_cli_splice_in() {
1156+
splice_in_via_cli("50000sat").await;
1157+
}
1158+
1159+
#[tokio::test]
1160+
async fn test_cli_splice_in_with_all() {
1161+
splice_in_via_cli("all").await;
1162+
}
1163+
11341164
#[tokio::test]
11351165
async fn test_cli_splice_out() {
11361166
let bitcoind = TestBitcoind::new();

ldk-server-cli/src/main.rs

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +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-
Bolt11ClaimForHashRequest, Bolt11ClaimForHashResponse, Bolt11FailForHashRequest,
27-
Bolt11FailForHashResponse, Bolt11ReceiveForHashRequest, Bolt11ReceiveForHashResponse,
28-
Bolt11ReceiveRequest, Bolt11ReceiveResponse, Bolt11ReceiveVariableAmountViaJitChannelRequest,
26+
open_channel_request, splice_in_request, AllFunds, Bolt11ClaimForHashRequest,
27+
Bolt11ClaimForHashResponse, Bolt11FailForHashRequest, Bolt11FailForHashResponse,
28+
Bolt11ReceiveForHashRequest, Bolt11ReceiveForHashResponse, Bolt11ReceiveRequest,
29+
Bolt11ReceiveResponse, Bolt11ReceiveVariableAmountViaJitChannelRequest,
2930
Bolt11ReceiveVariableAmountViaJitChannelResponse, Bolt11ReceiveViaJitChannelRequest,
3031
Bolt11ReceiveViaJitChannelResponse, Bolt11SendRequest, Bolt11SendResponse,
3132
Bolt11SendUnderpayingRequest, Bolt11SendUnderpayingResponse, Bolt12ReceiveRequest,
@@ -56,8 +57,8 @@ use ldk_server_client::{
5657
use serde::Serialize;
5758
use serde_json::{json, Value};
5859
use types::{
59-
Amount, CliListForwardedPaymentsResponse, CliListPaymentsResponse, CliPaginatedResponse,
60-
Preimage,
60+
Amount, AmountOrAll, CliListForwardedPaymentsResponse, CliListPaymentsResponse,
61+
CliPaginatedResponse, Preimage,
6162
};
6263

6364
mod types;
@@ -419,9 +420,9 @@ enum Commands {
419420
)]
420421
address: String,
421422
#[arg(
422-
help = "The amount to commit to the channel, e.g. 100sat or 100000msat, must be a whole sat amount, cannot send msats on-chain."
423+
help = "The amount to commit to the channel, e.g. 100sat or 100000msat, or 'all' to use all available on-chain funds. Exact amounts must be a whole sat amount."
423424
)]
424-
channel_amount: Amount,
425+
channel_amount: AmountOrAll,
425426
#[arg(long, help = "Amount to push to the remote side, e.g. 50sat or 50000msat")]
426427
push_to_counterparty: Option<Amount>,
427428
#[arg(long, help = "Whether the channel should be public")]
@@ -457,9 +458,9 @@ enum Commands {
457458
#[arg(help = "The hex-encoded public key of the channel's counterparty node")]
458459
counterparty_node_id: String,
459460
#[arg(
460-
help = "The amount to splice into the channel, e.g. 50sat or 50000msat, must be a whole sat amount, cannot send msats on-chain."
461+
help = "The amount to splice into the channel, e.g. 50sat or 50000msat, or 'all' to use all available on-chain funds. Exact amounts must be a whole sat amount."
461462
)]
462-
splice_amount: Amount,
463+
splice_amount: AmountOrAll,
463464
},
464465
#[command(about = "Decrease the channel balance by the given amount")]
465466
SpliceOut {
@@ -983,8 +984,10 @@ async fn main() {
983984
forwarding_fee_base_msat,
984985
cltv_expiry_delta,
985986
} => {
986-
let channel_amount_sats =
987-
channel_amount.to_sat().unwrap_or_else(|e| handle_error_msg(e));
987+
let amount = match channel_amount.to_sat().unwrap_or_else(|e| handle_error_msg(e)) {
988+
Some(amount_sats) => open_channel_request::Amount::ChannelAmountSats(amount_sats),
989+
None => open_channel_request::Amount::AllFunds(AllFunds {}),
990+
};
988991
let push_to_counterparty_msat = push_to_counterparty.map(|a| a.to_msat());
989992
let channel_config = build_open_channel_config(
990993
forwarding_fee_proportional_millionths,
@@ -1003,7 +1006,7 @@ async fn main() {
10031006
.open_channel(OpenChannelRequest {
10041007
node_pubkey,
10051008
address,
1006-
channel_amount_sats,
1009+
amount: Some(amount),
10071010
push_to_counterparty_msat,
10081011
channel_config,
10091012
announce_channel,
@@ -1013,13 +1016,16 @@ async fn main() {
10131016
);
10141017
},
10151018
Commands::SpliceIn { user_channel_id, counterparty_node_id, splice_amount } => {
1016-
let splice_amount_sats = splice_amount.to_sat().unwrap_or_else(|e| handle_error_msg(e));
1019+
let amount = match splice_amount.to_sat().unwrap_or_else(|e| handle_error_msg(e)) {
1020+
Some(amount_sats) => splice_in_request::Amount::SpliceAmountSats(amount_sats),
1021+
None => splice_in_request::Amount::AllFunds(AllFunds {}),
1022+
};
10171023
handle_response_result::<_, SpliceInResponse>(
10181024
client
10191025
.splice_in(SpliceInRequest {
10201026
user_channel_id,
10211027
counterparty_node_id,
1022-
splice_amount_sats,
1028+
amount: Some(amount),
10231029
})
10241030
.await,
10251031
);

ldk-server-cli/src/types.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,35 @@ impl FromStr for Amount {
120120
}
121121
}
122122

123+
/// An exact on-chain amount or all available on-chain funds.
124+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125+
pub enum AmountOrAll {
126+
Exact(Amount),
127+
All,
128+
}
129+
130+
impl AmountOrAll {
131+
/// Returns the exact amount in satoshis, or `None` when all funds should be used.
132+
pub fn to_sat(self) -> Result<Option<u64>, String> {
133+
match self {
134+
Self::Exact(amount) => amount.to_sat().map(Some),
135+
Self::All => Ok(None),
136+
}
137+
}
138+
}
139+
140+
impl FromStr for AmountOrAll {
141+
type Err = String;
142+
143+
fn from_str(s: &str) -> Result<Self, Self::Err> {
144+
if s.trim() == "all" {
145+
Ok(Self::All)
146+
} else {
147+
Amount::from_str(s).map(Self::Exact)
148+
}
149+
}
150+
}
151+
123152
/// A validated 32-byte payment preimage, parsed from a 64-character hex string.
124153
#[derive(Debug, Clone)]
125154
pub struct Preimage(pub [u8; 32]);
@@ -207,6 +236,14 @@ mod tests {
207236
assert!(Amount::from_str(&big).is_err());
208237
}
209238

239+
#[test]
240+
fn amount_or_all_parses_exact_amount_or_all() {
241+
assert_eq!(AmountOrAll::from_str("all").unwrap(), AmountOrAll::All);
242+
assert_eq!(AmountOrAll::from_str(" all ").unwrap(), AmountOrAll::All);
243+
assert_eq!(AmountOrAll::from_str("100sat").unwrap().to_sat().unwrap(), Some(100));
244+
assert_eq!(AmountOrAll::All.to_sat().unwrap(), None);
245+
}
246+
210247
#[test]
211248
fn preimage_parsing_and_roundtrip() {
212249
// valid 64-char hex string

ldk-server-grpc/src/api.rs

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,13 @@ pub struct SpontaneousSendResponse {
521521
#[prost(string, tag = "1")]
522522
pub payment_id: ::prost::alloc::string::String,
523523
}
524+
/// Selects all available on-chain funds.
525+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
526+
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
527+
#[cfg_attr(feature = "serde", serde(default))]
528+
#[allow(clippy::derive_partial_eq_without_eq)]
529+
#[derive(Clone, PartialEq, ::prost::Message)]
530+
pub struct AllFunds {}
524531
/// Creates a new outbound channel to the given remote node.
525532
/// See more: <https://docs.rs/ldk-node/latest/ldk_node/struct.Node.html#method.connect_open_channel>
526533
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@@ -536,9 +543,6 @@ pub struct OpenChannelRequest {
536543
/// It can be of type IPv4:port, IPv6:port, OnionV3:port or hostname:port
537544
#[prost(string, tag = "2")]
538545
pub address: ::prost::alloc::string::String,
539-
/// The amount of satoshis the caller is willing to commit to the channel.
540-
#[prost(uint64, tag = "3")]
541-
pub channel_amount_sats: u64,
542546
/// The amount of satoshis to push to the remote side as part of the initial commitment state.
543547
#[prost(uint64, optional, tag = "4")]
544548
pub push_to_counterparty_msat: ::core::option::Option<u64>,
@@ -551,6 +555,25 @@ pub struct OpenChannelRequest {
551555
/// Allow the counterparty to spend all its channel balance. This cannot be set together with `announce_channel`.
552556
#[prost(bool, tag = "7")]
553557
pub disable_counterparty_reserve: bool,
558+
/// Required. The funds to commit to the channel.
559+
#[prost(oneof = "open_channel_request::Amount", tags = "3, 8")]
560+
pub amount: ::core::option::Option<open_channel_request::Amount>,
561+
}
562+
/// Nested message and enum types in `OpenChannelRequest`.
563+
pub mod open_channel_request {
564+
/// Required. The funds to commit to the channel.
565+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
566+
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
567+
#[allow(clippy::derive_partial_eq_without_eq)]
568+
#[derive(Clone, PartialEq, ::prost::Oneof)]
569+
pub enum Amount {
570+
/// Commit the given amount of satoshis.
571+
#[prost(uint64, tag = "3")]
572+
ChannelAmountSats(u64),
573+
/// Commit all available on-chain funds, minus fees and anchor reserves.
574+
#[prost(message, tag = "8")]
575+
AllFunds(super::AllFunds),
576+
}
554577
}
555578
/// The response for the `OpenChannel` RPC. On failure, a gRPC error status is returned.
556579
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@@ -577,9 +600,25 @@ pub struct SpliceInRequest {
577600
/// The hex-encoded public key of the channel's counterparty node.
578601
#[prost(string, tag = "2")]
579602
pub counterparty_node_id: ::prost::alloc::string::String,
580-
/// The amount of sats to splice into the channel.
581-
#[prost(uint64, tag = "3")]
582-
pub splice_amount_sats: u64,
603+
/// Required. The funds to splice into the channel.
604+
#[prost(oneof = "splice_in_request::Amount", tags = "3, 4")]
605+
pub amount: ::core::option::Option<splice_in_request::Amount>,
606+
}
607+
/// Nested message and enum types in `SpliceInRequest`.
608+
pub mod splice_in_request {
609+
/// Required. The funds to splice into the channel.
610+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
611+
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
612+
#[allow(clippy::derive_partial_eq_without_eq)]
613+
#[derive(Clone, PartialEq, ::prost::Oneof)]
614+
pub enum Amount {
615+
/// Splice in the given amount of satoshis.
616+
#[prost(uint64, tag = "3")]
617+
SpliceAmountSats(u64),
618+
/// Splice in all available confirmed on-chain funds.
619+
#[prost(message, tag = "4")]
620+
AllFunds(super::AllFunds),
621+
}
583622
}
584623
/// The response for the `SpliceIn` RPC. On failure, a gRPC error status is returned.
585624
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]

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

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,9 @@ message SpontaneousSendResponse {
408408
string payment_id = 1;
409409
}
410410

411+
// Selects all available on-chain funds.
412+
message AllFunds {}
413+
411414
// Creates a new outbound channel to the given remote node.
412415
// See more: https://docs.rs/ldk-node/latest/ldk_node/struct.Node.html#method.connect_open_channel
413416
message OpenChannelRequest {
@@ -419,8 +422,14 @@ message OpenChannelRequest {
419422
// It can be of type IPv4:port, IPv6:port, OnionV3:port or hostname:port
420423
string address = 2;
421424

422-
// The amount of satoshis the caller is willing to commit to the channel.
423-
uint64 channel_amount_sats = 3;
425+
// Required. The funds to commit to the channel.
426+
oneof amount {
427+
// Commit the given amount of satoshis.
428+
uint64 channel_amount_sats = 3;
429+
430+
// Commit all available on-chain funds, minus fees and anchor reserves.
431+
AllFunds all_funds = 8;
432+
}
424433

425434
// The amount of satoshis to push to the remote side as part of the initial commitment state.
426435
optional uint64 push_to_counterparty_msat = 4;
@@ -452,8 +461,14 @@ message SpliceInRequest {
452461
// The hex-encoded public key of the channel's counterparty node.
453462
string counterparty_node_id = 2;
454463

455-
// The amount of sats to splice into the channel.
456-
uint64 splice_amount_sats = 3;
464+
// Required. The funds to splice into the channel.
465+
oneof amount {
466+
// Splice in the given amount of satoshis.
467+
uint64 splice_amount_sats = 3;
468+
469+
// Splice in all available confirmed on-chain funds.
470+
AllFunds all_funds = 4;
471+
}
457472
}
458473

459474
// The response for the `SpliceIn` RPC. On failure, a gRPC error status is returned.

0 commit comments

Comments
 (0)