diff --git a/core/archipelago/src/api/rpc/lnd/wallet.rs b/core/archipelago/src/api/rpc/lnd/wallet.rs index df0bab3b..445adaa5 100644 --- a/core/archipelago/src/api/rpc/lnd/wallet.rs +++ b/core/archipelago/src/api/rpc/lnd/wallet.rs @@ -327,10 +327,10 @@ impl RpcHandler { } let (client, macaroon_hex) = self.lnd_client().await?; - let invoice_body = serde_json::json!({ - "value": amount_sats.to_string(), - "memo": memo, - }); + // See `build_invoice_request_body` for why `private: true` is + // unconditional — this is the seller-side/peer-file invoice path, + // the twin of the bug fixed in `handle_lnd_createinvoice` below. + let invoice_body = build_invoice_request_body(amount_sats, memo); // LND's REST endpoint can briefly drop/reset connections under load // (swap pressure, just-restarted, TLS handshake races), which used to // hard-fail the buy-file invoice with an opaque 503. Retry on a @@ -551,10 +551,10 @@ impl RpcHandler { let (client, macaroon_hex) = self.lnd_client().await?; - let invoice_body = serde_json::json!({ - "value": amount_sats.to_string(), - "memo": memo, - }); + // See `build_invoice_request_body` for why `private: true` is + // unconditional. This is the wallet UI's Receive flow / the + // `lnd.createinvoice` RPC — the bug diagnosed on archy-x250-mad2. + let invoice_body = build_invoice_request_body(amount_sats, memo); let resp = match client .post(format!("{LND_REST_BASE_URL}/v1/invoices")) @@ -1105,10 +1105,39 @@ fn classify_lnd_address_error(message: &str) -> &'static str { } } +/// Build the LND `/v1/invoices` POST body used to mint a Lightning invoice. +/// +/// `private: true` is unconditional and must stay that way: it tells LND to +/// embed a route hint for every private/unannounced channel this node +/// holds, which is the only way a payment can find its way in over such a +/// channel. It is harmless when the node also has public channels — LND +/// still routes directly over a public channel when it can, and the hint is +/// simply an unused alternate path. +/// +/// Shared by both invoice-creation call sites (`create_invoice`, the +/// seller-side/peer-file flow, and `handle_lnd_createinvoice`, the wallet +/// UI's Receive flow / `lnd.createinvoice` RPC) so a single test pins the +/// field for both and neither can silently drift back to `false`. +fn build_invoice_request_body(amount_sats: i64, memo: &str) -> serde_json::Value { + serde_json::json!({ + "value": amount_sats.to_string(), + "memo": memo, + "private": true, + }) +} + #[cfg(test)] mod tests { use super::*; + #[test] + fn invoice_request_body_always_sets_private_true() { + let body = build_invoice_request_body(1_234, "test memo"); + assert_eq!(body["private"], serde_json::json!(true)); + assert_eq!(body["value"], serde_json::json!("1234")); + assert_eq!(body["memo"], serde_json::json!("test memo")); + } + #[test] fn lnd_error_message_prefers_message_field() { let body = serde_json::json!({