Skip to content

Commit 86d444d

Browse files
Populate response fields when decoding a BOLT12 invoice
Following review feedback: the BOLT12 path previously only validated the invoice and reported `kind`, returning no other data. It now extracts the fields that map onto `DecodeInvoiceResponse` -- signing pubkey, payment hash, amount, creation time, relative expiry, description, fallback address, invoice features, and expiry status. BOLT11-only fields (`payment_secret`, `route_hints`, `currency`, etc.) remain empty for BOLT12 invoices. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d782e4a commit 86d444d

3 files changed

Lines changed: 38 additions & 8 deletions

File tree

ldk-server-grpc/src/api.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1122,7 +1122,8 @@ pub struct DecodeInvoiceRequest {
11221122
pub invoice: ::prost::alloc::string::String,
11231123
}
11241124
/// The response for the `DecodeInvoice` RPC. On failure, a gRPC error status is returned.
1125-
/// For a BOLT12 invoice only `kind` is populated; all other fields apply to BOLT11 invoices.
1125+
/// `kind` indicates which invoice type was decoded; fields that do not apply to that type
1126+
/// are left empty (e.g. `payment_secret` and `route_hints` are BOLT11-only).
11261127
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
11271128
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
11281129
#[cfg_attr(feature = "serde", serde(default))]

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -803,7 +803,8 @@ message DecodeInvoiceRequest {
803803
}
804804

805805
// The response for the `DecodeInvoice` RPC. On failure, a gRPC error status is returned.
806-
// For a BOLT12 invoice only `kind` is populated; all other fields apply to BOLT11 invoices.
806+
// `kind` indicates which invoice type was decoded; fields that do not apply to that type
807+
// are left empty (e.g. `payment_secret` and `route_hints` are BOLT11-only).
807808
message DecodeInvoiceResponse {
808809
// The hex-encoded public key of the destination node.
809810
string destination = 1;

ldk-server/src/api/decode_invoice.rs

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use std::sync::Arc;
1313
use hex::prelude::*;
1414
use ldk_node::lightning::offers::invoice::Bolt12Invoice;
1515
use ldk_node::lightning_invoice::Bolt11Invoice;
16-
use ldk_node::lightning_types::features::Bolt11InvoiceFeatures;
16+
use ldk_node::lightning_types::features::{Bolt11InvoiceFeatures, Bolt12InvoiceFeatures};
1717
use ldk_server_grpc::api::{DecodeInvoiceRequest, DecodeInvoiceResponse};
1818
use ldk_server_grpc::types::{Bolt11HopHint, Bolt11RouteHint};
1919

@@ -47,11 +47,29 @@ fn decode_invoice(invoice: &str) -> Result<DecodeInvoiceResponse, LdkServerError
4747
///
4848
/// Unlike offers and BOLT11 invoices, a BOLT12 invoice has no human-readable string
4949
/// encoding — it is exchanged as raw bytes — so the input is expected to be hex-encoded.
50-
/// Only the `kind` field is populated for a BOLT12 invoice.
50+
/// Fields that do not apply to BOLT12 invoices (e.g. `payment_secret`, `route_hints`) are
51+
/// left at their default empty values.
5152
fn decode_bolt12_invoice(invoice: &str) -> Option<DecodeInvoiceResponse> {
5253
let bytes = Vec::<u8>::from_hex(invoice).ok()?;
53-
Bolt12Invoice::try_from(bytes).ok()?;
54-
Some(DecodeInvoiceResponse { kind: INVOICE_KIND_BOLT12.to_string(), ..Default::default() })
54+
let invoice = Bolt12Invoice::try_from(bytes).ok()?;
55+
56+
let features = decode_features(invoice.invoice_features().le_flags(), |bytes| {
57+
Bolt12InvoiceFeatures::from_le_bytes(bytes).to_string()
58+
});
59+
60+
Some(DecodeInvoiceResponse {
61+
destination: invoice.signing_pubkey().to_string(),
62+
payment_hash: invoice.payment_hash().0.to_lower_hex_string(),
63+
amount_msat: Some(invoice.amount_msats()),
64+
timestamp: invoice.created_at().as_secs(),
65+
expiry: invoice.relative_expiry().as_secs(),
66+
description: invoice.description().map(|d| d.to_string()),
67+
fallback_address: invoice.fallbacks().into_iter().next().map(|a| a.to_string()),
68+
features,
69+
is_expired: invoice.is_expired(),
70+
kind: INVOICE_KIND_BOLT12.to_string(),
71+
..Default::default()
72+
})
5573
}
5674

5775
fn decode_bolt11_invoice(invoice: &Bolt11Invoice) -> DecodeInvoiceResponse {
@@ -152,11 +170,18 @@ mod tests {
152170
PublicKey::from_secret_key(&secp, &SecretKey::from_slice(&[byte; 32]).unwrap())
153171
}
154172

173+
/// The keypair the sample BOLT12 invoice is signed with; its public key is the
174+
/// invoice's `signing_pubkey`.
175+
fn signing_keypair() -> Keypair {
176+
let secp = Secp256k1::new();
177+
Keypair::from_secret_key(&secp, &SecretKey::from_slice(&[43; 32]).unwrap())
178+
}
179+
155180
/// Builds a signed BOLT12 invoice and returns it hex-encoded, matching how a BOLT12
156181
/// invoice would be supplied to `DecodeInvoice`.
157182
fn sample_bolt12_invoice_hex() -> String {
158183
let secp = Secp256k1::new();
159-
let keys = Keypair::from_secret_key(&secp, &SecretKey::from_slice(&[43; 32]).unwrap());
184+
let keys = signing_keypair();
160185

161186
let payment_paths = vec![BlindedPaymentPath::from_blinded_path_and_payinfo(
162187
pubkey(40),
@@ -203,8 +228,11 @@ mod tests {
203228
}
204229

205230
#[test]
206-
fn decodes_bolt12_invoice_with_bolt12_kind() {
231+
fn decodes_bolt12_invoice_and_populates_fields() {
207232
let response = decode_invoice(&sample_bolt12_invoice_hex()).unwrap();
208233
assert_eq!(response.kind, INVOICE_KIND_BOLT12);
234+
assert_eq!(response.destination, signing_keypair().public_key().to_string());
235+
assert_eq!(response.payment_hash, "2a".repeat(32));
236+
assert_eq!(response.amount_msat, Some(1_000));
209237
}
210238
}

0 commit comments

Comments
 (0)