Skip to content

Commit e8e8045

Browse files
committed
Make BOLT12 CLI more ergonomic
It was unclear how to create a reusable offer, and `bolt12-receive 1000sat` silently used the amount as the description. Take the amount positionally with an optional -d description, like bolt11-receive, and document that offers can be paid repeatedly until they expire.
1 parent 21a903c commit e8e8045

6 files changed

Lines changed: 48 additions & 25 deletions

File tree

‎e2e-tests/tests/e2e.rs‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ async fn test_cli_bolt12_receive() {
329329
// BOLT12 offers need announced channels for blinded reply paths
330330
setup_funded_channel(&bitcoind, &server_a, &server_b, 100_000).await;
331331

332-
let output = run_cli(&server_a, &["bolt12-receive", "test offer"]);
332+
let output = run_cli(&server_a, &["bolt12-receive", "-d", "test offer"]);
333333
let offer_str = output["offer"].as_str().unwrap();
334334
assert!(offer_str.starts_with("lno"), "Expected lno prefix, got: {}", offer_str);
335335

@@ -347,7 +347,7 @@ async fn test_cli_decode_offer() {
347347
setup_funded_channel(&bitcoind, &server_a, &server_b, 100_000).await;
348348

349349
// Create a BOLT12 offer with known parameters
350-
let output = run_cli(&server_a, &["bolt12-receive", "decode offer test"]);
350+
let output = run_cli(&server_a, &["bolt12-receive", "-d", "decode offer test"]);
351351
let offer_str = output["offer"].as_str().unwrap();
352352

353353
// Decode it
@@ -378,21 +378,21 @@ async fn test_cli_decode_offer() {
378378
assert!(decoded.get("amount").is_none() || decoded["amount"].is_null());
379379

380380
// Test a fixed-amount offer
381-
let output_fixed = run_cli(&server_a, &["bolt12-receive", "fixed amount", "50000sat"]);
381+
let output_fixed = run_cli(&server_a, &["bolt12-receive", "50000sat", "-d", "fixed amount"]);
382382
let decoded_fixed =
383383
run_cli(&server_a, &["decode-offer", output_fixed["offer"].as_str().unwrap()]);
384384
assert_eq!(decoded_fixed["amount"]["amount"]["bitcoin_amount_msats"], 50_000_000);
385385

386386
// Test that ANSI escape sequences cannot reach the terminal via CLI output.
387387
let desc_with_ansi = "offer\x1b[31m RED \x1b[0m";
388-
let output_ansi = run_cli(&server_a, &["bolt12-receive", desc_with_ansi]);
388+
let output_ansi = run_cli(&server_a, &["bolt12-receive", "-d", desc_with_ansi]);
389389
let raw_decoded =
390390
run_cli_raw(&server_a, &["decode-offer", output_ansi["offer"].as_str().unwrap()]);
391391
assert!(!raw_decoded.contains('\x1b'), "Raw CLI output must not contain ANSI escape bytes");
392392

393393
// Test that Unicode bidi override characters in the description are escaped
394394
let desc_with_bidi = "offer\u{202E}evil";
395-
let output_bidi = run_cli(&server_a, &["bolt12-receive", desc_with_bidi]);
395+
let output_bidi = run_cli(&server_a, &["bolt12-receive", "-d", desc_with_bidi]);
396396
let raw_bidi =
397397
run_cli_raw(&server_a, &["decode-offer", output_bidi["offer"].as_str().unwrap()]);
398398
// LDK exposes offer descriptions through PrintableString, which may replace

‎ldk-server-cli/src/main.rs‎

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -339,17 +339,33 @@ enum Commands {
339339
)]
340340
max_channel_saturation_power_of_half: Option<u32>,
341341
},
342-
#[command(about = "Return a BOLT12 offer for receiving payments")]
342+
#[command(
343+
about = "Create a reusable BOLT12 offer for receiving payments",
344+
long_about = "Create a reusable BOLT12 offer for receiving payments.\n\n\
345+
A BOLT12 offer can be paid any number of times, by any number of payers, until it \
346+
expires. Without --expiry-secs the offer never expires, so it can be shared publicly \
347+
(e.g. for donations or a static payment code). Omit the amount to let each payer \
348+
choose how much to send."
349+
)]
343350
Bolt12Receive {
344-
#[arg(help = "Description to attach along with the offer")]
345-
description: String,
346351
#[arg(
347-
help = "Amount to request, e.g. 50sat or 50000msat. If unset, a variable-amount offer is returned"
352+
help = "Amount to request per item, e.g. 50sat or 50000msat. If unset, a variable-amount offer is returned"
348353
)]
349354
amount: Option<Amount>,
350-
#[arg(long, help = "Offer expiry time in seconds")]
355+
#[arg(short, long, help = "Description to attach along with the offer")]
356+
description: Option<String>,
357+
#[arg(
358+
short,
359+
long,
360+
help = "Offer expiry time in seconds. If unset, the offer never expires"
361+
)]
351362
expiry_secs: Option<u32>,
352-
#[arg(long, help = "Number of items requested. Can only be set for fixed-amount offers")]
363+
#[arg(
364+
short,
365+
long,
366+
requires = "amount",
367+
help = "Maximum number of items a payer may buy per payment. Only for fixed-amount offers"
368+
)]
353369
quantity: Option<u64>,
354370
},
355371
#[command(about = "Send a payment for a BOLT12 offer")]
@@ -1112,12 +1128,12 @@ async fn main() {
11121128
.await,
11131129
);
11141130
},
1115-
Commands::Bolt12Receive { description, amount, expiry_secs, quantity } => {
1131+
Commands::Bolt12Receive { amount, description, expiry_secs, quantity } => {
11161132
let amount_msat = amount.map(|a| a.to_msat());
11171133
handle_response_result::<_, Bolt12ReceiveResponse>(
11181134
client
11191135
.bolt12_receive(Bolt12ReceiveRequest {
1120-
description,
1136+
description: description.unwrap_or_default(),
11211137
amount_msat,
11221138
expiry_secs,
11231139
quantity,

‎ldk-server-grpc/src/api.rs‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,8 @@ pub struct Bolt11SendUnderpayingResponse {
537537
}
538538
/// Returns a BOLT12 offer for the given amount, if specified.
539539
///
540+
/// The offer is reusable: it can be paid any number of times until it expires.
541+
///
540542
/// See more:
541543
/// - <https://docs.rs/ldk-node/latest/ldk_node/payment/struct.Bolt12Payment.html#method.receive>
542544
/// - <https://docs.rs/ldk-node/latest/ldk_node/payment/struct.Bolt12Payment.html#method.receive_variable_amount>
@@ -550,13 +552,15 @@ pub struct Bolt12ReceiveRequest {
550552
/// Will be set in the description field of the encoded offer.
551553
#[prost(string, tag = "1")]
552554
pub description: ::prost::alloc::string::String,
553-
/// The amount in millisatoshi to send. If unset, a "zero-amount" or variable-amount offer is returned.
555+
/// The amount in millisatoshi to request per item. If unset, a "zero-amount" or variable-amount
556+
/// offer is returned.
554557
#[prost(uint64, optional, tag = "2")]
555558
pub amount_msat: ::core::option::Option<u64>,
556-
/// Offer expiry time in seconds.
559+
/// Offer expiry time in seconds. If unset, the offer never expires.
557560
#[prost(uint32, optional, tag = "3")]
558561
pub expiry_secs: ::core::option::Option<u32>,
559-
/// If set, it represents the number of items requested, can only be set for fixed-amount offers.
562+
/// If set, the maximum number of items a payer may request in a single payment. This does not
563+
/// limit how many times the offer can be paid. Can only be set for fixed-amount offers.
560564
#[prost(uint64, optional, tag = "4")]
561565
pub quantity: ::core::option::Option<u64>,
562566
}

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,8 @@ message Bolt11SendUnderpayingResponse {
409409

410410
// Returns a BOLT12 offer for the given amount, if specified.
411411
//
412+
// The offer is reusable: it can be paid any number of times until it expires.
413+
//
412414
// See more:
413415
// - https://docs.rs/ldk-node/latest/ldk_node/payment/struct.Bolt12Payment.html#method.receive
414416
// - https://docs.rs/ldk-node/latest/ldk_node/payment/struct.Bolt12Payment.html#method.receive_variable_amount
@@ -418,13 +420,15 @@ message Bolt12ReceiveRequest {
418420
// Will be set in the description field of the encoded offer.
419421
string description = 1;
420422

421-
// The amount in millisatoshi to send. If unset, a "zero-amount" or variable-amount offer is returned.
423+
// The amount in millisatoshi to request per item. If unset, a "zero-amount" or variable-amount
424+
// offer is returned.
422425
optional uint64 amount_msat = 2;
423426

424-
// Offer expiry time in seconds.
427+
// Offer expiry time in seconds. If unset, the offer never expires.
425428
optional uint32 expiry_secs = 3;
426429

427-
// If set, it represents the number of items requested, can only be set for fixed-amount offers.
430+
// If set, the maximum number of items a payer may request in a single payment. This does not
431+
// limit how many times the offer can be paid. Can only be set for fixed-amount offers.
428432
optional uint64 quantity = 4;
429433
}
430434

‎ldk-server-mcp/src/tools/mod.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ pub fn build_tool_registry() -> ToolRegistry {
193193
),
194194
tool_spec(
195195
"bolt12_receive",
196-
"Create a BOLT12 offer for receiving Lightning payments",
196+
"Create a reusable BOLT12 offer for receiving Lightning payments",
197197
schema::bolt12_receive_schema,
198198
|client, args| Box::pin(handlers::handle_bolt12_receive(client, args)),
199199
),

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -430,18 +430,17 @@ pub fn bolt12_receive_schema() -> Value {
430430
},
431431
"amount_msat": {
432432
"type": "integer",
433-
"description": "Amount in millisatoshis. If unset, a variable-amount offer is returned"
433+
"description": "Amount in millisatoshis to request per item. If unset, a variable-amount offer is returned"
434434
},
435435
"expiry_secs": {
436436
"type": "integer",
437-
"description": "Offer expiry time in seconds"
437+
"description": "Offer expiry time in seconds. If unset, the offer never expires"
438438
},
439439
"quantity": {
440440
"type": "integer",
441-
"description": "Number of items requested. Can only be set for fixed-amount offers"
441+
"description": "Maximum number of items a payer may request in a single payment. Does not limit how many times the offer can be paid. Can only be set for fixed-amount offers"
442442
}
443-
},
444-
"required": ["description"]
443+
}
445444
})
446445
}
447446

0 commit comments

Comments
 (0)