From 245c729140f42c74089de1bb73e7ab857ba8aa67 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Mon, 7 Sep 2026 23:25:57 -0500 Subject: [PATCH 1/9] Update ldk-node and adapt hold-invoice RPCs Update the pinned node API and replace hash-based claim and fail RPCs with payment-ID requests across the server, clients, CLI, and MCP. Keep manual handling enabled for unknown BOLT 11 payments. Adapt fallible payment reads, paginated metric reads, event inputs, and mnemonic generation to the new dependency. Keep event schemas and the existing metric writers unchanged here. AI assistance: OpenAI Codex was used for this change. --- Cargo.lock | 84 +++++++----- docs/api-guide.md | 24 ++-- e2e-tests/Cargo.lock | 86 +++++++----- e2e-tests/Cargo.toml | 2 +- e2e-tests/tests/e2e.rs | 125 ++++++++++++------ ldk-server-cli/src/main.rs | 34 +++-- ldk-server-client/src/client.rs | 26 ++-- ldk-server-grpc/src/api.rs | 36 ++--- ldk-server-grpc/src/endpoints.rs | 4 +- ldk-server-grpc/src/proto/api.proto | 42 +++--- ldk-server-mcp/src/tools/handlers.rs | 14 +- ldk-server-mcp/src/tools/mod.rs | 16 +-- ldk-server-mcp/src/tools/schema.rs | 16 +-- ldk-server-mcp/tests/integration.rs | 14 +- ldk-server/Cargo.toml | 2 +- ...aim_for_hash.rs => bolt11_claim_for_id.rs} | 30 ++--- ldk-server/src/api/bolt11_fail_for_hash.rs | 34 ----- ldk-server/src/api/bolt11_fail_for_id.rs | 25 ++++ ldk-server/src/api/error.rs | 4 + ldk-server/src/api/get_payment_details.rs | 14 +- ldk-server/src/api/mod.rs | 23 +++- ldk-server/src/main.rs | 40 +++--- ldk-server/src/service.rs | 16 +-- ldk-server/src/util/entropy.rs | 4 +- ldk-server/src/util/metrics.rs | 68 ++++++---- 25 files changed, 442 insertions(+), 341 deletions(-) rename ldk-server/src/api/{bolt11_claim_for_hash.rs => bolt11_claim_for_id.rs} (50%) delete mode 100644 ldk-server/src/api/bolt11_fail_for_hash.rs create mode 100644 ldk-server/src/api/bolt11_fail_for_id.rs diff --git a/Cargo.lock b/Cargo.lock index 9fbb7373..8fba40c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -203,7 +203,7 @@ checksum = "83986307ea92997c3d051e8c306af8115a05add601e22acb7c1903008e6b614e" dependencies = [ "async-trait", "bdk_core", - "esplora-client", + "esplora-client 0.12.3", "futures", ] @@ -302,7 +302,7 @@ checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf" [[package]] name = "bitcoin-payment-instructions" version = "0.6.0" -source = "git+https://github.com/tnull/bitcoin-payment-instructions?rev=0e430be98c09540624a68a68022ee0551e86d1be#0e430be98c09540624a68a68022ee0551e86d1be" +source = "git+https://github.com/jkczyz/bitcoin-payment-instructions?rev=c359b125e972ff49b5c2e9f6865afb11500286a0#c359b125e972ff49b5c2e9f6865afb11500286a0" dependencies = [ "bitcoin", "dnssec-prover", @@ -350,6 +350,7 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b65c2e1ab98050c93cc9ada070d5070e014329c65196d03f6f8f1f75c2666f37" dependencies = [ + "base64 0.22.1", "rustls 0.23.42", "rustls-webpki 0.103.13", "serde", @@ -573,7 +574,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -591,6 +592,20 @@ dependencies = [ "tokio", ] +[[package]] +name = "esplora-client" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "647f9c89c653d69cce478202b7377c03737e33fc6bc5ec044187bf83e5597ea2" +dependencies = [ + "bitcoin", + "bitreq", + "hex-conservative 0.2.2", + "serde", + "serde_json", + "tokio", +] + [[package]] name = "fallible-iterator" version = "0.3.0" @@ -1266,7 +1281,7 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "ldk-node" version = "0.8.0+git" -source = "git+https://github.com/lightningdevkit/ldk-node?rev=056447c28221be02c3d39f8c6ae430a67ebbd850#056447c28221be02c3d39f8c6ae430a67ebbd850" +source = "git+https://github.com/lightningdevkit/ldk-node?rev=b1337d2f1665716313d2ccf849a04114f8b3ca19#b1337d2f1665716313d2ccf849a04114f8b3ca19" dependencies = [ "async-trait", "base64 0.22.1", @@ -1281,7 +1296,8 @@ dependencies = [ "bitreq", "chrono", "electrum-client", - "esplora-client", + "esplora-client 0.12.3", + "esplora-client 0.13.0", "getrandom 0.3.4", "libc", "lightning", @@ -1303,6 +1319,7 @@ dependencies = [ "serde", "serde_json", "tokio", + "tokio-util", "vss-client-ng", "winapi", ] @@ -1419,8 +1436,8 @@ dependencies = [ [[package]] name = "lightning" -version = "0.3.0+git" -source = "git+https://github.com/lightningdevkit/rust-lightning?rev=506cb91f2e0fb87906188b79777bcf42595d3623#506cb91f2e0fb87906188b79777bcf42595d3623" +version = "0.3.0-rc1" +source = "git+https://github.com/lightningdevkit/rust-lightning?rev=4219131b05e44663a1b417e4f6cf09c7740ead2a#4219131b05e44663a1b417e4f6cf09c7740ead2a" dependencies = [ "bech32", "bitcoin", @@ -1436,8 +1453,8 @@ dependencies = [ [[package]] name = "lightning-background-processor" -version = "0.3.0+git" -source = "git+https://github.com/lightningdevkit/rust-lightning?rev=506cb91f2e0fb87906188b79777bcf42595d3623#506cb91f2e0fb87906188b79777bcf42595d3623" +version = "0.3.0-rc1" +source = "git+https://github.com/lightningdevkit/rust-lightning?rev=4219131b05e44663a1b417e4f6cf09c7740ead2a#4219131b05e44663a1b417e4f6cf09c7740ead2a" dependencies = [ "bitcoin", "bitcoin-io", @@ -1450,8 +1467,8 @@ dependencies = [ [[package]] name = "lightning-block-sync" -version = "0.3.0+git" -source = "git+https://github.com/lightningdevkit/rust-lightning?rev=506cb91f2e0fb87906188b79777bcf42595d3623#506cb91f2e0fb87906188b79777bcf42595d3623" +version = "0.3.0-rc1" +source = "git+https://github.com/lightningdevkit/rust-lightning?rev=4219131b05e44663a1b417e4f6cf09c7740ead2a#4219131b05e44663a1b417e4f6cf09c7740ead2a" dependencies = [ "bitcoin", "bitreq", @@ -1462,8 +1479,8 @@ dependencies = [ [[package]] name = "lightning-dns-resolver" -version = "0.3.0+git" -source = "git+https://github.com/lightningdevkit/rust-lightning?rev=506cb91f2e0fb87906188b79777bcf42595d3623#506cb91f2e0fb87906188b79777bcf42595d3623" +version = "0.3.0-rc1" +source = "git+https://github.com/lightningdevkit/rust-lightning?rev=4219131b05e44663a1b417e4f6cf09c7740ead2a#4219131b05e44663a1b417e4f6cf09c7740ead2a" dependencies = [ "dnssec-prover", "lightning", @@ -1473,8 +1490,8 @@ dependencies = [ [[package]] name = "lightning-invoice" -version = "0.35.0+git" -source = "git+https://github.com/lightningdevkit/rust-lightning?rev=506cb91f2e0fb87906188b79777bcf42595d3623#506cb91f2e0fb87906188b79777bcf42595d3623" +version = "0.35.0-rc1" +source = "git+https://github.com/lightningdevkit/rust-lightning?rev=4219131b05e44663a1b417e4f6cf09c7740ead2a#4219131b05e44663a1b417e4f6cf09c7740ead2a" dependencies = [ "bech32", "bitcoin", @@ -1484,8 +1501,8 @@ dependencies = [ [[package]] name = "lightning-liquidity" -version = "0.3.0+git" -source = "git+https://github.com/lightningdevkit/rust-lightning?rev=506cb91f2e0fb87906188b79777bcf42595d3623#506cb91f2e0fb87906188b79777bcf42595d3623" +version = "0.3.0-rc1" +source = "git+https://github.com/lightningdevkit/rust-lightning?rev=4219131b05e44663a1b417e4f6cf09c7740ead2a#4219131b05e44663a1b417e4f6cf09c7740ead2a" dependencies = [ "bitcoin", "bitreq", @@ -1501,7 +1518,7 @@ dependencies = [ [[package]] name = "lightning-macros" version = "0.2.2+git" -source = "git+https://github.com/lightningdevkit/rust-lightning?rev=506cb91f2e0fb87906188b79777bcf42595d3623#506cb91f2e0fb87906188b79777bcf42595d3623" +source = "git+https://github.com/lightningdevkit/rust-lightning?rev=4219131b05e44663a1b417e4f6cf09c7740ead2a#4219131b05e44663a1b417e4f6cf09c7740ead2a" dependencies = [ "proc-macro2", "quote", @@ -1510,8 +1527,8 @@ dependencies = [ [[package]] name = "lightning-net-tokio" -version = "0.3.0+git" -source = "git+https://github.com/lightningdevkit/rust-lightning?rev=506cb91f2e0fb87906188b79777bcf42595d3623#506cb91f2e0fb87906188b79777bcf42595d3623" +version = "0.3.0-rc1" +source = "git+https://github.com/lightningdevkit/rust-lightning?rev=4219131b05e44663a1b417e4f6cf09c7740ead2a#4219131b05e44663a1b417e4f6cf09c7740ead2a" dependencies = [ "bitcoin", "lightning", @@ -1520,8 +1537,8 @@ dependencies = [ [[package]] name = "lightning-persister" -version = "0.3.0+git" -source = "git+https://github.com/lightningdevkit/rust-lightning?rev=506cb91f2e0fb87906188b79777bcf42595d3623#506cb91f2e0fb87906188b79777bcf42595d3623" +version = "0.3.0-rc1" +source = "git+https://github.com/lightningdevkit/rust-lightning?rev=4219131b05e44663a1b417e4f6cf09c7740ead2a#4219131b05e44663a1b417e4f6cf09c7740ead2a" dependencies = [ "bitcoin", "lightning", @@ -1531,8 +1548,8 @@ dependencies = [ [[package]] name = "lightning-rapid-gossip-sync" -version = "0.3.0+git" -source = "git+https://github.com/lightningdevkit/rust-lightning?rev=506cb91f2e0fb87906188b79777bcf42595d3623#506cb91f2e0fb87906188b79777bcf42595d3623" +version = "0.3.0-rc1" +source = "git+https://github.com/lightningdevkit/rust-lightning?rev=4219131b05e44663a1b417e4f6cf09c7740ead2a#4219131b05e44663a1b417e4f6cf09c7740ead2a" dependencies = [ "bitcoin", "bitcoin-io", @@ -1542,12 +1559,12 @@ dependencies = [ [[package]] name = "lightning-transaction-sync" -version = "0.3.0+git" -source = "git+https://github.com/lightningdevkit/rust-lightning?rev=506cb91f2e0fb87906188b79777bcf42595d3623#506cb91f2e0fb87906188b79777bcf42595d3623" +version = "0.3.0-rc1" +source = "git+https://github.com/lightningdevkit/rust-lightning?rev=4219131b05e44663a1b417e4f6cf09c7740ead2a#4219131b05e44663a1b417e4f6cf09c7740ead2a" dependencies = [ "bitcoin", "electrum-client", - "esplora-client", + "esplora-client 0.13.0", "futures", "lightning", "lightning-macros", @@ -1555,8 +1572,8 @@ dependencies = [ [[package]] name = "lightning-types" -version = "0.4.0+git" -source = "git+https://github.com/lightningdevkit/rust-lightning?rev=506cb91f2e0fb87906188b79777bcf42595d3623#506cb91f2e0fb87906188b79777bcf42595d3623" +version = "0.4.0-rc1" +source = "git+https://github.com/lightningdevkit/rust-lightning?rev=4219131b05e44663a1b417e4f6cf09c7740ead2a#4219131b05e44663a1b417e4f6cf09c7740ead2a" dependencies = [ "bitcoin", ] @@ -1715,7 +1732,7 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "possiblyrandom" version = "0.2.0" -source = "git+https://github.com/lightningdevkit/rust-lightning?rev=506cb91f2e0fb87906188b79777bcf42595d3623#506cb91f2e0fb87906188b79777bcf42595d3623" +source = "git+https://github.com/lightningdevkit/rust-lightning?rev=4219131b05e44663a1b417e4f6cf09c7740ead2a#4219131b05e44663a1b417e4f6cf09c7740ead2a" dependencies = [ "getrandom 0.2.16", ] @@ -1863,7 +1880,7 @@ dependencies = [ "once_cell", "socket2 0.5.10", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -2105,7 +2122,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2428,7 +2445,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix 1.1.2", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2553,6 +2570,7 @@ dependencies = [ "bytes", "futures-core", "futures-sink", + "futures-util", "pin-project-lite", "tokio", ] diff --git a/docs/api-guide.md b/docs/api-guide.md index badd3fb0..8b161b17 100644 --- a/docs/api-guide.md +++ b/docs/api-guide.md @@ -113,8 +113,8 @@ These RPCs support a manual claim/fail workflow for held payments. See | RPC | Description | |------------------------|--------------------------------------------------------------------| | `Bolt11ReceiveForHash` | Create an invoice for a given payment hash (manual claim required) | -| `Bolt11ClaimForHash` | Claim a held payment by providing the preimage | -| `Bolt11FailForHash` | Reject a held payment | +| `Bolt11ClaimForId` | Claim a held payment by its payment ID and preimage | +| `Bolt11FailForId` | Reject a held payment by its payment ID | ### BOLT11 JIT Channels (LSPS2) @@ -233,16 +233,20 @@ optional Basic Auth. See [Configuration](configuration.md#metrics) for setup. Hodl invoices allow you to inspect and conditionally accept incoming payments: -1. **Create the invoice:** Call `Bolt11ReceiveForHash` with a payment hash you control. -2. **Wait for payment:** Subscribe to events via `SubscribeEvents` and watch for a - `PaymentClaimable` event matching your payment hash. -3. **Decide:** - - **Accept:** Call `Bolt11ClaimForHash` with the preimage corresponding to the payment hash. - - **Reject:** Call `Bolt11FailForHash` with the payment hash. +1. **Subscribe:** Call `SubscribeEvents` before you create or share the invoice. Events are not + replayed. +2. **Create the invoice:** Generate a new payment hash. Call `Bolt11ReceiveForHash` with this hash. + Never reuse a payment hash. Reuse is unsafe and can cause loss of funds. +3. **Handle each payment:** Save `payment.id` from each `PaymentClaimable` event. A payer can pay + the same invoice more than once. Each payment has a separate event and payment ID. +4. **Decide before `claim_deadline`:** + - **Accept an expected payment:** Call `Bolt11ClaimForId` with its payment ID and preimage. + - **Reject an unexpected payment:** Call `Bolt11FailForId` with its payment ID. Reject duplicate + and late payments instead of ignoring or claiming them. The payment is held in a pending state until you explicitly claim or fail it. **You must -always call one of these.** If you do neither, the HTLC will eventually time out, which -can cause a force-closure of the channel. +always handle each event.** If you do not, the HTLC will eventually time out. This can cause a +force-closure of the channel. ## Pagination diff --git a/e2e-tests/Cargo.lock b/e2e-tests/Cargo.lock index 310f66d6..4aa04d85 100644 --- a/e2e-tests/Cargo.lock +++ b/e2e-tests/Cargo.lock @@ -165,7 +165,7 @@ checksum = "83986307ea92997c3d051e8c306af8115a05add601e22acb7c1903008e6b614e" dependencies = [ "async-trait", "bdk_core", - "esplora-client", + "esplora-client 0.12.3", "futures", ] @@ -250,7 +250,7 @@ checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" [[package]] name = "bitcoin-payment-instructions" version = "0.6.0" -source = "git+https://github.com/tnull/bitcoin-payment-instructions?rev=0e430be98c09540624a68a68022ee0551e86d1be#0e430be98c09540624a68a68022ee0551e86d1be" +source = "git+https://github.com/jkczyz/bitcoin-payment-instructions?rev=c359b125e972ff49b5c2e9f6865afb11500286a0#c359b125e972ff49b5c2e9f6865afb11500286a0" dependencies = [ "bitcoin", "dnssec-prover", @@ -298,6 +298,7 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b65c2e1ab98050c93cc9ada070d5070e014329c65196d03f6f8f1f75c2666f37" dependencies = [ + "base64 0.22.1", "rustls 0.23.42", "rustls-webpki 0.103.13", "serde", @@ -566,7 +567,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -584,6 +585,20 @@ dependencies = [ "tokio", ] +[[package]] +name = "esplora-client" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "647f9c89c653d69cce478202b7377c03737e33fc6bc5ec044187bf83e5597ea2" +dependencies = [ + "bitcoin", + "bitreq", + "hex-conservative", + "serde", + "serde_json", + "tokio", +] + [[package]] name = "fallible-iterator" version = "0.3.0" @@ -1269,7 +1284,7 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "ldk-node" version = "0.8.0+git" -source = "git+https://github.com/lightningdevkit/ldk-node?rev=056447c28221be02c3d39f8c6ae430a67ebbd850#056447c28221be02c3d39f8c6ae430a67ebbd850" +source = "git+https://github.com/lightningdevkit/ldk-node?rev=b1337d2f1665716313d2ccf849a04114f8b3ca19#b1337d2f1665716313d2ccf849a04114f8b3ca19" dependencies = [ "async-trait", "base64 0.22.1", @@ -1284,7 +1299,8 @@ dependencies = [ "bitreq", "chrono", "electrum-client", - "esplora-client", + "esplora-client 0.12.3", + "esplora-client 0.13.0", "getrandom 0.3.4", "libc", "lightning", @@ -1306,6 +1322,7 @@ dependencies = [ "serde", "serde_json", "tokio", + "tokio-util", "vss-client-ng", "winapi", ] @@ -1381,8 +1398,8 @@ dependencies = [ [[package]] name = "lightning" -version = "0.3.0+git" -source = "git+https://github.com/lightningdevkit/rust-lightning?rev=506cb91f2e0fb87906188b79777bcf42595d3623#506cb91f2e0fb87906188b79777bcf42595d3623" +version = "0.3.0-rc1" +source = "git+https://github.com/lightningdevkit/rust-lightning?rev=4219131b05e44663a1b417e4f6cf09c7740ead2a#4219131b05e44663a1b417e4f6cf09c7740ead2a" dependencies = [ "bech32", "bitcoin", @@ -1398,8 +1415,8 @@ dependencies = [ [[package]] name = "lightning-background-processor" -version = "0.3.0+git" -source = "git+https://github.com/lightningdevkit/rust-lightning?rev=506cb91f2e0fb87906188b79777bcf42595d3623#506cb91f2e0fb87906188b79777bcf42595d3623" +version = "0.3.0-rc1" +source = "git+https://github.com/lightningdevkit/rust-lightning?rev=4219131b05e44663a1b417e4f6cf09c7740ead2a#4219131b05e44663a1b417e4f6cf09c7740ead2a" dependencies = [ "bitcoin", "bitcoin-io", @@ -1412,8 +1429,8 @@ dependencies = [ [[package]] name = "lightning-block-sync" -version = "0.3.0+git" -source = "git+https://github.com/lightningdevkit/rust-lightning?rev=506cb91f2e0fb87906188b79777bcf42595d3623#506cb91f2e0fb87906188b79777bcf42595d3623" +version = "0.3.0-rc1" +source = "git+https://github.com/lightningdevkit/rust-lightning?rev=4219131b05e44663a1b417e4f6cf09c7740ead2a#4219131b05e44663a1b417e4f6cf09c7740ead2a" dependencies = [ "bitcoin", "bitreq", @@ -1424,8 +1441,8 @@ dependencies = [ [[package]] name = "lightning-dns-resolver" -version = "0.3.0+git" -source = "git+https://github.com/lightningdevkit/rust-lightning?rev=506cb91f2e0fb87906188b79777bcf42595d3623#506cb91f2e0fb87906188b79777bcf42595d3623" +version = "0.3.0-rc1" +source = "git+https://github.com/lightningdevkit/rust-lightning?rev=4219131b05e44663a1b417e4f6cf09c7740ead2a#4219131b05e44663a1b417e4f6cf09c7740ead2a" dependencies = [ "dnssec-prover", "lightning", @@ -1435,8 +1452,8 @@ dependencies = [ [[package]] name = "lightning-invoice" -version = "0.35.0+git" -source = "git+https://github.com/lightningdevkit/rust-lightning?rev=506cb91f2e0fb87906188b79777bcf42595d3623#506cb91f2e0fb87906188b79777bcf42595d3623" +version = "0.35.0-rc1" +source = "git+https://github.com/lightningdevkit/rust-lightning?rev=4219131b05e44663a1b417e4f6cf09c7740ead2a#4219131b05e44663a1b417e4f6cf09c7740ead2a" dependencies = [ "bech32", "bitcoin", @@ -1446,8 +1463,8 @@ dependencies = [ [[package]] name = "lightning-liquidity" -version = "0.3.0+git" -source = "git+https://github.com/lightningdevkit/rust-lightning?rev=506cb91f2e0fb87906188b79777bcf42595d3623#506cb91f2e0fb87906188b79777bcf42595d3623" +version = "0.3.0-rc1" +source = "git+https://github.com/lightningdevkit/rust-lightning?rev=4219131b05e44663a1b417e4f6cf09c7740ead2a#4219131b05e44663a1b417e4f6cf09c7740ead2a" dependencies = [ "bitcoin", "bitreq", @@ -1463,7 +1480,7 @@ dependencies = [ [[package]] name = "lightning-macros" version = "0.2.2+git" -source = "git+https://github.com/lightningdevkit/rust-lightning?rev=506cb91f2e0fb87906188b79777bcf42595d3623#506cb91f2e0fb87906188b79777bcf42595d3623" +source = "git+https://github.com/lightningdevkit/rust-lightning?rev=4219131b05e44663a1b417e4f6cf09c7740ead2a#4219131b05e44663a1b417e4f6cf09c7740ead2a" dependencies = [ "proc-macro2", "quote", @@ -1472,8 +1489,8 @@ dependencies = [ [[package]] name = "lightning-net-tokio" -version = "0.3.0+git" -source = "git+https://github.com/lightningdevkit/rust-lightning?rev=506cb91f2e0fb87906188b79777bcf42595d3623#506cb91f2e0fb87906188b79777bcf42595d3623" +version = "0.3.0-rc1" +source = "git+https://github.com/lightningdevkit/rust-lightning?rev=4219131b05e44663a1b417e4f6cf09c7740ead2a#4219131b05e44663a1b417e4f6cf09c7740ead2a" dependencies = [ "bitcoin", "lightning", @@ -1482,8 +1499,8 @@ dependencies = [ [[package]] name = "lightning-persister" -version = "0.3.0+git" -source = "git+https://github.com/lightningdevkit/rust-lightning?rev=506cb91f2e0fb87906188b79777bcf42595d3623#506cb91f2e0fb87906188b79777bcf42595d3623" +version = "0.3.0-rc1" +source = "git+https://github.com/lightningdevkit/rust-lightning?rev=4219131b05e44663a1b417e4f6cf09c7740ead2a#4219131b05e44663a1b417e4f6cf09c7740ead2a" dependencies = [ "bitcoin", "lightning", @@ -1493,8 +1510,8 @@ dependencies = [ [[package]] name = "lightning-rapid-gossip-sync" -version = "0.3.0+git" -source = "git+https://github.com/lightningdevkit/rust-lightning?rev=506cb91f2e0fb87906188b79777bcf42595d3623#506cb91f2e0fb87906188b79777bcf42595d3623" +version = "0.3.0-rc1" +source = "git+https://github.com/lightningdevkit/rust-lightning?rev=4219131b05e44663a1b417e4f6cf09c7740ead2a#4219131b05e44663a1b417e4f6cf09c7740ead2a" dependencies = [ "bitcoin", "bitcoin-io", @@ -1504,12 +1521,12 @@ dependencies = [ [[package]] name = "lightning-transaction-sync" -version = "0.3.0+git" -source = "git+https://github.com/lightningdevkit/rust-lightning?rev=506cb91f2e0fb87906188b79777bcf42595d3623#506cb91f2e0fb87906188b79777bcf42595d3623" +version = "0.3.0-rc1" +source = "git+https://github.com/lightningdevkit/rust-lightning?rev=4219131b05e44663a1b417e4f6cf09c7740ead2a#4219131b05e44663a1b417e4f6cf09c7740ead2a" dependencies = [ "bitcoin", "electrum-client", - "esplora-client", + "esplora-client 0.13.0", "futures", "lightning", "lightning-macros", @@ -1517,8 +1534,8 @@ dependencies = [ [[package]] name = "lightning-types" -version = "0.4.0+git" -source = "git+https://github.com/lightningdevkit/rust-lightning?rev=506cb91f2e0fb87906188b79777bcf42595d3623#506cb91f2e0fb87906188b79777bcf42595d3623" +version = "0.4.0-rc1" +source = "git+https://github.com/lightningdevkit/rust-lightning?rev=4219131b05e44663a1b417e4f6cf09c7740ead2a#4219131b05e44663a1b417e4f6cf09c7740ead2a" dependencies = [ "bitcoin", ] @@ -1674,7 +1691,7 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "possiblyrandom" version = "0.2.0" -source = "git+https://github.com/lightningdevkit/rust-lightning?rev=506cb91f2e0fb87906188b79777bcf42595d3623#506cb91f2e0fb87906188b79777bcf42595d3623" +source = "git+https://github.com/lightningdevkit/rust-lightning?rev=4219131b05e44663a1b417e4f6cf09c7740ead2a#4219131b05e44663a1b417e4f6cf09c7740ead2a" dependencies = [ "getrandom 0.2.17", ] @@ -2089,7 +2106,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2306,7 +2323,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2408,10 +2425,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2514,6 +2531,7 @@ dependencies = [ "bytes", "futures-core", "futures-sink", + "futures-util", "pin-project-lite", "tokio", ] diff --git a/e2e-tests/Cargo.toml b/e2e-tests/Cargo.toml index 461ba244..8bbb2700 100644 --- a/e2e-tests/Cargo.toml +++ b/e2e-tests/Cargo.toml @@ -21,4 +21,4 @@ ldk-server-client = { path = "../ldk-server-client" } ldk-server-grpc = { path = "../ldk-server-grpc", features = ["serde"] } serde_json = "1.0" hex-conservative = { version = "0.2", features = ["std"] } -ldk-node = { git = "https://github.com/lightningdevkit/ldk-node", rev = "056447c28221be02c3d39f8c6ae430a67ebbd850" } +ldk-node = { git = "https://github.com/lightningdevkit/ldk-node", rev = "b1337d2f1665716313d2ccf849a04114f8b3ca19" } diff --git a/e2e-tests/tests/e2e.rs b/e2e-tests/tests/e2e.rs index f5dd07dc..0f22d925 100644 --- a/e2e-tests/tests/e2e.rs +++ b/e2e-tests/tests/e2e.rs @@ -22,9 +22,10 @@ use ldk_node::lightning::ln::msgs::SocketAddress; use ldk_node::lightning::offers::offer::Offer; use ldk_node::lightning::offers::refund::Refund; use ldk_node::lightning_invoice::Bolt11Invoice; +use ldk_server_client::error::LdkServerErrorCode::InvalidRequestError; use ldk_server_client::ldk_server_grpc::api::{ - open_channel_request, Bolt11ReceiveRequest, Bolt12ReceiveRequest, GetBalancesRequest, - OnchainReceiveRequest, OpenChannelRequest, + open_channel_request, Bolt11ClaimForIdRequest, Bolt11FailForIdRequest, Bolt11ReceiveRequest, + Bolt12ReceiveRequest, GetBalancesRequest, OnchainReceiveRequest, OpenChannelRequest, }; use ldk_server_client::ldk_server_grpc::events::event_envelope::Event; use ldk_server_client::ldk_server_grpc::events::{ @@ -875,14 +876,21 @@ async fn test_cli_bolt11_send() { // Pay via CLI from A let output = run_cli(&server_a, &["bolt11-send", &invoice_resp.invoice]); - assert!(!output["payment_id"].as_str().unwrap().is_empty()); + let send_payment_id = output["payment_id"].as_str().unwrap(); + assert!(!send_payment_id.is_empty()); // Verify events let event_a = wait_for_event(&mut events_a, |e| matches!(e, Event::PaymentSuccessful(_))).await; - assert!(matches!(&event_a.event, Some(Event::PaymentSuccessful(_)))); + let Some(Event::PaymentSuccessful(successful)) = &event_a.event else { + panic!("expected PaymentSuccessful"); + }; + assert_eq!(successful.payment.as_ref().unwrap().id, send_payment_id); let event_b = wait_for_event(&mut events_b, |e| matches!(e, Event::PaymentReceived(_))).await; - assert!(matches!(&event_b.event, Some(Event::PaymentReceived(_)))); + let Some(Event::PaymentReceived(received)) = &event_b.event else { + panic!("expected PaymentReceived"); + }; + assert!(!received.payment.as_ref().unwrap().id.is_empty()); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] @@ -1059,14 +1067,21 @@ async fn test_cli_spontaneous_send() { setup_funded_channel(&bitcoind, &server_a, &server_b, 100_000).await; let output = run_cli(&server_a, &["spontaneous-send", server_b.node_id(), "10000sat"]); - assert!(!output["payment_id"].as_str().unwrap().is_empty()); + let send_payment_id = output["payment_id"].as_str().unwrap(); + assert!(!send_payment_id.is_empty()); // Verify events let event_a = wait_for_event(&mut events_a, |e| matches!(e, Event::PaymentSuccessful(_))).await; - assert!(matches!(&event_a.event, Some(Event::PaymentSuccessful(_)))); + let Some(Event::PaymentSuccessful(successful)) = &event_a.event else { + panic!("expected PaymentSuccessful"); + }; + assert_eq!(successful.payment.as_ref().unwrap().id, send_payment_id); let event_b = wait_for_event(&mut events_b, |e| matches!(e, Event::PaymentReceived(_))).await; - assert!(matches!(&event_b.event, Some(Event::PaymentReceived(_)))); + let Some(Event::PaymentReceived(received)) = &event_b.event else { + panic!("expected PaymentReceived"); + }; + assert!(!received.payment.as_ref().unwrap().id.is_empty()); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] @@ -1099,6 +1114,7 @@ async fn test_cli_spontaneous_send_with_custom_tlvs() { let Some(Event::PaymentReceived(pr)) = event_b.event else { panic!("expected PaymentReceived"); }; + assert!(!pr.payment.as_ref().unwrap().id.is_empty()); assert_eq!(pr.custom_records.len(), 2); let by_type: HashMap> = pr.custom_records.into_iter().map(|r| (r.type_num, r.value.to_vec())).collect(); @@ -1400,7 +1416,7 @@ async fn test_forwarded_payment_event() { let b_addr = SocketAddress::from_str(&format!("127.0.0.1:{}", server_b.p2p_port)).unwrap(); builder_c.add_liquidity_source(b_node_id, b_addr, None, true); - let mnemonic_c = ldk_node::entropy::generate_entropy_mnemonic(None); + let mnemonic_c = ldk_node::bip39::Mnemonic::generate(24).unwrap(); let node_entropy_c = ldk_node::entropy::NodeEntropy::from_bip39_mnemonic(mnemonic_c, None); let node_c = builder_c.build(node_entropy_c).unwrap(); @@ -1448,6 +1464,11 @@ async fn test_forwarded_payment_event() { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_hodl_invoice_claim() { + enum InvalidClaim { + WrongPreimage, + InsufficientAmount, + } + let bitcoind = TestBitcoind::new(); let server_a = LdkServerHandle::start(&bitcoind).await; let server_b = LdkServerHandle::start(&bitcoind).await; @@ -1457,15 +1478,15 @@ async fn test_hodl_invoice_claim() { setup_funded_channel(&bitcoind, &server_a, &server_b, 100_000).await; - // Test three claim variants: (preimage, amount, hash) - let test_cases: Vec<([u8; 32], Option<&str>, bool)> = vec![ - ([42u8; 32], Some("10000000msat"), true), // all args - ([44u8; 32], Some("10000000msat"), false), // preimage + amount - ([45u8; 32], None, true), // preimage + hash - ([46u8; 32], None, false), // preimage only + // Test optional amount verification and rejected claim inputs. + let test_cases = [ + ([42u8; 32], Some("10000000msat"), None), + ([44u8; 32], None, None), + ([45u8; 32], Some("10000000msat"), Some(InvalidClaim::WrongPreimage)), + ([46u8; 32], Some("10000000msat"), Some(InvalidClaim::InsufficientAmount)), ]; - for (preimage_bytes, amount, include_hash) in &test_cases { + for (preimage_bytes, amount, invalid_claim) in &test_cases { let preimage_hex = preimage_bytes.to_lower_hex_string(); let payment_hash_hex = sha256::Hash::hash(preimage_bytes).to_byte_array().to_lower_hex_string(); @@ -1491,25 +1512,45 @@ async fn test_hodl_invoice_claim() { // Wait for PaymentClaimable event on B (drain other events) let claimable = wait_for_event(&mut events_b, |e| matches!(e, Event::PaymentClaimable(_))).await; - assert!(matches!( - &claimable.event, - Some(Event::PaymentClaimable(event)) if event.claim_deadline.is_some() - )); + let Some(Event::PaymentClaimable(claimable_event)) = &claimable.event else { + panic!("expected PaymentClaimable"); + }; + assert!(claimable_event.claim_deadline.is_some()); + assert!(!claimable_event.payment.as_ref().unwrap().id.is_empty()); + + if let Some(invalid_claim) = invalid_claim { + let invalid_preimage = [99u8; 32].to_lower_hex_string(); + let (attempted_preimage, attempted_amount) = match invalid_claim { + InvalidClaim::WrongPreimage => (&invalid_preimage, Some(10_000_000)), + InvalidClaim::InsufficientAmount => (&preimage_hex, Some(9_999_999)), + }; + let error = server_b + .client() + .bolt11_claim_for_id(Bolt11ClaimForIdRequest { + payment_id: claimable_event.payment.as_ref().unwrap().id.clone(), + claimable_amount_msat: attempted_amount, + preimage: attempted_preimage.clone(), + }) + .await + .unwrap_err(); + assert_eq!(error.error_code, InvalidRequestError); + } // Claim the payment on B - let mut args: Vec<&str> = vec!["bolt11-claim-for-hash", &preimage_hex]; + let mut args: Vec<&str> = + vec!["bolt11-claim-for-id", &claimable_event.payment.as_ref().unwrap().id, &preimage_hex]; if let Some(amt) = amount { args.extend(["-c", amt]); } - if *include_hash { - args.extend(["-p", &payment_hash_hex]); - } run_cli(&server_b, &args); // Wait for PaymentSuccessful on A after claim (drain other events) let successful = wait_for_event(&mut events_a, |e| matches!(e, Event::PaymentSuccessful(_))).await; - assert!(matches!(&successful.event, Some(Event::PaymentSuccessful(_)))); + let Some(Event::PaymentSuccessful(event)) = &successful.event else { + panic!("expected PaymentSuccessful"); + }; + assert!(!event.payment.as_ref().unwrap().id.is_empty()); } } @@ -1549,22 +1590,32 @@ async fn test_hodl_invoice_fail() { // Verify PaymentClaimable event on B let event_b = wait_for_event(&mut events_b, |e| matches!(e, Event::PaymentClaimable(_))).await; - assert!(matches!(&event_b.event, Some(Event::PaymentClaimable(_)))); + let Some(Event::PaymentClaimable(claimable)) = &event_b.event else { + panic!("expected PaymentClaimable"); + }; + assert!(!claimable.payment.as_ref().unwrap().id.is_empty()); + let unknown_payment_id = "00".repeat(32); + assert_ne!(claimable.payment.as_ref().unwrap().id, unknown_payment_id); + let error = server_b + .client() + .bolt11_fail_for_id(Bolt11FailForIdRequest { payment_id: unknown_payment_id }) + .await + .unwrap_err(); + assert_eq!(error.error_code, InvalidRequestError); // Fail the payment on B using CLI - run_cli(&server_b, &["bolt11-fail-for-hash", &payment_hash_hex]); + run_cli(&server_b, &["bolt11-fail-for-id", &claimable.payment.as_ref().unwrap().id]); // Verify PaymentFailed on A and its failure reason. let event_a = wait_for_event(&mut events_a, |e| matches!(e, Event::PaymentFailed(_))).await; - match &event_a.event { - Some(Event::PaymentFailed(payment_failed)) => { - assert_eq!( - payment_failed.reason, - Some(PaymentFailureReason::RecipientRejected as i32) - ); - }, - other => panic!("expected PaymentFailed event, got {other:?}"), - } + let Some(Event::PaymentFailed(failed)) = &event_a.event else { + panic!("expected PaymentFailed"); + }; + assert!(!failed.payment.as_ref().unwrap().id.is_empty()); + assert_eq!( + failed.reason, + Some(PaymentFailureReason::RecipientRejected as i32) + ); } #[tokio::test] @@ -1724,8 +1775,8 @@ async fn test_cli_spontaneous_send_with_preimage() { let Some(Event::PaymentReceived(pr)) = event_b.event else { panic!("expected PaymentReceived"); }; - let payment = pr.payment.unwrap(); + assert!(!payment.id.is_empty()); let Some(payment_kind::Kind::Spontaneous(spont)) = payment.kind.unwrap().kind else { panic!("expected spontaneous kind"); diff --git a/ldk-server-cli/src/main.rs b/ldk-server-cli/src/main.rs index 630a5084..95cc4cad 100644 --- a/ldk-server-cli/src/main.rs +++ b/ldk-server-cli/src/main.rs @@ -24,8 +24,8 @@ use ldk_server_client::error::LdkServerErrorCode::{ }; use ldk_server_client::ldk_server_grpc::api::{ onchain_send_request, open_channel_request, splice_in_request, AllFunds, - Bolt11ClaimForHashRequest, Bolt11ClaimForHashResponse, Bolt11FailForHashRequest, - Bolt11FailForHashResponse, Bolt11ReceiveForHashRequest, Bolt11ReceiveForHashResponse, + Bolt11ClaimForIdRequest, Bolt11ClaimForIdResponse, Bolt11FailForIdRequest, + Bolt11FailForIdResponse, Bolt11ReceiveForHashRequest, Bolt11ReceiveForHashResponse, Bolt11ReceiveRequest, Bolt11ReceiveResponse, Bolt11ReceiveVariableAmountViaJitChannelRequest, Bolt11ReceiveVariableAmountViaJitChannelResponse, Bolt11ReceiveViaJitChannelRequest, Bolt11ReceiveViaJitChannelResponse, Bolt11SendRequest, Bolt11SendResponse, @@ -163,7 +163,9 @@ enum Commands { expiry_secs: Option, }, #[command(about = "Claim a held payment by providing the preimage")] - Bolt11ClaimForHash { + Bolt11ClaimForId { + #[arg(help = "The hex-encoded 32-byte payment ID from PaymentClaimable")] + payment_id: String, #[arg(help = "The hex-encoded 32-byte payment preimage")] preimage: String, #[arg( @@ -172,17 +174,11 @@ enum Commands { help = "The claimable amount, e.g. 50sat or 50000msat, only used for verifying we are claiming the expected amount" )] claimable_amount: Option, - #[arg( - short, - long, - help = "The hex-encoded 32-byte payment hash, used to verify the preimage matches" - )] - payment_hash: Option, }, #[command(about = "Fail/reject a held payment")] - Bolt11FailForHash { - #[arg(help = "The hex-encoded 32-byte payment hash")] - payment_hash: String, + Bolt11FailForId { + #[arg(help = "The hex-encoded 32-byte payment ID from PaymentClaimable")] + payment_id: String, }, #[command(about = "Create a fixed-amount BOLT11 invoice to receive via an LSPS2 JIT channel")] Bolt11ReceiveViaJitChannel { @@ -763,20 +759,20 @@ async fn main() { client.bolt11_receive_for_hash(request).await, ); }, - Commands::Bolt11ClaimForHash { preimage, claimable_amount, payment_hash } => { - handle_response_result::<_, Bolt11ClaimForHashResponse>( + Commands::Bolt11ClaimForId { payment_id, preimage, claimable_amount } => { + handle_response_result::<_, Bolt11ClaimForIdResponse>( client - .bolt11_claim_for_hash(Bolt11ClaimForHashRequest { - payment_hash, + .bolt11_claim_for_id(Bolt11ClaimForIdRequest { + payment_id, claimable_amount_msat: claimable_amount.map(|a| a.to_msat()), preimage, }) .await, ); }, - Commands::Bolt11FailForHash { payment_hash } => { - handle_response_result::<_, Bolt11FailForHashResponse>( - client.bolt11_fail_for_hash(Bolt11FailForHashRequest { payment_hash }).await, + Commands::Bolt11FailForId { payment_id } => { + handle_response_result::<_, Bolt11FailForIdResponse>( + client.bolt11_fail_for_id(Bolt11FailForIdRequest { payment_id }).await, ); }, Commands::Bolt11ReceiveViaJitChannel { diff --git a/ldk-server-client/src/client.rs b/ldk-server-client/src/client.rs index 18962c52..6082ad2a 100644 --- a/ldk-server-client/src/client.rs +++ b/ldk-server-client/src/client.rs @@ -16,8 +16,8 @@ use hyper::body::HttpBody as _; use hyper::{Body as HyperBody, Client as HyperClient, Request as HyperRequest, Version}; use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; use ldk_server_grpc::api::{ - Bolt11ClaimForHashRequest, Bolt11ClaimForHashResponse, Bolt11FailForHashRequest, - Bolt11FailForHashResponse, Bolt11ReceiveForHashRequest, Bolt11ReceiveForHashResponse, + Bolt11ClaimForIdRequest, Bolt11ClaimForIdResponse, Bolt11FailForIdRequest, + Bolt11FailForIdResponse, Bolt11ReceiveForHashRequest, Bolt11ReceiveForHashResponse, Bolt11ReceiveRequest, Bolt11ReceiveResponse, Bolt11ReceiveVariableAmountViaJitChannelRequest, Bolt11ReceiveVariableAmountViaJitChannelResponse, Bolt11ReceiveViaJitChannelRequest, Bolt11ReceiveViaJitChannelResponse, Bolt11SendRequest, Bolt11SendResponse, @@ -42,7 +42,7 @@ use ldk_server_grpc::api::{ VerifySignatureResponse, }; use ldk_server_grpc::endpoints::{ - BOLT11_CLAIM_FOR_HASH_PATH, BOLT11_FAIL_FOR_HASH_PATH, BOLT11_RECEIVE_FOR_HASH_PATH, + BOLT11_CLAIM_FOR_ID_PATH, BOLT11_FAIL_FOR_ID_PATH, BOLT11_RECEIVE_FOR_HASH_PATH, BOLT11_RECEIVE_PATH, BOLT11_RECEIVE_VARIABLE_AMOUNT_VIA_JIT_CHANNEL_PATH, BOLT11_RECEIVE_VIA_JIT_CHANNEL_PATH, BOLT11_SEND_PATH, BOLT11_SEND_UNDERPAYING_PATH, BOLT12_RECEIVE_PATH, BOLT12_RECEIVE_REFUND_PATH, BOLT12_SEND_PATH, BOLT12_SEND_REFUND_PATH, @@ -210,18 +210,18 @@ impl LdkServerClient { self.grpc_unary(&request, BOLT11_RECEIVE_FOR_HASH_PATH).await } - /// Manually claim a payment for a given payment hash. - pub async fn bolt11_claim_for_hash( - &self, request: Bolt11ClaimForHashRequest, - ) -> Result { - self.grpc_unary(&request, BOLT11_CLAIM_FOR_HASH_PATH).await + /// Manually claim a payment for a given payment ID. + pub async fn bolt11_claim_for_id( + &self, request: Bolt11ClaimForIdRequest, + ) -> Result { + self.grpc_unary(&request, BOLT11_CLAIM_FOR_ID_PATH).await } - /// Manually fail a payment for a given payment hash. - pub async fn bolt11_fail_for_hash( - &self, request: Bolt11FailForHashRequest, - ) -> Result { - self.grpc_unary(&request, BOLT11_FAIL_FOR_HASH_PATH).await + /// Manually fail a payment for a given payment ID. + pub async fn bolt11_fail_for_id( + &self, request: Bolt11FailForIdRequest, + ) -> Result { + self.grpc_unary(&request, BOLT11_FAIL_FOR_ID_PATH).await } /// Retrieve a new fixed-amount BOLT11 invoice for receiving via an LSPS2 JIT channel. diff --git a/ldk-server-grpc/src/api.rs b/ldk-server-grpc/src/api.rs index a8a6b9f3..17d15a41 100644 --- a/ldk-server-grpc/src/api.rs +++ b/ldk-server-grpc/src/api.rs @@ -205,8 +205,8 @@ pub struct Bolt11ReceiveResponse { } /// Return a BOLT11 payable invoice for a given payment hash. /// The inbound payment will NOT be automatically claimed upon arrival. -/// Instead, the payment will need to be manually claimed by calling `Bolt11ClaimForHash` -/// or manually failed by calling `Bolt11FailForHash`. +/// Instead, the payment will need to be manually claimed by calling `Bolt11ClaimForId` +/// or manually failed by calling `Bolt11FailForId`. /// See more: /// - /// - @@ -227,6 +227,7 @@ pub struct Bolt11ReceiveForHashRequest { #[prost(uint32, tag = "3")] pub expiry_secs: u32, /// The hex-encoded 32-byte payment hash to use for the invoice. + /// Use a new payment hash for each invoice. Reuse is unsafe and can cause loss of funds. #[prost(string, tag = "4")] pub payment_hash: ::prost::alloc::string::String, } @@ -243,19 +244,18 @@ pub struct Bolt11ReceiveForHashResponse { #[prost(string, tag = "1")] pub invoice: ::prost::alloc::string::String, } -/// Manually claim a payment for a given payment hash with the corresponding preimage. +/// Manually claim a payment for a given payment ID with the corresponding preimage. /// This should be used to claim payments created via `Bolt11ReceiveForHash`. -/// See more: +/// See more: #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] #[cfg_attr(feature = "serde", serde(default))] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct Bolt11ClaimForHashRequest { - /// The hex-encoded 32-byte payment hash. - /// If provided, it will be used to verify that the preimage matches. - #[prost(string, optional, tag = "1")] - pub payment_hash: ::core::option::Option<::prost::alloc::string::String>, +pub struct Bolt11ClaimForIdRequest { + /// The hex-encoded 32-byte payment ID from `PaymentClaimable`. + #[prost(string, tag = "1")] + pub payment_id: ::prost::alloc::string::String, /// The amount in millisatoshi that is claimable. /// If not provided, skips amount verification. #[prost(uint64, optional, tag = "2")] @@ -264,33 +264,33 @@ pub struct Bolt11ClaimForHashRequest { #[prost(string, tag = "3")] pub preimage: ::prost::alloc::string::String, } -/// The response for the `Bolt11ClaimForHash` RPC. On failure, a gRPC error status is returned. +/// The response for the `Bolt11ClaimForId` RPC. On failure, a gRPC error status is returned. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] #[cfg_attr(feature = "serde", serde(default))] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct Bolt11ClaimForHashResponse {} -/// Manually fail a payment for a given payment hash. +pub struct Bolt11ClaimForIdResponse {} +/// Manually fail a payment for a given payment ID. /// This should be used to reject payments created via `Bolt11ReceiveForHash`. -/// See more: +/// See more: #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] #[cfg_attr(feature = "serde", serde(default))] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct Bolt11FailForHashRequest { - /// The hex-encoded 32-byte payment hash. +pub struct Bolt11FailForIdRequest { + /// The hex-encoded 32-byte payment ID from `PaymentClaimable`. #[prost(string, tag = "1")] - pub payment_hash: ::prost::alloc::string::String, + pub payment_id: ::prost::alloc::string::String, } -/// The response for the `Bolt11FailForHash` RPC. On failure, a gRPC error status is returned. +/// The response for the `Bolt11FailForId` RPC. On failure, a gRPC error status is returned. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] #[cfg_attr(feature = "serde", serde(default))] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct Bolt11FailForHashResponse {} +pub struct Bolt11FailForIdResponse {} /// Return a BOLT11 payable invoice that can be used to request and receive a payment via an /// LSPS2 just-in-time channel. /// See more: diff --git a/ldk-server-grpc/src/endpoints.rs b/ldk-server-grpc/src/endpoints.rs index e9fe3f16..84d4e58e 100644 --- a/ldk-server-grpc/src/endpoints.rs +++ b/ldk-server-grpc/src/endpoints.rs @@ -16,8 +16,8 @@ pub const ONCHAIN_RECEIVE_PATH: &str = "OnchainReceive"; pub const ONCHAIN_SEND_PATH: &str = "OnchainSend"; pub const BOLT11_RECEIVE_PATH: &str = "Bolt11Receive"; pub const BOLT11_RECEIVE_FOR_HASH_PATH: &str = "Bolt11ReceiveForHash"; -pub const BOLT11_CLAIM_FOR_HASH_PATH: &str = "Bolt11ClaimForHash"; -pub const BOLT11_FAIL_FOR_HASH_PATH: &str = "Bolt11FailForHash"; +pub const BOLT11_CLAIM_FOR_ID_PATH: &str = "Bolt11ClaimForId"; +pub const BOLT11_FAIL_FOR_ID_PATH: &str = "Bolt11FailForId"; pub const BOLT11_RECEIVE_VIA_JIT_CHANNEL_PATH: &str = "Bolt11ReceiveViaJitChannel"; pub const BOLT11_RECEIVE_VARIABLE_AMOUNT_VIA_JIT_CHANNEL_PATH: &str = "Bolt11ReceiveVariableAmountViaJitChannel"; diff --git a/ldk-server-grpc/src/proto/api.proto b/ldk-server-grpc/src/proto/api.proto index dc1ef97a..6837cca6 100644 --- a/ldk-server-grpc/src/proto/api.proto +++ b/ldk-server-grpc/src/proto/api.proto @@ -161,8 +161,8 @@ message Bolt11ReceiveResponse { // Return a BOLT11 payable invoice for a given payment hash. // The inbound payment will NOT be automatically claimed upon arrival. -// Instead, the payment will need to be manually claimed by calling `Bolt11ClaimForHash` -// or manually failed by calling `Bolt11FailForHash`. +// Instead, the payment will need to be manually claimed by calling `Bolt11ClaimForId` +// or manually failed by calling `Bolt11FailForId`. // See more: // - https://docs.rs/ldk-node/latest/ldk_node/payment/struct.Bolt11Payment.html#method.receive_for_hash // - https://docs.rs/ldk-node/latest/ldk_node/payment/struct.Bolt11Payment.html#method.receive_variable_amount_for_hash @@ -179,6 +179,7 @@ message Bolt11ReceiveForHashRequest { uint32 expiry_secs = 3; // The hex-encoded 32-byte payment hash to use for the invoice. + // Use a new payment hash for each invoice. Reuse is unsafe and can cause loss of funds. string payment_hash = 4; } @@ -191,14 +192,13 @@ message Bolt11ReceiveForHashResponse { string invoice = 1; } -// Manually claim a payment for a given payment hash with the corresponding preimage. +// Manually claim a payment for a given payment ID with the corresponding preimage. // This should be used to claim payments created via `Bolt11ReceiveForHash`. -// See more: https://docs.rs/ldk-node/latest/ldk_node/payment/struct.Bolt11Payment.html#method.claim_for_hash -message Bolt11ClaimForHashRequest { +// See more: https://docs.rs/ldk-node/latest/ldk_node/payment/struct.Bolt11Payment.html#method.claim_for_id +message Bolt11ClaimForIdRequest { - // The hex-encoded 32-byte payment hash. - // If provided, it will be used to verify that the preimage matches. - optional string payment_hash = 1; + // The hex-encoded 32-byte payment ID from `PaymentClaimable`. + string payment_id = 1; // The amount in millisatoshi that is claimable. // If not provided, skips amount verification. @@ -208,20 +208,20 @@ message Bolt11ClaimForHashRequest { string preimage = 3; } -// The response for the `Bolt11ClaimForHash` RPC. On failure, a gRPC error status is returned. -message Bolt11ClaimForHashResponse {} +// The response for the `Bolt11ClaimForId` RPC. On failure, a gRPC error status is returned. +message Bolt11ClaimForIdResponse {} -// Manually fail a payment for a given payment hash. +// Manually fail a payment for a given payment ID. // This should be used to reject payments created via `Bolt11ReceiveForHash`. -// See more: https://docs.rs/ldk-node/latest/ldk_node/payment/struct.Bolt11Payment.html#method.fail_for_hash -message Bolt11FailForHashRequest { +// See more: https://docs.rs/ldk-node/latest/ldk_node/payment/struct.Bolt11Payment.html#method.fail_for_id +message Bolt11FailForIdRequest { - // The hex-encoded 32-byte payment hash. - string payment_hash = 1; + // The hex-encoded 32-byte payment ID from `PaymentClaimable`. + string payment_id = 1; } -// The response for the `Bolt11FailForHash` RPC. On failure, a gRPC error status is returned. -message Bolt11FailForHashResponse {} +// The response for the `Bolt11FailForId` RPC. On failure, a gRPC error status is returned. +message Bolt11FailForIdResponse {} // Return a BOLT11 payable invoice that can be used to request and receive a payment via an // LSPS2 just-in-time channel. @@ -1008,10 +1008,10 @@ service LightningNode { rpc Bolt11Receive(Bolt11ReceiveRequest) returns (Bolt11ReceiveResponse); // Return a BOLT11 payable invoice for a given payment hash. rpc Bolt11ReceiveForHash(Bolt11ReceiveForHashRequest) returns (Bolt11ReceiveForHashResponse); - // Manually claim a payment for a given payment hash. - rpc Bolt11ClaimForHash(Bolt11ClaimForHashRequest) returns (Bolt11ClaimForHashResponse); - // Manually fail a payment for a given payment hash. - rpc Bolt11FailForHash(Bolt11FailForHashRequest) returns (Bolt11FailForHashResponse); + // Manually claim a payment for a given payment ID. + rpc Bolt11ClaimForId(Bolt11ClaimForIdRequest) returns (Bolt11ClaimForIdResponse); + // Manually fail a payment for a given payment ID. + rpc Bolt11FailForId(Bolt11FailForIdRequest) returns (Bolt11FailForIdResponse); // Return a BOLT11 invoice for receiving via a JIT channel. rpc Bolt11ReceiveViaJitChannel(Bolt11ReceiveViaJitChannelRequest) returns (Bolt11ReceiveViaJitChannelResponse); // Return a variable-amount BOLT11 invoice for receiving via a JIT channel. diff --git a/ldk-server-mcp/src/tools/handlers.rs b/ldk-server-mcp/src/tools/handlers.rs index 5c9cf16a..b0e10132 100644 --- a/ldk-server-mcp/src/tools/handlers.rs +++ b/ldk-server-mcp/src/tools/handlers.rs @@ -10,7 +10,7 @@ use hex_conservative::DisplayHex; use ldk_server_client::client::LdkServerClient; use ldk_server_client::ldk_server_grpc::api::{ - Bolt11ClaimForHashRequest, Bolt11FailForHashRequest, Bolt11ReceiveForHashRequest, + Bolt11ClaimForIdRequest, Bolt11FailForIdRequest, Bolt11ReceiveForHashRequest, Bolt11ReceiveRequest, Bolt11ReceiveVariableAmountViaJitChannelRequest, Bolt11ReceiveViaJitChannelRequest, Bolt11SendRequest, Bolt11SendUnderpayingRequest, Bolt12ReceiveRefundRequest, Bolt12ReceiveRequest, Bolt12SendRefundRequest, Bolt12SendRequest, @@ -169,19 +169,19 @@ pub async fn handle_bolt11_receive_for_hash( serialize_response(response) } -pub async fn handle_bolt11_claim_for_hash( +pub async fn handle_bolt11_claim_for_id( client: &LdkServerClient, args: Value, ) -> Result { - let request: Bolt11ClaimForHashRequest = parse_request(args)?; - let response = client.bolt11_claim_for_hash(request).await.map_err(McpError::from)?; + let request: Bolt11ClaimForIdRequest = parse_request(args)?; + let response = client.bolt11_claim_for_id(request).await.map_err(McpError::from)?; serialize_response(response) } -pub async fn handle_bolt11_fail_for_hash( +pub async fn handle_bolt11_fail_for_id( client: &LdkServerClient, args: Value, ) -> Result { - let request: Bolt11FailForHashRequest = parse_request(args)?; - let response = client.bolt11_fail_for_hash(request).await.map_err(McpError::from)?; + let request: Bolt11FailForIdRequest = parse_request(args)?; + let response = client.bolt11_fail_for_id(request).await.map_err(McpError::from)?; serialize_response(response) } diff --git a/ldk-server-mcp/src/tools/mod.rs b/ldk-server-mcp/src/tools/mod.rs index a60d6734..40706863 100644 --- a/ldk-server-mcp/src/tools/mod.rs +++ b/ldk-server-mcp/src/tools/mod.rs @@ -106,16 +106,16 @@ pub fn build_tool_registry() -> ToolRegistry { |client, args| Box::pin(handlers::handle_bolt11_receive_for_hash(client, args)), ), tool_spec( - "bolt11_claim_for_hash", - "Manually claim a BOLT11 payment for a specific payment hash", - schema::bolt11_claim_for_hash_schema, - |client, args| Box::pin(handlers::handle_bolt11_claim_for_hash(client, args)), + "bolt11_claim_for_id", + "Manually claim a BOLT11 payment for a specific payment ID", + schema::bolt11_claim_for_id_schema, + |client, args| Box::pin(handlers::handle_bolt11_claim_for_id(client, args)), ), tool_spec( - "bolt11_fail_for_hash", - "Manually fail a BOLT11 payment for a specific payment hash", - schema::bolt11_fail_for_hash_schema, - |client, args| Box::pin(handlers::handle_bolt11_fail_for_hash(client, args)), + "bolt11_fail_for_id", + "Manually fail a BOLT11 payment for a specific payment ID", + schema::bolt11_fail_for_id_schema, + |client, args| Box::pin(handlers::handle_bolt11_fail_for_id(client, args)), ), tool_spec( "bolt11_receive_via_jit_channel", diff --git a/ldk-server-mcp/src/tools/schema.rs b/ldk-server-mcp/src/tools/schema.rs index e2fb9ec4..ff7b7be3 100644 --- a/ldk-server-mcp/src/tools/schema.rs +++ b/ldk-server-mcp/src/tools/schema.rs @@ -218,13 +218,13 @@ pub fn bolt11_receive_for_hash_schema() -> Value { }) } -pub fn bolt11_claim_for_hash_schema() -> Value { +pub fn bolt11_claim_for_id_schema() -> Value { json!({ "type": "object", "properties": { - "payment_hash": { + "payment_id": { "type": "string", - "description": "The hex-encoded 32-byte payment hash. If provided, verifies that the preimage matches" + "description": "The hex-encoded 32-byte payment ID from PaymentClaimable" }, "claimable_amount_msat": { "type": "integer", @@ -235,20 +235,20 @@ pub fn bolt11_claim_for_hash_schema() -> Value { "description": "The hex-encoded 32-byte payment preimage" } }, - "required": ["preimage"] + "required": ["payment_id", "preimage"] }) } -pub fn bolt11_fail_for_hash_schema() -> Value { +pub fn bolt11_fail_for_id_schema() -> Value { json!({ "type": "object", "properties": { - "payment_hash": { + "payment_id": { "type": "string", - "description": "The hex-encoded 32-byte payment hash" + "description": "The hex-encoded 32-byte payment ID from PaymentClaimable" } }, - "required": ["payment_hash"] + "required": ["payment_id"] }) } diff --git a/ldk-server-mcp/tests/integration.rs b/ldk-server-mcp/tests/integration.rs index fb229f23..128d878f 100644 --- a/ldk-server-mcp/tests/integration.rs +++ b/ldk-server-mcp/tests/integration.rs @@ -13,8 +13,8 @@ use serde_json::{json, Value}; const NUM_TOOLS: usize = 40; const EXPECTED_TOOLS: [&str; NUM_TOOLS] = [ - "bolt11_claim_for_hash", - "bolt11_fail_for_hash", + "bolt11_claim_for_id", + "bolt11_fail_for_id", "bolt11_receive", "bolt11_receive_for_hash", "bolt11_receive_variable_amount_via_jit_channel", @@ -296,19 +296,19 @@ fn test_bolt11_receive_for_hash_unreachable() { } #[test] -fn test_bolt11_claim_for_hash_unreachable() { +fn test_bolt11_claim_for_id_unreachable() { assert_unreachable_tool( - "bolt11_claim_for_hash", + "bolt11_claim_for_id", json!({ - "payment_hash": "11".repeat(32), + "payment_id": "11".repeat(32), "preimage": "22".repeat(32) }), ); } #[test] -fn test_bolt11_fail_for_hash_unreachable() { - assert_unreachable_tool("bolt11_fail_for_hash", json!({ "payment_hash": "33".repeat(32) })); +fn test_bolt11_fail_for_id_unreachable() { + assert_unreachable_tool("bolt11_fail_for_id", json!({ "payment_id": "33".repeat(32) })); } #[test] diff --git a/ldk-server/Cargo.toml b/ldk-server/Cargo.toml index 2a9783d0..5590d97f 100644 --- a/ldk-server/Cargo.toml +++ b/ldk-server/Cargo.toml @@ -13,7 +13,7 @@ keywords = ["bitcoin", "lightning", "ldk", "server"] categories = ["cryptography::cryptocurrencies"] [dependencies] -ldk-node = { git = "https://github.com/lightningdevkit/ldk-node", rev = "056447c28221be02c3d39f8c6ae430a67ebbd850" } +ldk-node = { git = "https://github.com/lightningdevkit/ldk-node", rev = "b1337d2f1665716313d2ccf849a04114f8b3ca19" } serde = { version = "1.0.203", default-features = false, features = ["derive"] } hyper = { version = "1", default-features = false, features = ["server", "http2"] } http-body-util = { version = "0.1", default-features = false } diff --git a/ldk-server/src/api/bolt11_claim_for_hash.rs b/ldk-server/src/api/bolt11_claim_for_id.rs similarity index 50% rename from ldk-server/src/api/bolt11_claim_for_hash.rs rename to ldk-server/src/api/bolt11_claim_for_id.rs index d68cfb18..fa1dc320 100644 --- a/ldk-server/src/api/bolt11_claim_for_hash.rs +++ b/ldk-server/src/api/bolt11_claim_for_id.rs @@ -10,17 +10,18 @@ use std::sync::Arc; use hex::FromHex; -use ldk_node::bitcoin::hashes::{sha256, Hash}; -use ldk_node::lightning_types::payment::{PaymentHash, PaymentPreimage}; -use ldk_server_grpc::api::{Bolt11ClaimForHashRequest, Bolt11ClaimForHashResponse}; +use ldk_node::lightning_types::payment::PaymentPreimage; +use ldk_server_grpc::api::{Bolt11ClaimForIdRequest, Bolt11ClaimForIdResponse}; use crate::api::error::LdkServerError; use crate::api::error::LdkServerErrorCode::InvalidRequestError; use crate::service::Context; -pub(crate) async fn handle_bolt11_claim_for_hash_request( - context: Arc, request: Bolt11ClaimForHashRequest, -) -> Result { +pub(crate) async fn handle_bolt11_claim_for_id_request( + context: Arc, request: Bolt11ClaimForIdRequest, +) -> Result { + let payment_id = crate::api::parse_payment_id(&request.payment_id)?; + let preimage_bytes = <[u8; 32]>::from_hex(&request.preimage).map_err(|_| { LdkServerError::new( InvalidRequestError, @@ -29,21 +30,8 @@ pub(crate) async fn handle_bolt11_claim_for_hash_request( })?; let preimage = PaymentPreimage(preimage_bytes); - let payment_hash = if let Some(hash_hex) = &request.payment_hash { - let hash_bytes = <[u8; 32]>::from_hex(hash_hex).map_err(|_| { - LdkServerError::new( - InvalidRequestError, - "Invalid payment_hash, must be a 32-byte hex string.".to_string(), - ) - })?; - PaymentHash(hash_bytes) - } else { - PaymentHash(sha256::Hash::hash(&preimage.0).to_byte_array()) - }; - let claimable_amount_msat = request.claimable_amount_msat.unwrap_or(u64::MAX); + context.node.bolt11_payment().claim_for_id(payment_id, claimable_amount_msat, preimage)?; - context.node.bolt11_payment().claim_for_hash(payment_hash, claimable_amount_msat, preimage)?; - - Ok(Bolt11ClaimForHashResponse {}) + Ok(Bolt11ClaimForIdResponse {}) } diff --git a/ldk-server/src/api/bolt11_fail_for_hash.rs b/ldk-server/src/api/bolt11_fail_for_hash.rs deleted file mode 100644 index e65527fe..00000000 --- a/ldk-server/src/api/bolt11_fail_for_hash.rs +++ /dev/null @@ -1,34 +0,0 @@ -// This file is Copyright its original authors, visible in version control -// history. -// -// This file is licensed under the Apache License, Version 2.0 or the MIT license -// , at your option. -// You may not use this file except in accordance with one or both of these -// licenses. - -use std::sync::Arc; - -use hex::FromHex; -use ldk_node::lightning_types::payment::PaymentHash; -use ldk_server_grpc::api::{Bolt11FailForHashRequest, Bolt11FailForHashResponse}; - -use crate::api::error::LdkServerError; -use crate::api::error::LdkServerErrorCode::InvalidRequestError; -use crate::service::Context; - -pub(crate) async fn handle_bolt11_fail_for_hash_request( - context: Arc, request: Bolt11FailForHashRequest, -) -> Result { - let hash_bytes = <[u8; 32]>::from_hex(&request.payment_hash).map_err(|_| { - LdkServerError::new( - InvalidRequestError, - "Invalid payment_hash, must be a 32-byte hex string.".to_string(), - ) - })?; - let payment_hash = PaymentHash(hash_bytes); - - context.node.bolt11_payment().fail_for_hash(payment_hash)?; - - Ok(Bolt11FailForHashResponse {}) -} diff --git a/ldk-server/src/api/bolt11_fail_for_id.rs b/ldk-server/src/api/bolt11_fail_for_id.rs new file mode 100644 index 00000000..8b81a955 --- /dev/null +++ b/ldk-server/src/api/bolt11_fail_for_id.rs @@ -0,0 +1,25 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +use std::sync::Arc; + +use ldk_server_grpc::api::{Bolt11FailForIdRequest, Bolt11FailForIdResponse}; + +use crate::api::error::LdkServerError; +use crate::service::Context; + +pub(crate) async fn handle_bolt11_fail_for_id_request( + context: Arc, request: Bolt11FailForIdRequest, +) -> Result { + let payment_id = crate::api::parse_payment_id(&request.payment_id)?; + + context.node.bolt11_payment().fail_for_id(payment_id)?; + + Ok(Bolt11FailForIdResponse {}) +} diff --git a/ldk-server/src/api/error.rs b/ldk-server/src/api/error.rs index eef727f2..13509e3c 100644 --- a/ldk-server/src/api/error.rs +++ b/ldk-server/src/api/error.rs @@ -72,6 +72,7 @@ impl From for LdkServerError { | NodeError::InvalidSocketAddress | NodeError::InvalidPublicKey | NodeError::InvalidSecretKey + | NodeError::InvalidMnemonic | NodeError::InvalidOfferId | NodeError::InvalidNodeId | NodeError::InvalidPaymentId @@ -89,8 +90,10 @@ impl From for LdkServerError { | NodeError::InvalidNodeAlias | NodeError::InvalidDateTime | NodeError::InvalidFeeRate + | NodeError::InvalidPageToken | NodeError::UriParameterParsingFailed | NodeError::InvalidBlindedPaths + | NodeError::InvalidPayerProof | NodeError::AsyncPaymentServicesDisabled => { (error.to_string(), LdkServerErrorCode::InvalidRequestError) }, @@ -122,6 +125,7 @@ impl From for LdkServerError { | NodeError::GossipUpdateTimeout | NodeError::LiquiditySourceUnavailable | NodeError::LiquidityRequestFailed + | NodeError::PayerProofCreationFailed | NodeError::OnchainTxCreationFailed | NodeError::OnchainTxSigningFailed | NodeError::TxSyncFailed diff --git a/ldk-server/src/api/get_payment_details.rs b/ldk-server/src/api/get_payment_details.rs index e7d58a79..c243aee5 100644 --- a/ldk-server/src/api/get_payment_details.rs +++ b/ldk-server/src/api/get_payment_details.rs @@ -9,27 +9,17 @@ use std::sync::Arc; -use hex::FromHex; -use ldk_node::lightning::ln::channelmanager::PaymentId; use ldk_server_grpc::api::{GetPaymentDetailsRequest, GetPaymentDetailsResponse}; use crate::api::error::LdkServerError; -use crate::api::error::LdkServerErrorCode::InvalidRequestError; use crate::service::Context; use crate::util::proto_adapter::payment_to_proto; pub(crate) async fn handle_get_payment_details_request( context: Arc, request: GetPaymentDetailsRequest, ) -> Result { - let payment_id_bytes = - <[u8; PaymentId::LENGTH]>::from_hex(&request.payment_id).map_err(|_| { - LdkServerError::new( - InvalidRequestError, - format!("Invalid payment_id, must be a {}-byte hex-string.", PaymentId::LENGTH), - ) - })?; - - let payment_details = context.node.payment(&PaymentId(payment_id_bytes)); + let payment_id = crate::api::parse_payment_id(&request.payment_id)?; + let payment_details = context.node.payment(&payment_id)?; let response = GetPaymentDetailsResponse { payment: payment_details.map(payment_to_proto) }; diff --git a/ldk-server/src/api/mod.rs b/ldk-server/src/api/mod.rs index 15f7ff06..6f454c49 100644 --- a/ldk-server/src/api/mod.rs +++ b/ldk-server/src/api/mod.rs @@ -7,7 +7,9 @@ // You may not use this file except in accordance with one or both of these // licenses. +use hex::FromHex; use ldk_node::config::{ChannelConfig, MaxDustHTLCExposure}; +use ldk_node::lightning::ln::channelmanager::PaymentId; use ldk_node::lightning::routing::router::RouteParametersConfig; use ldk_node::CustomTlvRecord as NodeCustomTlvRecord; use ldk_server_grpc::types::channel_config::MaxDustHtlcExposure; @@ -16,8 +18,8 @@ use ldk_server_grpc::types::CustomTlvRecord as ProtoCustomTlvRecord; use crate::api::error::LdkServerError; use crate::api::error::LdkServerErrorCode::InvalidRequestError; -pub(crate) mod bolt11_claim_for_hash; -pub(crate) mod bolt11_fail_for_hash; +pub(crate) mod bolt11_claim_for_id; +pub(crate) mod bolt11_fail_for_id; pub(crate) mod bolt11_receive; pub(crate) mod bolt11_receive_for_hash; pub(crate) mod bolt11_receive_via_jit_channel; @@ -62,6 +64,16 @@ pub(crate) fn require_amount(amount: Option) -> Result }) } +pub(crate) fn parse_payment_id(payment_id: &str) -> Result { + let bytes = <[u8; PaymentId::LENGTH]>::from_hex(payment_id).map_err(|_| { + LdkServerError::new( + InvalidRequestError, + format!("Invalid payment_id, must be a {}-byte hex string.", PaymentId::LENGTH), + ) + })?; + Ok(PaymentId(bytes)) +} + pub(crate) fn build_channel_config_from_proto( default_config: ChannelConfig, proto_channel_config: ldk_server_grpc::types::ChannelConfig, ) -> Result { @@ -156,6 +168,13 @@ mod tests { assert!(require_amount::(None).is_err()); } + #[test] + fn payment_id_is_parsed() { + let payment_id = "01".repeat(PaymentId::LENGTH); + assert_eq!(parse_payment_id(&payment_id).unwrap(), PaymentId([1; PaymentId::LENGTH])); + assert!(parse_payment_id("invalid").is_err()); + } + #[test] fn proto_to_node_custom_tlv_preserves_fields() { let proto = diff --git a/ldk-server/src/main.rs b/ldk-server/src/main.rs index e950b6b7..e205a52c 100644 --- a/ldk-server/src/main.rs +++ b/ldk-server/src/main.rs @@ -160,6 +160,9 @@ fn main() { ldk_node_config.hrn_config = config_file.hrn_config; ldk_node_config.anchor_channels_config.enable_zero_fee_commitments = config_file.enable_zero_fee_commitments; + // The server exposes receive-for-hash APIs, so unknown inbound BOLT11 HTLCs + // must emit PaymentClaimable instead of being failed back. + ldk_node_config.manually_handle_unknown_bolt11_payments = true; let mut builder = Builder::from_config(ldk_node_config); builder.set_log_facade_logger(); @@ -498,10 +501,9 @@ fn main() { .. } => { info!( - "PAYMENT_RECEIVED: with id {:?}, hash {}, amount_msat {}", + "PAYMENT_RECEIVED: with id {}, hash {}, amount_msat {}", payment_id, payment_hash, amount_msat ); - let payment_id = payment_id.expect("PaymentId expected for ldk-server >=0.1"); let proto_custom_records: Vec<_> = custom_records .iter() @@ -526,8 +528,6 @@ fn main() { } }, Event::PaymentSuccessful {payment_id, ..} => { - let payment_id = payment_id.expect("PaymentId expected for ldk-server >=0.1"); - send_event_and_upsert_payment(&payment_id, |payment_ref| event_envelope::Event::PaymentSuccessful(events::PaymentSuccessful { payment: Some(payment_ref.clone()), @@ -542,9 +542,7 @@ fn main() { } }, Event::PaymentFailed {payment_id, reason, ..} => { - let payment_id = payment_id.expect("PaymentId expected for ldk-server >=0.1"); let proto_reason = reason.as_ref().map(payment_failure_reason_to_proto); - send_event_and_upsert_payment(&payment_id, move |payment_ref| event_envelope::Event::PaymentFailed(events::PaymentFailed { payment: Some(payment_ref.clone()), @@ -563,7 +561,11 @@ fn main() { &payment_id, |payment_ref| { event_envelope::Event::PaymentClaimable( - build_payment_claimable_proto(payment_ref, &custom_records, claim_deadline), + build_payment_claimable_proto( + payment_ref, + &custom_records, + claim_deadline, + ), ) }, &event_node, @@ -581,7 +583,7 @@ fn main() { } => { info!( "PAYMENT_FORWARDED: outbound_amount_forwarded_msat {}, total_fee_earned_msat: {}, inbound HTLCs: {}, outbound HTLCs: {}", - outbound_amount_forwarded_msat.unwrap_or(0), + outbound_amount_forwarded_msat, total_fee_earned_msat.unwrap_or(0), prev_htlcs.len(), next_htlcs.len(), @@ -610,7 +612,7 @@ fn main() { total_fee_earned_msat, skimmed_fee_msat, claim_from_onchain_tx, - outbound_amount_forwarded_msat + Some(outbound_amount_forwarded_msat), ); let mut forwarded_payment_id = [0u8; 32]; @@ -753,17 +755,19 @@ fn send_event_and_upsert_payment( event_node: &Node, event_sender: &broadcast::Sender, paginated_store: Arc, ) { - if let Some(payment_details) = event_node.payment(payment_id) { - let payment = payment_to_proto(payment_details); + match event_node.payment(payment_id) { + Ok(Some(payment_details)) => { + let payment = payment_to_proto(payment_details); - let event = payment_to_event(&payment); - if let Err(e) = event_sender.send(EventEnvelope { event: Some(event) }) { - debug!("No event subscribers connected, skipping event: {e}"); - } + let event = payment_to_event(&payment); + if let Err(e) = event_sender.send(EventEnvelope { event: Some(event) }) { + debug!("No event subscribers connected, skipping event: {e}"); + } - upsert_payment_details(event_node, Arc::clone(&paginated_store), &payment); - } else { - error!("Unable to find payment with paymentId: {payment_id}"); + upsert_payment_details(event_node, Arc::clone(&paginated_store), &payment); + }, + Ok(None) => error!("Unable to find payment with payment ID: {payment_id}"), + Err(e) => error!("Failed to retrieve payment with payment ID {payment_id}: {e}"), } } diff --git a/ldk-server/src/service.rs b/ldk-server/src/service.rs index ed14458e..3657e2a6 100644 --- a/ldk-server/src/service.rs +++ b/ldk-server/src/service.rs @@ -19,7 +19,7 @@ use ldk_node::bitcoin::hashes::hmac::{Hmac, HmacEngine}; use ldk_node::bitcoin::hashes::{sha256, Hash, HashEngine}; use ldk_node::Node; use ldk_server_grpc::endpoints::{ - BOLT11_CLAIM_FOR_HASH_PATH, BOLT11_FAIL_FOR_HASH_PATH, BOLT11_RECEIVE_FOR_HASH_PATH, + BOLT11_CLAIM_FOR_ID_PATH, BOLT11_FAIL_FOR_ID_PATH, BOLT11_RECEIVE_FOR_HASH_PATH, BOLT11_RECEIVE_PATH, BOLT11_RECEIVE_VARIABLE_AMOUNT_VIA_JIT_CHANNEL_PATH, BOLT11_RECEIVE_VIA_JIT_CHANNEL_PATH, BOLT11_SEND_PATH, BOLT11_SEND_UNDERPAYING_PATH, BOLT12_RECEIVE_PATH, BOLT12_RECEIVE_REFUND_PATH, BOLT12_SEND_PATH, BOLT12_SEND_REFUND_PATH, @@ -42,8 +42,8 @@ use ldk_server_grpc::grpc::{ use prost::Message; use tokio::sync::{broadcast, mpsc}; -use crate::api::bolt11_claim_for_hash::handle_bolt11_claim_for_hash_request; -use crate::api::bolt11_fail_for_hash::handle_bolt11_fail_for_hash_request; +use crate::api::bolt11_claim_for_id::handle_bolt11_claim_for_id_request; +use crate::api::bolt11_fail_for_id::handle_bolt11_fail_for_id_request; use crate::api::bolt11_receive::handle_bolt11_receive_request; use crate::api::bolt11_receive_for_hash::handle_bolt11_receive_for_hash_request; use crate::api::bolt11_receive_via_jit_channel::{ @@ -295,13 +295,11 @@ impl Service> for NodeService { handle_grpc_unary(context, body_bytes, handle_bolt11_receive_for_hash_request) .await }, - BOLT11_CLAIM_FOR_HASH_PATH => { - handle_grpc_unary(context, body_bytes, handle_bolt11_claim_for_hash_request) - .await + BOLT11_CLAIM_FOR_ID_PATH => { + handle_grpc_unary(context, body_bytes, handle_bolt11_claim_for_id_request).await }, - BOLT11_FAIL_FOR_HASH_PATH => { - handle_grpc_unary(context, body_bytes, handle_bolt11_fail_for_hash_request) - .await + BOLT11_FAIL_FOR_ID_PATH => { + handle_grpc_unary(context, body_bytes, handle_bolt11_fail_for_id_request).await }, BOLT11_RECEIVE_VIA_JIT_CHANNEL_PATH => { handle_grpc_unary( diff --git a/ldk-server/src/util/entropy.rs b/ldk-server/src/util/entropy.rs index d3299f7a..cf64fc22 100644 --- a/ldk-server/src/util/entropy.rs +++ b/ldk-server/src/util/entropy.rs @@ -12,7 +12,7 @@ use std::path::Path; use std::str::FromStr; use ldk_node::bip39::Mnemonic; -use ldk_node::entropy::{generate_entropy_mnemonic, NodeEntropy}; +use ldk_node::entropy::NodeEntropy; use log::info; use crate::util::{create_dir_all_private, read_to_string_with_limit, write_new}; @@ -34,7 +34,7 @@ pub(crate) fn load_or_generate_node_entropy(storage_dir: &Path) -> io::Result Result { + let mut counts = PaymentCounts::default(); + let mut page_token = None; + loop { + let page = node.list_payments(page_token)?; + for payment in page.payments { + counts.total += 1; + match payment.status { + PaymentStatus::Succeeded => counts.successful += 1, + PaymentStatus::Pending => counts.pending += 1, + PaymentStatus::Failed => counts.failed += 1, + } + } + match page.next_page_token { + Some(token) => page_token = Some(token), + None => return Ok(counts), + } + } +} /// Holds all the metrics that are tracked for LDK Server. /// @@ -92,20 +121,14 @@ impl Metrics { } pub fn initialize_payment_metrics(&self, node: &Node) { - let mut successful_payments_count = 0; - let mut failed_payments_count = 0; - let mut pending_payments_count = 0; - - for payment_details in node.list_payments() { - match payment_details.status { - PaymentStatus::Succeeded => successful_payments_count += 1, - PaymentStatus::Failed => failed_payments_count += 1, - PaymentStatus::Pending => pending_payments_count += 1, - } + match payment_status_counts(node) { + Ok(counts) => { + self.total_successful_payments_count.store(counts.successful, Ordering::Relaxed); + self.total_failed_payments_count.store(counts.failed, Ordering::Relaxed); + self.total_pending_payments_count.store(counts.pending, Ordering::Relaxed); + }, + Err(e) => error!("Failed to initialize payment metrics: {e}"), } - self.total_successful_payments_count.store(successful_payments_count, Ordering::Relaxed); - self.total_failed_payments_count.store(failed_payments_count, Ordering::Relaxed); - self.total_pending_payments_count.store(pending_payments_count, Ordering::Relaxed); let channels_count = node.list_channels().len() as i64; self.total_channels_count.store(channels_count, Ordering::Relaxed); @@ -129,17 +152,14 @@ impl Metrics { } pub fn update_all_pollable_metrics(&self, node: &Node) { - let all_payments = node.list_payments(); let all_channels = node.list_channels(); - - let payments_count = all_payments.len() as i64; - self.total_payments_count.store(payments_count, Ordering::Relaxed); - - let pending_payments_count = all_payments - .iter() - .filter(|payment_details| payment_details.status == PaymentStatus::Pending) - .count() as i64; - self.total_pending_payments_count.store(pending_payments_count, Ordering::Relaxed); + match payment_status_counts(node) { + Ok(counts) => { + self.total_payments_count.store(counts.total, Ordering::Relaxed); + self.total_pending_payments_count.store(counts.pending, Ordering::Relaxed); + }, + Err(e) => error!("Failed to update payment metrics: {e}"), + } let public_channels_count = all_channels.iter().filter(|channel_details| channel_details.is_announced).count() From 5a6320587501ee322275f5e41e819cb83b20556e Mon Sep 17 00:00:00 2001 From: benthecarman Date: Mon, 7 Sep 2026 23:26:20 -0500 Subject: [PATCH 2/9] Rename the public payment identifier field Rename Payment.id to Payment.payment_id for consistent naming across payment requests and responses. This changes generated client fields and JSON output; protobuf field number 1 remains unchanged. AI assistance: OpenAI Codex was used for this change. --- docs/api-guide.md | 2 +- e2e-tests/tests/e2e.rs | 30 +++++++++++++-------------- ldk-server-grpc/src/proto/types.proto | 2 +- ldk-server-grpc/src/types.rs | 2 +- ldk-server/src/main.rs | 2 +- ldk-server/src/util/proto_adapter.rs | 2 +- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/api-guide.md b/docs/api-guide.md index 8b161b17..1ae99842 100644 --- a/docs/api-guide.md +++ b/docs/api-guide.md @@ -237,7 +237,7 @@ Hodl invoices allow you to inspect and conditionally accept incoming payments: replayed. 2. **Create the invoice:** Generate a new payment hash. Call `Bolt11ReceiveForHash` with this hash. Never reuse a payment hash. Reuse is unsafe and can cause loss of funds. -3. **Handle each payment:** Save `payment.id` from each `PaymentClaimable` event. A payer can pay +3. **Handle each payment:** Save `payment.payment_id` from each `PaymentClaimable` event. A payer can pay the same invoice more than once. Each payment has a separate event and payment ID. 4. **Decide before `claim_deadline`:** - **Accept an expected payment:** Call `Bolt11ClaimForId` with its payment ID and preimage. diff --git a/e2e-tests/tests/e2e.rs b/e2e-tests/tests/e2e.rs index 0f22d925..ac8a863b 100644 --- a/e2e-tests/tests/e2e.rs +++ b/e2e-tests/tests/e2e.rs @@ -884,13 +884,13 @@ async fn test_cli_bolt11_send() { let Some(Event::PaymentSuccessful(successful)) = &event_a.event else { panic!("expected PaymentSuccessful"); }; - assert_eq!(successful.payment.as_ref().unwrap().id, send_payment_id); + assert_eq!(successful.payment.as_ref().unwrap().payment_id, send_payment_id); let event_b = wait_for_event(&mut events_b, |e| matches!(e, Event::PaymentReceived(_))).await; let Some(Event::PaymentReceived(received)) = &event_b.event else { panic!("expected PaymentReceived"); }; - assert!(!received.payment.as_ref().unwrap().id.is_empty()); + assert!(!received.payment.as_ref().unwrap().payment_id.is_empty()); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] @@ -1075,13 +1075,13 @@ async fn test_cli_spontaneous_send() { let Some(Event::PaymentSuccessful(successful)) = &event_a.event else { panic!("expected PaymentSuccessful"); }; - assert_eq!(successful.payment.as_ref().unwrap().id, send_payment_id); + assert_eq!(successful.payment.as_ref().unwrap().payment_id, send_payment_id); let event_b = wait_for_event(&mut events_b, |e| matches!(e, Event::PaymentReceived(_))).await; let Some(Event::PaymentReceived(received)) = &event_b.event else { panic!("expected PaymentReceived"); }; - assert!(!received.payment.as_ref().unwrap().id.is_empty()); + assert!(!received.payment.as_ref().unwrap().payment_id.is_empty()); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] @@ -1114,7 +1114,7 @@ async fn test_cli_spontaneous_send_with_custom_tlvs() { let Some(Event::PaymentReceived(pr)) = event_b.event else { panic!("expected PaymentReceived"); }; - assert!(!pr.payment.as_ref().unwrap().id.is_empty()); + assert!(!pr.payment.as_ref().unwrap().payment_id.is_empty()); assert_eq!(pr.custom_records.len(), 2); let by_type: HashMap> = pr.custom_records.into_iter().map(|r| (r.type_num, r.value.to_vec())).collect(); @@ -1150,7 +1150,7 @@ async fn test_cli_get_payment_details() { let output = run_cli(&server_a, &["get-payment-details", payment_id]); assert!(output.get("payment").is_some()); - assert_eq!(output["payment"]["id"], payment_id); + assert_eq!(output["payment"]["payment_id"], payment_id); } #[tokio::test] @@ -1516,7 +1516,7 @@ async fn test_hodl_invoice_claim() { panic!("expected PaymentClaimable"); }; assert!(claimable_event.claim_deadline.is_some()); - assert!(!claimable_event.payment.as_ref().unwrap().id.is_empty()); + assert!(!claimable_event.payment.as_ref().unwrap().payment_id.is_empty()); if let Some(invalid_claim) = invalid_claim { let invalid_preimage = [99u8; 32].to_lower_hex_string(); @@ -1527,7 +1527,7 @@ async fn test_hodl_invoice_claim() { let error = server_b .client() .bolt11_claim_for_id(Bolt11ClaimForIdRequest { - payment_id: claimable_event.payment.as_ref().unwrap().id.clone(), + payment_id: claimable_event.payment.as_ref().unwrap().payment_id.clone(), claimable_amount_msat: attempted_amount, preimage: attempted_preimage.clone(), }) @@ -1538,7 +1538,7 @@ async fn test_hodl_invoice_claim() { // Claim the payment on B let mut args: Vec<&str> = - vec!["bolt11-claim-for-id", &claimable_event.payment.as_ref().unwrap().id, &preimage_hex]; + vec!["bolt11-claim-for-id", &claimable_event.payment.as_ref().unwrap().payment_id, &preimage_hex]; if let Some(amt) = amount { args.extend(["-c", amt]); } @@ -1550,7 +1550,7 @@ async fn test_hodl_invoice_claim() { let Some(Event::PaymentSuccessful(event)) = &successful.event else { panic!("expected PaymentSuccessful"); }; - assert!(!event.payment.as_ref().unwrap().id.is_empty()); + assert!(!event.payment.as_ref().unwrap().payment_id.is_empty()); } } @@ -1593,9 +1593,9 @@ async fn test_hodl_invoice_fail() { let Some(Event::PaymentClaimable(claimable)) = &event_b.event else { panic!("expected PaymentClaimable"); }; - assert!(!claimable.payment.as_ref().unwrap().id.is_empty()); + assert!(!claimable.payment.as_ref().unwrap().payment_id.is_empty()); let unknown_payment_id = "00".repeat(32); - assert_ne!(claimable.payment.as_ref().unwrap().id, unknown_payment_id); + assert_ne!(claimable.payment.as_ref().unwrap().payment_id, unknown_payment_id); let error = server_b .client() .bolt11_fail_for_id(Bolt11FailForIdRequest { payment_id: unknown_payment_id }) @@ -1604,14 +1604,14 @@ async fn test_hodl_invoice_fail() { assert_eq!(error.error_code, InvalidRequestError); // Fail the payment on B using CLI - run_cli(&server_b, &["bolt11-fail-for-id", &claimable.payment.as_ref().unwrap().id]); + run_cli(&server_b, &["bolt11-fail-for-id", &claimable.payment.as_ref().unwrap().payment_id]); // Verify PaymentFailed on A and its failure reason. let event_a = wait_for_event(&mut events_a, |e| matches!(e, Event::PaymentFailed(_))).await; let Some(Event::PaymentFailed(failed)) = &event_a.event else { panic!("expected PaymentFailed"); }; - assert!(!failed.payment.as_ref().unwrap().id.is_empty()); + assert!(!failed.payment.as_ref().unwrap().payment_id.is_empty()); assert_eq!( failed.reason, Some(PaymentFailureReason::RecipientRejected as i32) @@ -1776,7 +1776,7 @@ async fn test_cli_spontaneous_send_with_preimage() { panic!("expected PaymentReceived"); }; let payment = pr.payment.unwrap(); - assert!(!payment.id.is_empty()); + assert!(!payment.payment_id.is_empty()); let Some(payment_kind::Kind::Spontaneous(spont)) = payment.kind.unwrap().kind else { panic!("expected spontaneous kind"); diff --git a/ldk-server-grpc/src/proto/types.proto b/ldk-server-grpc/src/proto/types.proto index f8596288..a51698b0 100644 --- a/ldk-server-grpc/src/proto/types.proto +++ b/ldk-server-grpc/src/proto/types.proto @@ -5,7 +5,7 @@ package types; // See more: https://docs.rs/ldk-node/latest/ldk_node/payment/struct.PaymentDetails.html message Payment { // An identifier used to uniquely identify a payment in hex-encoded form. - string id = 1; + string payment_id = 1; // The kind of the payment. PaymentKind kind = 2; diff --git a/ldk-server-grpc/src/types.rs b/ldk-server-grpc/src/types.rs index 276302a5..315d1917 100644 --- a/ldk-server-grpc/src/types.rs +++ b/ldk-server-grpc/src/types.rs @@ -17,7 +17,7 @@ pub struct Payment { /// An identifier used to uniquely identify a payment in hex-encoded form. #[prost(string, tag = "1")] - pub id: ::prost::alloc::string::String, + pub payment_id: ::prost::alloc::string::String, /// The kind of the payment. #[prost(message, optional, tag = "2")] pub kind: ::core::option::Option, diff --git a/ldk-server/src/main.rs b/ldk-server/src/main.rs index e205a52c..c6eb8106 100644 --- a/ldk-server/src/main.rs +++ b/ldk-server/src/main.rs @@ -955,7 +955,7 @@ fn upsert_payment_details( match paginated_store.write( PAYMENTS_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENTS_PERSISTENCE_SECONDARY_NAMESPACE, - &payment.id, + &payment.payment_id, time, &payment.encode_to_vec(), ) { diff --git a/ldk-server/src/util/proto_adapter.rs b/ldk-server/src/util/proto_adapter.rs index dd0de1f3..0315dd7c 100644 --- a/ldk-server/src/util/proto_adapter.rs +++ b/ldk-server/src/util/proto_adapter.rs @@ -179,7 +179,7 @@ pub(crate) fn payment_to_proto(payment: PaymentDetails) -> Payment { } = payment; Payment { - id: id.to_string(), + payment_id: id.to_string(), kind: Some(payment_kind_to_proto(kind)), amount_msat, fee_paid_msat, From 51b83e0d415a7d44fb9a96b8e4fc8b16b4be1d7e Mon Sep 17 00:00:00 2001 From: benthecarman Date: Mon, 7 Sep 2026 23:26:28 -0500 Subject: [PATCH 3/9] Expose payment IDs and claimable event amounts Add local payment IDs to payment events and expose the claimable amount for hold-invoice consumers. Explain the existing lower-bound claim check without changing its optional request parameter. Number event fields in declaration order, with payment_id first. Backward compatibility is not required yet. AI assistance: OpenAI Codex was used for this change. --- docs/api-guide.md | 11 +++++++-- e2e-tests/tests/e2e.rs | 12 ++++----- ldk-server-cli/src/main.rs | 2 +- ldk-server-grpc/src/api.rs | 7 ++++-- ldk-server-grpc/src/events.rs | 34 ++++++++++++++++++++------ ldk-server-grpc/src/proto/api.proto | 7 ++++-- ldk-server-grpc/src/proto/events.proto | 29 ++++++++++++++++------ ldk-server/src/main.rs | 25 ++++++++++++++++--- 8 files changed, 95 insertions(+), 32 deletions(-) diff --git a/docs/api-guide.md b/docs/api-guide.md index 1ae99842..4c3ad4f2 100644 --- a/docs/api-guide.md +++ b/docs/api-guide.md @@ -237,13 +237,20 @@ Hodl invoices allow you to inspect and conditionally accept incoming payments: replayed. 2. **Create the invoice:** Generate a new payment hash. Call `Bolt11ReceiveForHash` with this hash. Never reuse a payment hash. Reuse is unsafe and can cause loss of funds. -3. **Handle each payment:** Save `payment.payment_id` from each `PaymentClaimable` event. A payer can pay +3. **Handle each payment:** Save the payment ID from each `PaymentClaimable` event. A payer can pay the same invoice more than once. Each payment has a separate event and payment ID. 4. **Decide before `claim_deadline`:** - - **Accept an expected payment:** Call `Bolt11ClaimForId` with its payment ID and preimage. + - **Accept an expected payment:** Check the event's `claimable_amount_msat` against the amount + you expect. Call `Bolt11ClaimForId` with its payment ID, preimage, and the event's claimable + amount. - **Reject an unexpected payment:** Call `Bolt11FailForId` with its payment ID. Reject duplicate and late payments instead of ignoring or claiming them. +The claim request's optional amount is passed to LDK Node for a lower-bound check against its +stored payment amount, less any skimmed fee. It is not an exact amount check or a request to claim +that many millisatoshis. A larger supplied amount passes this check; omitting it skips the check. +Always validate the event's amount before you claim the payment. + The payment is held in a pending state until you explicitly claim or fail it. **You must always handle each event.** If you do not, the HTLC will eventually time out. This can cause a force-closure of the channel. diff --git a/e2e-tests/tests/e2e.rs b/e2e-tests/tests/e2e.rs index ac8a863b..97e3e5d0 100644 --- a/e2e-tests/tests/e2e.rs +++ b/e2e-tests/tests/e2e.rs @@ -1516,7 +1516,7 @@ async fn test_hodl_invoice_claim() { panic!("expected PaymentClaimable"); }; assert!(claimable_event.claim_deadline.is_some()); - assert!(!claimable_event.payment.as_ref().unwrap().payment_id.is_empty()); + assert!(!claimable_event.payment_id.is_empty()); if let Some(invalid_claim) = invalid_claim { let invalid_preimage = [99u8; 32].to_lower_hex_string(); @@ -1527,7 +1527,7 @@ async fn test_hodl_invoice_claim() { let error = server_b .client() .bolt11_claim_for_id(Bolt11ClaimForIdRequest { - payment_id: claimable_event.payment.as_ref().unwrap().payment_id.clone(), + payment_id: claimable_event.payment_id.clone(), claimable_amount_msat: attempted_amount, preimage: attempted_preimage.clone(), }) @@ -1538,7 +1538,7 @@ async fn test_hodl_invoice_claim() { // Claim the payment on B let mut args: Vec<&str> = - vec!["bolt11-claim-for-id", &claimable_event.payment.as_ref().unwrap().payment_id, &preimage_hex]; + vec!["bolt11-claim-for-id", &claimable_event.payment_id, &preimage_hex]; if let Some(amt) = amount { args.extend(["-c", amt]); } @@ -1593,9 +1593,9 @@ async fn test_hodl_invoice_fail() { let Some(Event::PaymentClaimable(claimable)) = &event_b.event else { panic!("expected PaymentClaimable"); }; - assert!(!claimable.payment.as_ref().unwrap().payment_id.is_empty()); + assert!(!claimable.payment_id.is_empty()); let unknown_payment_id = "00".repeat(32); - assert_ne!(claimable.payment.as_ref().unwrap().payment_id, unknown_payment_id); + assert_ne!(claimable.payment_id, unknown_payment_id); let error = server_b .client() .bolt11_fail_for_id(Bolt11FailForIdRequest { payment_id: unknown_payment_id }) @@ -1604,7 +1604,7 @@ async fn test_hodl_invoice_fail() { assert_eq!(error.error_code, InvalidRequestError); // Fail the payment on B using CLI - run_cli(&server_b, &["bolt11-fail-for-id", &claimable.payment.as_ref().unwrap().payment_id]); + run_cli(&server_b, &["bolt11-fail-for-id", &claimable.payment_id]); // Verify PaymentFailed on A and its failure reason. let event_a = wait_for_event(&mut events_a, |e| matches!(e, Event::PaymentFailed(_))).await; diff --git a/ldk-server-cli/src/main.rs b/ldk-server-cli/src/main.rs index 95cc4cad..19940b69 100644 --- a/ldk-server-cli/src/main.rs +++ b/ldk-server-cli/src/main.rs @@ -171,7 +171,7 @@ enum Commands { #[arg( short, long, - help = "The claimable amount, e.g. 50sat or 50000msat, only used for verifying we are claiming the expected amount" + help = "The amount from PaymentClaimable, e.g. 50sat or 50000msat. Used for a lower-bound check, not an exact amount check; validate the event amount before claiming" )] claimable_amount: Option, }, diff --git a/ldk-server-grpc/src/api.rs b/ldk-server-grpc/src/api.rs index 17d15a41..52aeb531 100644 --- a/ldk-server-grpc/src/api.rs +++ b/ldk-server-grpc/src/api.rs @@ -256,8 +256,11 @@ pub struct Bolt11ClaimForIdRequest { /// The hex-encoded 32-byte payment ID from `PaymentClaimable`. #[prost(string, tag = "1")] pub payment_id: ::prost::alloc::string::String, - /// The amount in millisatoshi that is claimable. - /// If not provided, skips amount verification. + /// The claimable amount in millisatoshis from the PaymentClaimable event. + /// LDK Node rejects a value below its stored payment amount, less any skimmed fee. + /// A larger value passes this check. This is not an exact amount check or a request + /// to claim that many millisatoshis. Validate the event's amount before claiming. + /// If not provided, skips this amount check. #[prost(uint64, optional, tag = "2")] pub claimable_amount_msat: ::core::option::Option, /// The hex-encoded 32-byte payment preimage. diff --git a/ldk-server-grpc/src/events.rs b/ldk-server-grpc/src/events.rs index a53e808e..31644a08 100644 --- a/ldk-server-grpc/src/events.rs +++ b/ldk-server-grpc/src/events.rs @@ -188,11 +188,14 @@ pub struct SpliceNegotiationFailed { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct PaymentReceived { + /// The local identifier used to track the payment, in hex-encoded form. + #[prost(string, tag = "1")] + pub payment_id: ::prost::alloc::string::String, /// The payment details for the payment in event. - #[prost(message, optional, tag = "1")] + #[prost(message, optional, tag = "2")] pub payment: ::core::option::Option, /// Custom TLV records attached to the incoming payment, if any. - #[prost(message, repeated, tag = "2")] + #[prost(message, repeated, tag = "3")] pub custom_records: ::prost::alloc::vec::Vec, } /// PaymentSuccessful indicates a sent payment was successful. @@ -202,8 +205,11 @@ pub struct PaymentReceived { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct PaymentSuccessful { + /// The local identifier used to track the payment, in hex-encoded form. + #[prost(string, tag = "1")] + pub payment_id: ::prost::alloc::string::String, /// The payment details for the payment in event. - #[prost(message, optional, tag = "1")] + #[prost(message, optional, tag = "2")] pub payment: ::core::option::Option, } /// PaymentFailed indicates a sent payment has failed. @@ -213,34 +219,46 @@ pub struct PaymentSuccessful { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct PaymentFailed { + /// The local identifier used to track the payment, in hex-encoded form. + #[prost(string, tag = "1")] + pub payment_id: ::prost::alloc::string::String, /// The payment details for the payment in event. - #[prost(message, optional, tag = "1")] + #[prost(message, optional, tag = "2")] pub payment: ::core::option::Option, /// The reason the payment failed, if known. /// /// This is only available on the emitted event; `GetPaymentDetails` cannot /// recover it as LDK Node does not currently persist the failure reason in /// `PaymentDetails`. - #[prost(enumeration = "PaymentFailureReason", optional, tag = "2")] + #[prost(enumeration = "PaymentFailureReason", optional, tag = "3")] pub reason: ::core::option::Option, } /// PaymentClaimable indicates a payment has arrived and is waiting to be manually claimed or failed. /// This event is only emitted for payments created via `Bolt11ReceiveForHash`. +/// Handle every event by its payment ID before `claim_deadline`. +/// The same invoice can produce more than one event. Fail unexpected duplicate or late payments. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] #[cfg_attr(feature = "serde", serde(default))] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct PaymentClaimable { + /// The local identifier used to track the payment, in hex-encoded form. + #[prost(string, tag = "1")] + pub payment_id: ::prost::alloc::string::String, /// The payment details for the claimable payment. - #[prost(message, optional, tag = "1")] + #[prost(message, optional, tag = "2")] pub payment: ::core::option::Option, /// Custom TLV records attached to the claimable payment, if any. - #[prost(message, repeated, tag = "2")] + #[prost(message, repeated, tag = "3")] pub custom_records: ::prost::alloc::vec::Vec, /// The block height by which this payment must be claimed before it is failed back. - #[prost(uint32, optional, tag = "3")] + #[prost(uint32, optional, tag = "4")] pub claim_deadline: ::core::option::Option, + /// The amount in millisatoshis that is claimable. Validate this against the amount you expect + /// before calling Bolt11ClaimForId, and pass this value as its claimable_amount_msat. + #[prost(uint64, tag = "5")] + pub claimable_amount_msat: u64, } /// PaymentForwarded indicates a payment was forwarded through the node. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] diff --git a/ldk-server-grpc/src/proto/api.proto b/ldk-server-grpc/src/proto/api.proto index 6837cca6..8255d426 100644 --- a/ldk-server-grpc/src/proto/api.proto +++ b/ldk-server-grpc/src/proto/api.proto @@ -200,8 +200,11 @@ message Bolt11ClaimForIdRequest { // The hex-encoded 32-byte payment ID from `PaymentClaimable`. string payment_id = 1; - // The amount in millisatoshi that is claimable. - // If not provided, skips amount verification. + // The claimable amount in millisatoshis from the PaymentClaimable event. + // LDK Node rejects a value below its stored payment amount, less any skimmed fee. + // A larger value passes this check. This is not an exact amount check or a request + // to claim that many millisatoshis. Validate the event's amount before claiming. + // If not provided, skips this amount check. optional uint64 claimable_amount_msat = 2; // The hex-encoded 32-byte payment preimage. diff --git a/ldk-server-grpc/src/proto/events.proto b/ldk-server-grpc/src/proto/events.proto index 77812010..e95e5f5c 100644 --- a/ldk-server-grpc/src/proto/events.proto +++ b/ldk-server-grpc/src/proto/events.proto @@ -145,39 +145,52 @@ message SpliceNegotiationFailed { // PaymentReceived indicates a payment has been received. message PaymentReceived { + // The local identifier used to track the payment, in hex-encoded form. + string payment_id = 1; // The payment details for the payment in event. - types.Payment payment = 1; + types.Payment payment = 2; // Custom TLV records attached to the incoming payment, if any. - repeated types.CustomTlvRecord custom_records = 2; + repeated types.CustomTlvRecord custom_records = 3; } // PaymentSuccessful indicates a sent payment was successful. message PaymentSuccessful { + // The local identifier used to track the payment, in hex-encoded form. + string payment_id = 1; // The payment details for the payment in event. - types.Payment payment = 1; + types.Payment payment = 2; } // PaymentFailed indicates a sent payment has failed. message PaymentFailed { + // The local identifier used to track the payment, in hex-encoded form. + string payment_id = 1; // The payment details for the payment in event. - types.Payment payment = 1; + types.Payment payment = 2; // The reason the payment failed, if known. // // This is only available on the emitted event; `GetPaymentDetails` cannot // recover it as LDK Node does not currently persist the failure reason in // `PaymentDetails`. - optional PaymentFailureReason reason = 2; + optional PaymentFailureReason reason = 3; } // PaymentClaimable indicates a payment has arrived and is waiting to be manually claimed or failed. // This event is only emitted for payments created via `Bolt11ReceiveForHash`. +// Handle every event by its payment ID before `claim_deadline`. +// The same invoice can produce more than one event. Fail unexpected duplicate or late payments. message PaymentClaimable { + // The local identifier used to track the payment, in hex-encoded form. + string payment_id = 1; // The payment details for the claimable payment. - types.Payment payment = 1; + types.Payment payment = 2; // Custom TLV records attached to the claimable payment, if any. - repeated types.CustomTlvRecord custom_records = 2; + repeated types.CustomTlvRecord custom_records = 3; // The block height by which this payment must be claimed before it is failed back. - optional uint32 claim_deadline = 3; + optional uint32 claim_deadline = 4; + // The amount in millisatoshis that is claimable. Validate this against the amount you expect + // before calling Bolt11ClaimForId, and pass this value as its claimable_amount_msat. + uint64 claimable_amount_msat = 5; } // PaymentForwarded indicates a payment was forwarded through the node. diff --git a/ldk-server/src/main.rs b/ldk-server/src/main.rs index c6eb8106..8a9914bc 100644 --- a/ldk-server/src/main.rs +++ b/ldk-server/src/main.rs @@ -514,6 +514,7 @@ fn main() { &payment_id, move |payment_ref| { event_envelope::Event::PaymentReceived(events::PaymentReceived { + payment_id: payment_id.to_string(), payment: Some(payment_ref.clone()), custom_records: proto_custom_records, }) @@ -530,6 +531,7 @@ fn main() { Event::PaymentSuccessful {payment_id, ..} => { send_event_and_upsert_payment(&payment_id, |payment_ref| event_envelope::Event::PaymentSuccessful(events::PaymentSuccessful { + payment_id: payment_id.to_string(), payment: Some(payment_ref.clone()), }), &event_node, @@ -545,6 +547,7 @@ fn main() { let proto_reason = reason.as_ref().map(payment_failure_reason_to_proto); send_event_and_upsert_payment(&payment_id, move |payment_ref| event_envelope::Event::PaymentFailed(events::PaymentFailed { + payment_id: payment_id.to_string(), payment: Some(payment_ref.clone()), reason: proto_reason.map(|r| r as i32), }), @@ -556,7 +559,7 @@ fn main() { metrics.update_payments_count(false); } }, - Event::PaymentClaimable { payment_id, custom_records, claim_deadline, .. } => { + Event::PaymentClaimable { payment_id, custom_records, claim_deadline, claimable_amount_msat, .. } => { send_event_and_upsert_payment( &payment_id, |payment_ref| { @@ -565,6 +568,8 @@ fn main() { payment_ref, &custom_records, claim_deadline, + claimable_amount_msat, + payment_id.to_string(), ), ) }, @@ -1011,13 +1016,16 @@ fn load_or_generate_api_key(storage_dir: &Path) -> std::io::Result { fn build_payment_claimable_proto( payment_ref: &Payment, custom_records: &[CustomTlvRecord], claim_deadline: Option, + claimable_amount_msat: u64, payment_id: String, ) -> events::PaymentClaimable { let proto_custom_records: Vec<_> = custom_records.iter().map(node_to_proto_custom_tlv).collect(); events::PaymentClaimable { + payment_id, payment: Some(payment_ref.clone()), custom_records: proto_custom_records, claim_deadline, + claimable_amount_msat, } } @@ -1140,13 +1148,24 @@ mod tests { } #[test] - fn payment_claimable_proto_contains_custom_records() { + fn payment_claimable_proto_preserves_event_fields() { let payment = ldk_server_grpc::types::Payment::default(); let records = vec![ CustomTlvRecord { type_num: 65537, value: vec![1, 2, 3] }, CustomTlvRecord { type_num: 65538, value: Vec::new() }, ]; - let proto = build_payment_claimable_proto(&payment, &records, None); + let proto = build_payment_claimable_proto( + &payment, + &records, + Some(800_000), + 42_123, + "abc123".to_string(), + ); + let encoded = proto.encode_to_vec(); + let proto = events::PaymentClaimable::decode(encoded.as_slice()).unwrap(); + assert_eq!(proto.payment_id, "abc123"); + assert_eq!(proto.claim_deadline, Some(800_000)); + assert_eq!(proto.claimable_amount_msat, 42_123); assert_eq!(proto.custom_records.len(), 2); assert_eq!(proto.custom_records[0].type_num, 65537); assert_eq!(proto.custom_records[0].value.to_vec(), vec![1, 2, 3]); From 74be4da84afa55e3f657bac6ee2110ddd2365cd8 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Mon, 7 Sep 2026 23:27:11 -0500 Subject: [PATCH 4/9] Expose channel types and HTLC amounts Return negotiated channel-type features from ListChannels and carry incoming and outgoing HTLC amounts in forwarding records. Keep HTLC amounts optional because the node may not know them. AI assistance: OpenAI Codex was used for this change. --- e2e-tests/tests/e2e.rs | 1 + ldk-server-grpc/build.rs | 1 + ldk-server-grpc/src/proto/types.proto | 10 +++++++++- ldk-server-grpc/src/types.rs | 9 +++++++++ ldk-server/src/main.rs | 2 ++ ldk-server/src/util/proto_adapter.rs | 11 ++++++++++- 6 files changed, 32 insertions(+), 2 deletions(-) diff --git a/e2e-tests/tests/e2e.rs b/e2e-tests/tests/e2e.rs index 97e3e5d0..b317501a 100644 --- a/e2e-tests/tests/e2e.rs +++ b/e2e-tests/tests/e2e.rs @@ -827,6 +827,7 @@ async fn test_cli_list_channels() { // This test opens a default (anchor) channel with no trusted_peers_no_reserve // configured, so the reserve type is deterministically Adaptive. assert_eq!(channel["reserve_type"].as_i64(), Some(ReserveType::Adaptive as i64)); + assert!(!channel["channel_type"].as_object().unwrap().is_empty()); } #[tokio::test] diff --git a/ldk-server-grpc/build.rs b/ldk-server-grpc/build.rs index 2f8b4392..ece88bc3 100644 --- a/ldk-server-grpc/build.rs +++ b/ldk-server-grpc/build.rs @@ -40,6 +40,7 @@ fn generate_protos() { "api.GetNodeInfoResponse.features", "api.DecodeInvoiceResponse.features", "api.DecodeOfferResponse.features", + "types.Channel.channel_type", "types.GraphNodeAnnouncement.features", ]) .type_attribute( diff --git a/ldk-server-grpc/src/proto/types.proto b/ldk-server-grpc/src/proto/types.proto index a51698b0..9c4f715a 100644 --- a/ldk-server-grpc/src/proto/types.proto +++ b/ldk-server-grpc/src/proto/types.proto @@ -297,6 +297,11 @@ message HtlcLocator { // The node id of the counterparty for this HTLC. // This can be unset for older serialized events. optional string node_id = 3; + + // The amount in millisatoshis of the HTLC that was sent or received, if known. + // This can be unset for events serialized by LDK Node v0.7.0 and prior, + // or forwarding records stored by LDK Server before this field was added. + optional uint64 amount_msat = 4; } // A forwarded payment through our node. @@ -503,7 +508,6 @@ message Channel { // The minimum difference in CLTV expiry between an ingoing HTLC and its outgoing counterpart, // such that the outgoing HTLC is forwardable to this counterparty. optional uint32 counterparty_forwarding_info_cltv_expiry_delta = 25; - // The channel's `short_channel_id`, if we've negotiated the funding transaction with our // counterparty already and it's reached the required number of confirmations. // @@ -538,6 +542,10 @@ message Channel { // Will be unset until channel negotiation has completed and determined whether this channel // uses anchor or legacy reserve behavior. optional ReserveType reserve_type = 32; + + // The negotiated channel-type features, keyed by the signaled BOLT feature bit. + // This map is empty until channel negotiation determines the channel type. + map channel_type = 33; } // ChannelConfig represents the configuration settings for a channel in a Lightning Network node. diff --git a/ldk-server-grpc/src/types.rs b/ldk-server-grpc/src/types.rs index 315d1917..5615b76a 100644 --- a/ldk-server-grpc/src/types.rs +++ b/ldk-server-grpc/src/types.rs @@ -426,6 +426,11 @@ pub struct HtlcLocator { /// This can be unset for older serialized events. #[prost(string, optional, tag = "3")] pub node_id: ::core::option::Option<::prost::alloc::string::String>, + /// The amount in millisatoshis of the HTLC that was sent or received, if known. + /// This can be unset for events serialized by LDK Node v0.7.0 and prior, + /// or forwarding records stored by LDK Server before this field was added. + #[prost(uint64, optional, tag = "4")] + pub amount_msat: ::core::option::Option, } /// A forwarded payment through our node. /// @@ -643,6 +648,10 @@ pub struct Channel { /// uses anchor or legacy reserve behavior. #[prost(enumeration = "ReserveType", optional, tag = "32")] pub reserve_type: ::core::option::Option, + /// The negotiated channel-type features, keyed by the signaled BOLT feature bit. + /// This map is empty until channel negotiation determines the channel type. + #[prost(btree_map = "uint32, message", tag = "33")] + pub channel_type: ::prost::alloc::collections::BTreeMap, } /// ChannelConfig represents the configuration settings for a channel in a Lightning Network node. /// See more: diff --git a/ldk-server/src/main.rs b/ldk-server/src/main.rs index 8a9914bc..f587ae51 100644 --- a/ldk-server/src/main.rs +++ b/ldk-server/src/main.rs @@ -600,6 +600,7 @@ fn main() { channel_id: htlc.channel_id.to_string(), user_channel_id: htlc.user_channel_id.map(|u| u.0.to_string()), node_id: htlc.node_id.map(|n| n.to_string()), + amount_msat: htlc.amount_msat, }) .collect(); let next_htlcs = next_htlcs @@ -608,6 +609,7 @@ fn main() { channel_id: htlc.channel_id.to_string(), user_channel_id: htlc.user_channel_id.map(|u| u.0.to_string()), node_id: htlc.node_id.map(|n| n.to_string()), + amount_msat: htlc.amount_msat, }) .collect(); diff --git a/ldk-server/src/util/proto_adapter.rs b/ldk-server/src/util/proto_adapter.rs index 0315dd7c..f61aebfa 100644 --- a/ldk-server/src/util/proto_adapter.rs +++ b/ldk-server/src/util/proto_adapter.rs @@ -19,7 +19,7 @@ use ldk_node::lightning::routing::gossip::{ ChannelInfo, ChannelUpdateInfo, NodeAnnouncementInfo, NodeInfo, RoutingFees, }; use ldk_node::lightning_invoice::{Bolt11InvoiceDescription, Description, Sha256}; -use ldk_node::lightning_types::features::NodeFeatures; +use ldk_node::lightning_types::features::{ChannelTypeFeatures, NodeFeatures}; use ldk_node::payment::{ Channel as LdkTransactionChannel, ConfirmationStatus, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, TransactionType as LdkTransactionType, @@ -87,6 +87,14 @@ pub(crate) fn reserve_type_to_proto(reserve_type: &ReserveType) -> ProtoReserveT } pub(crate) fn channel_to_proto(channel: ChannelDetails) -> Channel { + let channel_type = channel + .channel_type + .map(|features| { + features_to_proto(features.le_flags(), |bytes| { + ChannelTypeFeatures::from_le_bytes(bytes).to_string() + }) + }) + .unwrap_or_default(); let counterparty = channel.counterparty; Channel { @@ -136,6 +144,7 @@ pub(crate) fn channel_to_proto(channel: ChannelDetails) -> Channel { .as_ref() .map(|s| channel_shutdown_state_to_proto(s) as i32), reserve_type: channel.reserve_type.as_ref().map(|r| reserve_type_to_proto(r) as i32), + channel_type, } } From fce9cffe0fff10a9c17dc09932f8ff96267b26d1 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Mon, 7 Sep 2026 23:27:21 -0500 Subject: [PATCH 5/9] Expose BOLT 12 payer proofs Add payer-proof creation to the gRPC, CLI, and MCP interfaces. Include the preimage and invoice in successful-payment events because stateless proof creation requires both values. Document the proof lifecycle and reject invalid proof inputs as request errors. AI assistance: OpenAI Codex was used for this change. --- docs/api-guide.md | 24 +++-- e2e-tests/tests/e2e.rs | 49 +++++++++++ ldk-server-cli/src/main.rs | 88 +++++++++++++++---- ldk-server-client/src/client.rs | 59 +++++++------ ldk-server-grpc/src/api.rs | 34 +++++++ ldk-server-grpc/src/endpoints.rs | 1 + ldk-server-grpc/src/events.rs | 7 ++ ldk-server-grpc/src/proto/api.proto | 26 ++++++ ldk-server-grpc/src/proto/events.proto | 5 ++ ldk-server-grpc/src/proto/types.proto | 22 +++++ ldk-server-grpc/src/types.rs | 27 ++++++ ldk-server-mcp/src/tools/handlers.rs | 19 ++-- ldk-server-mcp/src/tools/mod.rs | 6 ++ ldk-server-mcp/src/tools/schema.rs | 52 +++++++++++ ldk-server-mcp/tests/integration.rs | 3 +- .../src/api/bolt12_create_payer_proof.rs | 61 +++++++++++++ ldk-server/src/api/error.rs | 14 ++- ldk-server/src/api/mod.rs | 1 + ldk-server/src/main.rs | 9 +- ldk-server/src/service.rs | 24 +++-- 20 files changed, 465 insertions(+), 66 deletions(-) create mode 100644 ldk-server/src/api/bolt12_create_payer_proof.rs diff --git a/docs/api-guide.md b/docs/api-guide.md index 4c3ad4f2..8ce65523 100644 --- a/docs/api-guide.md +++ b/docs/api-guide.md @@ -128,12 +128,13 @@ when the invoice is paid. ### BOLT12 Offers and Refunds -| RPC | Description | -|-----------------------|-------------------------------------------------------------------------| -| `Bolt12Receive` | Create a BOLT12 offer (fixed or variable amount) | -| `Bolt12Send` | Pay a BOLT12 offer (with optional quantity, payer note, routing config) | -| `Bolt12SendRefund` | Create a BOLT12 refund that this node will pay | -| `Bolt12ReceiveRefund` | Request an incoming payment for a BOLT12 refund | +| RPC | Description | +|--------------------------|-------------------------------------------------------------------------| +| `Bolt12Receive` | Create a BOLT12 offer (fixed or variable amount) | +| `Bolt12Send` | Pay a BOLT12 offer (with optional quantity, payer note, routing config) | +| `Bolt12SendRefund` | Create a BOLT12 refund that this node will pay | +| `Bolt12ReceiveRefund` | Request an incoming payment for a BOLT12 refund | +| `Bolt12CreatePayerProof` | Create a BOLT 12 payer proof from a successful payment | ### Spontaneous and Unified Send @@ -229,6 +230,17 @@ GET /metrics Returns Prometheus-format text. Requires `[metrics] enabled = true` in the config. Supports optional Basic Auth. See [Configuration](configuration.md#metrics) for setup. +## BOLT 12 Payer-Proof Lifecycle + +Subscribe with `SubscribeEvents` before you send a BOLT 12 payment. Events are not replayed. + +When `PaymentSuccessful` arrives, retain its `payment_id`, `payment_preimage`, and +`bolt12_invoice`. Pass these values to `Bolt12CreatePayerProof`. The request can also select the +optional invoice fields that the proof discloses. + +The `bolt12_invoice` field is absent for static-invoice payments. These asynchronous payments +cannot produce payer proofs. + ## Hodl Invoice Lifecycle Hodl invoices allow you to inspect and conditionally accept incoming payments: diff --git a/e2e-tests/tests/e2e.rs b/e2e-tests/tests/e2e.rs index b317501a..05dea885 100644 --- a/e2e-tests/tests/e2e.rs +++ b/e2e-tests/tests/e2e.rs @@ -1056,6 +1056,55 @@ async fn test_cli_bolt12_refund() { wait_for_event(&mut events_b, |e| matches!(e, Event::PaymentSuccessful(_))).await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_cli_bolt12_create_payer_proof() { + let bitcoind = TestBitcoind::new(); + let server_a = LdkServerHandle::start(&bitcoind).await; + let server_b = LdkServerHandle::start(&bitcoind).await; + + let mut events_a = server_a.client().subscribe_events().await.unwrap(); + + setup_funded_channel(&bitcoind, &server_a, &server_b, 100_000).await; + + let offer_resp = server_b + .client() + .bolt12_receive(Bolt12ReceiveRequest { + description: "payer proof offer".to_string(), + amount_msat: Some(10_000_000), + expiry_secs: None, + quantity: None, + }) + .await + .unwrap(); + + let send_output = run_cli(&server_a, &["bolt12-send", &offer_resp.offer]); + let send_payment_id = send_output["payment_id"].as_str().unwrap(); + assert!(!send_payment_id.is_empty()); + + let event_a = wait_for_event(&mut events_a, |e| matches!(e, Event::PaymentSuccessful(_))).await; + let Some(Event::PaymentSuccessful(successful)) = &event_a.event else { + panic!("expected PaymentSuccessful"); + }; + assert_eq!(successful.payment_id, send_payment_id); + let payment_preimage = successful.payment_preimage.as_ref().expect("preimage"); + let invoice = successful.bolt12_invoice.as_ref().expect("bolt12 invoice"); + + let proof_output = run_cli( + &server_a, + &[ + "bolt12-create-payer-proof", + send_payment_id, + payment_preimage, + invoice, + "--include-offer-description", + "--include-invoice-amount", + "--note", + "Paid in full", + ], + ); + assert!(!proof_output["payer_proof"].as_str().unwrap().is_empty()); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_cli_spontaneous_send() { let bitcoind = TestBitcoind::new(); diff --git a/ldk-server-cli/src/main.rs b/ldk-server-cli/src/main.rs index 19940b69..6b383dc5 100644 --- a/ldk-server-cli/src/main.rs +++ b/ldk-server-cli/src/main.rs @@ -29,27 +29,28 @@ use ldk_server_client::ldk_server_grpc::api::{ Bolt11ReceiveRequest, Bolt11ReceiveResponse, Bolt11ReceiveVariableAmountViaJitChannelRequest, Bolt11ReceiveVariableAmountViaJitChannelResponse, Bolt11ReceiveViaJitChannelRequest, Bolt11ReceiveViaJitChannelResponse, Bolt11SendRequest, Bolt11SendResponse, - Bolt11SendUnderpayingRequest, Bolt11SendUnderpayingResponse, Bolt12ReceiveRefundRequest, - Bolt12ReceiveRefundResponse, Bolt12ReceiveRequest, Bolt12ReceiveResponse, - Bolt12SendRefundRequest, Bolt12SendRefundResponse, Bolt12SendRequest, Bolt12SendResponse, - CloseChannelRequest, CloseChannelResponse, ConnectPeerRequest, ConnectPeerResponse, - DecodeInvoiceRequest, DecodeInvoiceResponse, DecodeOfferRequest, DecodeOfferResponse, - DisconnectPeerRequest, DisconnectPeerResponse, ExportPathfindingScoresRequest, - ForceCloseChannelRequest, ForceCloseChannelResponse, GetBalancesRequest, GetBalancesResponse, - GetNodeInfoRequest, GetNodeInfoResponse, GetPaymentDetailsRequest, GetPaymentDetailsResponse, - GraphGetChannelRequest, GraphGetChannelResponse, GraphGetNodeRequest, GraphGetNodeResponse, - GraphListChannelsRequest, GraphListChannelsResponse, GraphListNodesRequest, - GraphListNodesResponse, ListChannelsRequest, ListChannelsResponse, - ListForwardedPaymentsRequest, ListPaymentsRequest, ListPeersRequest, ListPeersResponse, - OnchainReceiveRequest, OnchainReceiveResponse, OnchainSendRequest, OnchainSendResponse, - OpenChannelRequest, OpenChannelResponse, SignMessageRequest, SignMessageResponse, - SpliceInRequest, SpliceInResponse, SpliceOutRequest, SpliceOutResponse, SpontaneousSendRequest, - SpontaneousSendResponse, UnifiedSendRequest, UnifiedSendResponse, UpdateChannelConfigRequest, - UpdateChannelConfigResponse, VerifySignatureRequest, VerifySignatureResponse, + Bolt11SendUnderpayingRequest, Bolt11SendUnderpayingResponse, Bolt12CreatePayerProofRequest, + Bolt12CreatePayerProofResponse, Bolt12ReceiveRefundRequest, Bolt12ReceiveRefundResponse, + Bolt12ReceiveRequest, Bolt12ReceiveResponse, Bolt12SendRefundRequest, Bolt12SendRefundResponse, + Bolt12SendRequest, Bolt12SendResponse, CloseChannelRequest, CloseChannelResponse, + ConnectPeerRequest, ConnectPeerResponse, DecodeInvoiceRequest, DecodeInvoiceResponse, + DecodeOfferRequest, DecodeOfferResponse, DisconnectPeerRequest, DisconnectPeerResponse, + ExportPathfindingScoresRequest, ForceCloseChannelRequest, ForceCloseChannelResponse, + GetBalancesRequest, GetBalancesResponse, GetNodeInfoRequest, GetNodeInfoResponse, + GetPaymentDetailsRequest, GetPaymentDetailsResponse, GraphGetChannelRequest, + GraphGetChannelResponse, GraphGetNodeRequest, GraphGetNodeResponse, GraphListChannelsRequest, + GraphListChannelsResponse, GraphListNodesRequest, GraphListNodesResponse, ListChannelsRequest, + ListChannelsResponse, ListForwardedPaymentsRequest, ListPaymentsRequest, ListPeersRequest, + ListPeersResponse, OnchainReceiveRequest, OnchainReceiveResponse, OnchainSendRequest, + OnchainSendResponse, OpenChannelRequest, OpenChannelResponse, SignMessageRequest, + SignMessageResponse, SpliceInRequest, SpliceInResponse, SpliceOutRequest, SpliceOutResponse, + SpontaneousSendRequest, SpontaneousSendResponse, UnifiedSendRequest, UnifiedSendResponse, + UpdateChannelConfigRequest, UpdateChannelConfigResponse, VerifySignatureRequest, + VerifySignatureResponse, }; use ldk_server_client::ldk_server_grpc::types::{ bolt11_invoice_description, Bolt11InvoiceDescription, ChannelConfig, CustomTlvRecord, - PageToken, RouteParametersConfig, + PageToken, PayerProofOptions, RouteParametersConfig, }; use ldk_server_client::{ DEFAULT_EXPIRY_SECS, DEFAULT_MAX_CHANNEL_SATURATION_POWER_OF_HALF, DEFAULT_MAX_PATH_COUNT, @@ -353,6 +354,27 @@ enum Commands { #[arg(help = "A BOLT12 refund from the node that will send the payment")] refund: String, }, + #[command(about = "Create a BOLT 12 payer proof for a payment this node made")] + Bolt12CreatePayerProof { + #[arg(help = "The hex-encoded payment id from PaymentSuccessful")] + payment_id: String, + #[arg(help = "The hex-encoded 32-byte payment preimage from PaymentSuccessful")] + payment_preimage: String, + #[arg(help = "The hex-encoded BOLT 12 invoice from PaymentSuccessful")] + invoice: String, + #[arg(long, help = "Optional note to attach to the payer proof")] + note: Option, + #[arg(long, help = "Disclose the offer description in the proof")] + include_offer_description: bool, + #[arg(long, help = "Disclose the offer issuer in the proof")] + include_offer_issuer: bool, + #[arg(long, help = "Disclose the invoice amount in the proof")] + include_invoice_amount: bool, + #[arg(long, help = "Disclose the invoice creation timestamp in the proof")] + include_invoice_created_at: bool, + #[arg(long, help = "Additional TLV types to disclose")] + extra_tlv_types: Vec, + }, #[command(about = "Send a spontaneous payment (keysend) to a node")] SpontaneousSend { #[arg(help = "The hex-encoded public key of the node to send the payment to")] @@ -948,6 +970,36 @@ async fn main() { client.bolt12_receive_refund(Bolt12ReceiveRefundRequest { refund }).await, ); }, + Commands::Bolt12CreatePayerProof { + payment_id, + payment_preimage, + invoice, + note, + include_offer_description, + include_offer_issuer, + include_invoice_amount, + include_invoice_created_at, + extra_tlv_types, + } => { + let options = PayerProofOptions { + note, + include_offer_description, + include_offer_issuer, + include_invoice_amount, + include_invoice_created_at, + extra_tlv_types, + }; + handle_response_result::<_, Bolt12CreatePayerProofResponse>( + client + .bolt12_create_payer_proof(Bolt12CreatePayerProofRequest { + payment_id, + payment_preimage, + invoice, + options: Some(options), + }) + .await, + ); + }, Commands::SpontaneousSend { node_id, amount, diff --git a/ldk-server-client/src/client.rs b/ldk-server-client/src/client.rs index 6082ad2a..b8cf8b3f 100644 --- a/ldk-server-client/src/client.rs +++ b/ldk-server-client/src/client.rs @@ -21,22 +21,23 @@ use ldk_server_grpc::api::{ Bolt11ReceiveRequest, Bolt11ReceiveResponse, Bolt11ReceiveVariableAmountViaJitChannelRequest, Bolt11ReceiveVariableAmountViaJitChannelResponse, Bolt11ReceiveViaJitChannelRequest, Bolt11ReceiveViaJitChannelResponse, Bolt11SendRequest, Bolt11SendResponse, - Bolt11SendUnderpayingRequest, Bolt11SendUnderpayingResponse, Bolt12ReceiveRefundRequest, - Bolt12ReceiveRefundResponse, Bolt12ReceiveRequest, Bolt12ReceiveResponse, - Bolt12SendRefundRequest, Bolt12SendRefundResponse, Bolt12SendRequest, Bolt12SendResponse, - CloseChannelRequest, CloseChannelResponse, ConnectPeerRequest, ConnectPeerResponse, - DecodeInvoiceRequest, DecodeInvoiceResponse, DecodeOfferRequest, DecodeOfferResponse, - DisconnectPeerRequest, DisconnectPeerResponse, ExportPathfindingScoresRequest, - ExportPathfindingScoresResponse, ForceCloseChannelRequest, ForceCloseChannelResponse, - GetBalancesRequest, GetBalancesResponse, GetNodeInfoRequest, GetNodeInfoResponse, - GetPaymentDetailsRequest, GetPaymentDetailsResponse, GraphGetChannelRequest, - GraphGetChannelResponse, GraphGetNodeRequest, GraphGetNodeResponse, GraphListChannelsRequest, - GraphListChannelsResponse, GraphListNodesRequest, GraphListNodesResponse, ListChannelsRequest, - ListChannelsResponse, ListForwardedPaymentsRequest, ListForwardedPaymentsResponse, - ListPaymentsRequest, ListPaymentsResponse, ListPeersRequest, ListPeersResponse, - OnchainReceiveRequest, OnchainReceiveResponse, OnchainSendRequest, OnchainSendResponse, - OpenChannelRequest, OpenChannelResponse, SignMessageRequest, SignMessageResponse, - SpliceInRequest, SpliceInResponse, SpliceOutRequest, SpliceOutResponse, SpontaneousSendRequest, + Bolt11SendUnderpayingRequest, Bolt11SendUnderpayingResponse, Bolt12CreatePayerProofRequest, + Bolt12CreatePayerProofResponse, Bolt12ReceiveRefundRequest, Bolt12ReceiveRefundResponse, + Bolt12ReceiveRequest, Bolt12ReceiveResponse, Bolt12SendRefundRequest, Bolt12SendRefundResponse, + Bolt12SendRequest, Bolt12SendResponse, CloseChannelRequest, CloseChannelResponse, + ConnectPeerRequest, ConnectPeerResponse, DecodeInvoiceRequest, DecodeInvoiceResponse, + DecodeOfferRequest, DecodeOfferResponse, DisconnectPeerRequest, DisconnectPeerResponse, + ExportPathfindingScoresRequest, ExportPathfindingScoresResponse, ForceCloseChannelRequest, + ForceCloseChannelResponse, GetBalancesRequest, GetBalancesResponse, GetNodeInfoRequest, + GetNodeInfoResponse, GetPaymentDetailsRequest, GetPaymentDetailsResponse, + GraphGetChannelRequest, GraphGetChannelResponse, GraphGetNodeRequest, GraphGetNodeResponse, + GraphListChannelsRequest, GraphListChannelsResponse, GraphListNodesRequest, + GraphListNodesResponse, ListChannelsRequest, ListChannelsResponse, + ListForwardedPaymentsRequest, ListForwardedPaymentsResponse, ListPaymentsRequest, + ListPaymentsResponse, ListPeersRequest, ListPeersResponse, OnchainReceiveRequest, + OnchainReceiveResponse, OnchainSendRequest, OnchainSendResponse, OpenChannelRequest, + OpenChannelResponse, SignMessageRequest, SignMessageResponse, SpliceInRequest, + SpliceInResponse, SpliceOutRequest, SpliceOutResponse, SpontaneousSendRequest, SpontaneousSendResponse, SubscribeEventsRequest, UnifiedSendRequest, UnifiedSendResponse, UpdateChannelConfigRequest, UpdateChannelConfigResponse, VerifySignatureRequest, VerifySignatureResponse, @@ -45,15 +46,16 @@ use ldk_server_grpc::endpoints::{ BOLT11_CLAIM_FOR_ID_PATH, BOLT11_FAIL_FOR_ID_PATH, BOLT11_RECEIVE_FOR_HASH_PATH, BOLT11_RECEIVE_PATH, BOLT11_RECEIVE_VARIABLE_AMOUNT_VIA_JIT_CHANNEL_PATH, BOLT11_RECEIVE_VIA_JIT_CHANNEL_PATH, BOLT11_SEND_PATH, BOLT11_SEND_UNDERPAYING_PATH, - BOLT12_RECEIVE_PATH, BOLT12_RECEIVE_REFUND_PATH, BOLT12_SEND_PATH, BOLT12_SEND_REFUND_PATH, - CLOSE_CHANNEL_PATH, CONNECT_PEER_PATH, DECODE_INVOICE_PATH, DECODE_OFFER_PATH, - DISCONNECT_PEER_PATH, EXPORT_PATHFINDING_SCORES_PATH, FORCE_CLOSE_CHANNEL_PATH, - GET_BALANCES_PATH, GET_METRICS_PATH, GET_NODE_INFO_PATH, GET_PAYMENT_DETAILS_PATH, - GRAPH_GET_CHANNEL_PATH, GRAPH_GET_NODE_PATH, GRAPH_LIST_CHANNELS_PATH, GRAPH_LIST_NODES_PATH, - GRPC_SERVICE_PREFIX, LIST_CHANNELS_PATH, LIST_FORWARDED_PAYMENTS_PATH, LIST_PAYMENTS_PATH, - LIST_PEERS_PATH, ONCHAIN_RECEIVE_PATH, ONCHAIN_SEND_PATH, OPEN_CHANNEL_PATH, SIGN_MESSAGE_PATH, - SPLICE_IN_PATH, SPLICE_OUT_PATH, SPONTANEOUS_SEND_PATH, SUBSCRIBE_EVENTS_PATH, - UNIFIED_SEND_PATH, UPDATE_CHANNEL_CONFIG_PATH, VERIFY_SIGNATURE_PATH, + BOLT12_CREATE_PAYER_PROOF_PATH, BOLT12_RECEIVE_PATH, BOLT12_RECEIVE_REFUND_PATH, + BOLT12_SEND_PATH, BOLT12_SEND_REFUND_PATH, CLOSE_CHANNEL_PATH, CONNECT_PEER_PATH, + DECODE_INVOICE_PATH, DECODE_OFFER_PATH, DISCONNECT_PEER_PATH, EXPORT_PATHFINDING_SCORES_PATH, + FORCE_CLOSE_CHANNEL_PATH, GET_BALANCES_PATH, GET_METRICS_PATH, GET_NODE_INFO_PATH, + GET_PAYMENT_DETAILS_PATH, GRAPH_GET_CHANNEL_PATH, GRAPH_GET_NODE_PATH, + GRAPH_LIST_CHANNELS_PATH, GRAPH_LIST_NODES_PATH, GRPC_SERVICE_PREFIX, LIST_CHANNELS_PATH, + LIST_FORWARDED_PAYMENTS_PATH, LIST_PAYMENTS_PATH, LIST_PEERS_PATH, ONCHAIN_RECEIVE_PATH, + ONCHAIN_SEND_PATH, OPEN_CHANNEL_PATH, SIGN_MESSAGE_PATH, SPLICE_IN_PATH, SPLICE_OUT_PATH, + SPONTANEOUS_SEND_PATH, SUBSCRIBE_EVENTS_PATH, UNIFIED_SEND_PATH, UPDATE_CHANNEL_CONFIG_PATH, + VERIFY_SIGNATURE_PATH, }; use ldk_server_grpc::events::EventEnvelope; use ldk_server_grpc::grpc::{ @@ -284,6 +286,13 @@ impl LdkServerClient { self.grpc_unary(&request, BOLT12_RECEIVE_REFUND_PATH).await } + /// Create a BOLT 12 payer proof for a payment this node made. + pub async fn bolt12_create_payer_proof( + &self, request: Bolt12CreatePayerProofRequest, + ) -> Result { + self.grpc_unary(&request, BOLT12_CREATE_PAYER_PROOF_PATH).await + } + /// Creates a new outbound channel. pub async fn open_channel( &self, request: OpenChannelRequest, diff --git a/ldk-server-grpc/src/api.rs b/ldk-server-grpc/src/api.rs index 52aeb531..ed3b9259 100644 --- a/ldk-server-grpc/src/api.rs +++ b/ldk-server-grpc/src/api.rs @@ -563,6 +563,40 @@ pub struct Bolt12ReceiveRefundResponse { #[prost(string, tag = "1")] pub payment_hash: ::prost::alloc::string::String, } +/// Create a BOLT 12 payer proof for a payment this node made. +/// Inputs come from `PaymentSuccessful`: `payment_id`, `payment_preimage`, and `bolt12_invoice`. +/// See more: +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +#[cfg_attr(feature = "serde", serde(default))] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Bolt12CreatePayerProofRequest { + /// The local identifier used to track the payment, in hex-encoded form. + #[prost(string, tag = "1")] + pub payment_id: ::prost::alloc::string::String, + /// The hex-encoded 32-byte payment preimage from `PaymentSuccessful`. + #[prost(string, tag = "2")] + pub payment_preimage: ::prost::alloc::string::String, + /// The hex-encoded BOLT 12 invoice from `PaymentSuccessful.bolt12_invoice`. + /// Static invoices used for async payments cannot be proven. + #[prost(string, tag = "3")] + pub invoice: ::prost::alloc::string::String, + /// Controls which optional invoice fields the proof discloses. + #[prost(message, optional, tag = "4")] + pub options: ::core::option::Option, +} +/// The response for the `Bolt12CreatePayerProof` RPC. On failure, a gRPC error status is returned. +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +#[cfg_attr(feature = "serde", serde(default))] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Bolt12CreatePayerProofResponse { + /// The bech32-encoded payer proof. + #[prost(string, tag = "1")] + pub payer_proof: ::prost::alloc::string::String, +} /// Send a spontaneous payment, also known as "keysend", to a node. /// See more: #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] diff --git a/ldk-server-grpc/src/endpoints.rs b/ldk-server-grpc/src/endpoints.rs index 84d4e58e..2314dd17 100644 --- a/ldk-server-grpc/src/endpoints.rs +++ b/ldk-server-grpc/src/endpoints.rs @@ -27,6 +27,7 @@ pub const BOLT12_RECEIVE_PATH: &str = "Bolt12Receive"; pub const BOLT12_SEND_PATH: &str = "Bolt12Send"; pub const BOLT12_SEND_REFUND_PATH: &str = "Bolt12SendRefund"; pub const BOLT12_RECEIVE_REFUND_PATH: &str = "Bolt12ReceiveRefund"; +pub const BOLT12_CREATE_PAYER_PROOF_PATH: &str = "Bolt12CreatePayerProof"; pub const OPEN_CHANNEL_PATH: &str = "OpenChannel"; pub const SPLICE_IN_PATH: &str = "SpliceIn"; pub const SPLICE_OUT_PATH: &str = "SpliceOut"; diff --git a/ldk-server-grpc/src/events.rs b/ldk-server-grpc/src/events.rs index 31644a08..21acae36 100644 --- a/ldk-server-grpc/src/events.rs +++ b/ldk-server-grpc/src/events.rs @@ -211,6 +211,13 @@ pub struct PaymentSuccessful { /// The payment details for the payment in event. #[prost(message, optional, tag = "2")] pub payment: ::core::option::Option, + /// The hex-encoded payment preimage. Needed to build a BOLT 12 payer proof. + #[prost(string, optional, tag = "3")] + pub payment_preimage: ::core::option::Option<::prost::alloc::string::String>, + /// The hex-encoded paid BOLT 12 invoice, when the payment was for a standard BOLT 12 invoice. + /// Unset for non-BOLT12 payments and for static invoices used in async payments. + #[prost(string, optional, tag = "4")] + pub bolt12_invoice: ::core::option::Option<::prost::alloc::string::String>, } /// PaymentFailed indicates a sent payment has failed. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] diff --git a/ldk-server-grpc/src/proto/api.proto b/ldk-server-grpc/src/proto/api.proto index 8255d426..0e72159c 100644 --- a/ldk-server-grpc/src/proto/api.proto +++ b/ldk-server-grpc/src/proto/api.proto @@ -434,6 +434,30 @@ message Bolt12ReceiveRefundResponse { string payment_hash = 1; } +// Create a BOLT 12 payer proof for a payment this node made. +// Inputs come from `PaymentSuccessful`: `payment_id`, `payment_preimage`, and `bolt12_invoice`. +// See more: https://docs.rs/ldk-node/latest/ldk_node/payment/struct.Bolt12Payment.html#method.create_payer_proof +message Bolt12CreatePayerProofRequest { + // The local identifier used to track the payment, in hex-encoded form. + string payment_id = 1; + + // The hex-encoded 32-byte payment preimage from `PaymentSuccessful`. + string payment_preimage = 2; + + // The hex-encoded BOLT 12 invoice from `PaymentSuccessful.bolt12_invoice`. + // Static invoices used for async payments cannot be proven. + string invoice = 3; + + // Controls which optional invoice fields the proof discloses. + optional types.PayerProofOptions options = 4; +} + +// The response for the `Bolt12CreatePayerProof` RPC. On failure, a gRPC error status is returned. +message Bolt12CreatePayerProofResponse { + // The bech32-encoded payer proof. + string payer_proof = 1; +} + // Send a spontaneous payment, also known as "keysend", to a node. // See more: https://docs.rs/ldk-node/latest/ldk_node/payment/struct.SpontaneousPayment.html#method.send message SpontaneousSendRequest { @@ -1032,6 +1056,8 @@ service LightningNode { rpc Bolt12SendRefund(Bolt12SendRefundRequest) returns (Bolt12SendRefundResponse); // Request an incoming payment for a BOLT12 refund. rpc Bolt12ReceiveRefund(Bolt12ReceiveRefundRequest) returns (Bolt12ReceiveRefundResponse); + // Create a BOLT 12 payer proof for a payment this node made. + rpc Bolt12CreatePayerProof(Bolt12CreatePayerProofRequest) returns (Bolt12CreatePayerProofResponse); // Send a spontaneous payment (keysend). rpc SpontaneousSend(SpontaneousSendRequest) returns (SpontaneousSendResponse); // Create a new outbound channel. diff --git a/ldk-server-grpc/src/proto/events.proto b/ldk-server-grpc/src/proto/events.proto index e95e5f5c..08f1efae 100644 --- a/ldk-server-grpc/src/proto/events.proto +++ b/ldk-server-grpc/src/proto/events.proto @@ -159,6 +159,11 @@ message PaymentSuccessful { string payment_id = 1; // The payment details for the payment in event. types.Payment payment = 2; + // The hex-encoded payment preimage. Needed to build a BOLT 12 payer proof. + optional string payment_preimage = 3; + // The hex-encoded paid BOLT 12 invoice, when the payment was for a standard BOLT 12 invoice. + // Unset for non-BOLT12 payments and for static invoices used in async payments. + optional string bolt12_invoice = 4; } // PaymentFailed indicates a sent payment has failed. diff --git a/ldk-server-grpc/src/proto/types.proto b/ldk-server-grpc/src/proto/types.proto index 9c4f715a..0debfcf6 100644 --- a/ldk-server-grpc/src/proto/types.proto +++ b/ldk-server-grpc/src/proto/types.proto @@ -29,6 +29,28 @@ message Payment { uint64 latest_update_timestamp = 6; } +// Options that control which BOLT 12 invoice fields a payer proof discloses. +// See more: https://docs.rs/ldk-node/latest/ldk_node/payment/struct.PayerProofOptions.html +message PayerProofOptions { + // An optional note to attach to the payer proof itself. + optional string note = 1; + + // Whether to disclose the offer description. + bool include_offer_description = 2; + + // Whether to disclose the offer issuer. + bool include_offer_issuer = 3; + + // Whether to disclose the invoice amount. + bool include_invoice_amount = 4; + + // Whether to disclose the invoice creation timestamp. + bool include_invoice_created_at = 5; + + // Additional TLV types to disclose, for fields not covered by the flags above. + repeated uint64 extra_tlv_types = 6; +} + message PaymentKind { oneof kind { Onchain onchain = 1; diff --git a/ldk-server-grpc/src/types.rs b/ldk-server-grpc/src/types.rs index 5615b76a..1ebef88a 100644 --- a/ldk-server-grpc/src/types.rs +++ b/ldk-server-grpc/src/types.rs @@ -48,6 +48,33 @@ pub struct Payment { #[prost(uint64, tag = "6")] pub latest_update_timestamp: u64, } +/// Options that control which BOLT 12 invoice fields a payer proof discloses. +/// See more: +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +#[cfg_attr(feature = "serde", serde(default))] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PayerProofOptions { + /// An optional note to attach to the payer proof itself. + #[prost(string, optional, tag = "1")] + pub note: ::core::option::Option<::prost::alloc::string::String>, + /// Whether to disclose the offer description. + #[prost(bool, tag = "2")] + pub include_offer_description: bool, + /// Whether to disclose the offer issuer. + #[prost(bool, tag = "3")] + pub include_offer_issuer: bool, + /// Whether to disclose the invoice amount. + #[prost(bool, tag = "4")] + pub include_invoice_amount: bool, + /// Whether to disclose the invoice creation timestamp. + #[prost(bool, tag = "5")] + pub include_invoice_created_at: bool, + /// Additional TLV types to disclose, for fields not covered by the flags above. + #[prost(uint64, repeated, tag = "6")] + pub extra_tlv_types: ::prost::alloc::vec::Vec, +} #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] #[cfg_attr(feature = "serde", serde(default))] diff --git a/ldk-server-mcp/src/tools/handlers.rs b/ldk-server-mcp/src/tools/handlers.rs index b0e10132..7c81168b 100644 --- a/ldk-server-mcp/src/tools/handlers.rs +++ b/ldk-server-mcp/src/tools/handlers.rs @@ -13,11 +13,12 @@ use ldk_server_client::ldk_server_grpc::api::{ Bolt11ClaimForIdRequest, Bolt11FailForIdRequest, Bolt11ReceiveForHashRequest, Bolt11ReceiveRequest, Bolt11ReceiveVariableAmountViaJitChannelRequest, Bolt11ReceiveViaJitChannelRequest, Bolt11SendRequest, Bolt11SendUnderpayingRequest, - Bolt12ReceiveRefundRequest, Bolt12ReceiveRequest, Bolt12SendRefundRequest, Bolt12SendRequest, - CloseChannelRequest, ConnectPeerRequest, DecodeInvoiceRequest, DecodeOfferRequest, - DisconnectPeerRequest, ExportPathfindingScoresRequest, ForceCloseChannelRequest, - GetBalancesRequest, GetNodeInfoRequest, GetPaymentDetailsRequest, GraphGetChannelRequest, - GraphGetNodeRequest, GraphListChannelsRequest, GraphListNodesRequest, ListChannelsRequest, + Bolt12CreatePayerProofRequest, Bolt12ReceiveRefundRequest, Bolt12ReceiveRequest, + Bolt12SendRefundRequest, Bolt12SendRequest, CloseChannelRequest, ConnectPeerRequest, + DecodeInvoiceRequest, DecodeOfferRequest, DisconnectPeerRequest, + ExportPathfindingScoresRequest, ForceCloseChannelRequest, GetBalancesRequest, + GetNodeInfoRequest, GetPaymentDetailsRequest, GraphGetChannelRequest, GraphGetNodeRequest, + GraphListChannelsRequest, GraphListNodesRequest, ListChannelsRequest, ListForwardedPaymentsRequest, ListPaymentsRequest, ListPeersRequest, OnchainReceiveRequest, OnchainSendRequest, OpenChannelRequest, SignMessageRequest, SpliceInRequest, SpliceOutRequest, SpontaneousSendRequest, UnifiedSendRequest, UpdateChannelConfigRequest, VerifySignatureRequest, @@ -269,6 +270,14 @@ pub async fn handle_bolt12_receive_refund( serialize_response(response) } +pub async fn handle_bolt12_create_payer_proof( + client: &LdkServerClient, args: Value, +) -> Result { + let request: Bolt12CreatePayerProofRequest = parse_request(args)?; + let response = client.bolt12_create_payer_proof(request).await.map_err(McpError::from)?; + serialize_response(response) +} + pub async fn handle_spontaneous_send( client: &LdkServerClient, args: Value, ) -> Result { diff --git a/ldk-server-mcp/src/tools/mod.rs b/ldk-server-mcp/src/tools/mod.rs index 40706863..33d31e7e 100644 --- a/ldk-server-mcp/src/tools/mod.rs +++ b/ldk-server-mcp/src/tools/mod.rs @@ -169,6 +169,12 @@ pub fn build_tool_registry() -> ToolRegistry { schema::bolt12_receive_refund_schema, |client, args| Box::pin(handlers::handle_bolt12_receive_refund(client, args)), ), + tool_spec( + "bolt12_create_payer_proof", + "Create a BOLT12 payer proof for a payment this node made", + schema::bolt12_create_payer_proof_schema, + |client, args| Box::pin(handlers::handle_bolt12_create_payer_proof(client, args)), + ), tool_spec( "spontaneous_send", "Send a spontaneous (keysend) payment to a Lightning node", diff --git a/ldk-server-mcp/src/tools/schema.rs b/ldk-server-mcp/src/tools/schema.rs index ff7b7be3..fada6724 100644 --- a/ldk-server-mcp/src/tools/schema.rs +++ b/ldk-server-mcp/src/tools/schema.rs @@ -418,6 +418,58 @@ pub fn bolt12_receive_refund_schema() -> Value { }) } +pub fn bolt12_create_payer_proof_schema() -> Value { + json!({ + "type": "object", + "properties": { + "payment_id": { + "type": "string", + "description": "The hex-encoded payment id from PaymentSuccessful" + }, + "payment_preimage": { + "type": "string", + "description": "The hex-encoded 32-byte payment preimage from PaymentSuccessful" + }, + "invoice": { + "type": "string", + "description": "The hex-encoded BOLT 12 invoice from PaymentSuccessful" + }, + "options": { + "type": "object", + "description": "Controls which optional invoice fields the proof discloses", + "properties": { + "note": { + "type": "string", + "description": "Optional note to attach to the payer proof" + }, + "include_offer_description": { + "type": "boolean", + "description": "Disclose the offer description" + }, + "include_offer_issuer": { + "type": "boolean", + "description": "Disclose the offer issuer" + }, + "include_invoice_amount": { + "type": "boolean", + "description": "Disclose the invoice amount" + }, + "include_invoice_created_at": { + "type": "boolean", + "description": "Disclose the invoice creation timestamp" + }, + "extra_tlv_types": { + "type": "array", + "items": { "type": "integer" }, + "description": "Additional TLV types to disclose" + } + } + } + }, + "required": ["payment_id", "payment_preimage", "invoice"] + }) +} + pub fn spontaneous_send_schema() -> Value { json!({ "type": "object", diff --git a/ldk-server-mcp/tests/integration.rs b/ldk-server-mcp/tests/integration.rs index 128d878f..e05ae499 100644 --- a/ldk-server-mcp/tests/integration.rs +++ b/ldk-server-mcp/tests/integration.rs @@ -11,7 +11,7 @@ use std::io::{BufRead, BufReader, Write}; use serde_json::{json, Value}; -const NUM_TOOLS: usize = 40; +const NUM_TOOLS: usize = 41; const EXPECTED_TOOLS: [&str; NUM_TOOLS] = [ "bolt11_claim_for_id", "bolt11_fail_for_id", @@ -21,6 +21,7 @@ const EXPECTED_TOOLS: [&str; NUM_TOOLS] = [ "bolt11_receive_via_jit_channel", "bolt11_send", "bolt11_send_underpaying", + "bolt12_create_payer_proof", "bolt12_receive", "bolt12_receive_refund", "bolt12_send", diff --git a/ldk-server/src/api/bolt12_create_payer_proof.rs b/ldk-server/src/api/bolt12_create_payer_proof.rs new file mode 100644 index 00000000..f22c4218 --- /dev/null +++ b/ldk-server/src/api/bolt12_create_payer_proof.rs @@ -0,0 +1,61 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +use std::sync::Arc; + +use hex::FromHex; +use ldk_node::lightning::offers::invoice::Bolt12Invoice; +use ldk_node::lightning_types::payment::PaymentPreimage; +use ldk_node::payment::PayerProofOptions; +use ldk_server_grpc::api::{Bolt12CreatePayerProofRequest, Bolt12CreatePayerProofResponse}; + +use crate::api::error::LdkServerError; +use crate::api::error::LdkServerErrorCode::InvalidRequestError; +use crate::service::Context; + +pub(crate) async fn handle_bolt12_create_payer_proof_request( + context: Arc, request: Bolt12CreatePayerProofRequest, +) -> Result { + let payment_id = crate::api::parse_payment_id(&request.payment_id)?; + + let preimage_bytes = <[u8; 32]>::from_hex(&request.payment_preimage).map_err(|_| { + LdkServerError::new( + InvalidRequestError, + "Invalid payment_preimage, must be a 32-byte hex string.".to_string(), + ) + })?; + let payment_preimage = PaymentPreimage(preimage_bytes); + + let invoice_bytes = Vec::::from_hex(&request.invoice).map_err(|_| { + LdkServerError::new( + InvalidRequestError, + "Invalid invoice, must be a hex-encoded BOLT 12 invoice.".to_string(), + ) + })?; + let invoice = + Bolt12Invoice::try_from(invoice_bytes).map_err(|_| ldk_node::NodeError::InvalidInvoice)?; + + let options = request.options.map(|options| PayerProofOptions { + note: options.note, + include_offer_description: options.include_offer_description, + include_offer_issuer: options.include_offer_issuer, + include_invoice_amount: options.include_invoice_amount, + include_invoice_created_at: options.include_invoice_created_at, + extra_tlv_types: options.extra_tlv_types, + }); + + let payer_proof = context.node.bolt12_payment().create_payer_proof( + payment_id, + payment_preimage, + &invoice, + options, + )?; + + Ok(Bolt12CreatePayerProofResponse { payer_proof: payer_proof.to_string() }) +} diff --git a/ldk-server/src/api/error.rs b/ldk-server/src/api/error.rs index 13509e3c..b28c22a8 100644 --- a/ldk-server/src/api/error.rs +++ b/ldk-server/src/api/error.rs @@ -94,6 +94,7 @@ impl From for LdkServerError { | NodeError::UriParameterParsingFailed | NodeError::InvalidBlindedPaths | NodeError::InvalidPayerProof + | NodeError::PayerProofCreationFailed | NodeError::AsyncPaymentServicesDisabled => { (error.to_string(), LdkServerErrorCode::InvalidRequestError) }, @@ -125,7 +126,6 @@ impl From for LdkServerError { | NodeError::GossipUpdateTimeout | NodeError::LiquiditySourceUnavailable | NodeError::LiquidityRequestFailed - | NodeError::PayerProofCreationFailed | NodeError::OnchainTxCreationFailed | NodeError::OnchainTxSigningFailed | NodeError::TxSyncFailed @@ -139,3 +139,15 @@ impl From for LdkServerError { LdkServerError::new(error_code, message) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn payer_proof_creation_failure_is_invalid_request() { + let error = LdkServerError::from(NodeError::PayerProofCreationFailed); + + assert_eq!(error.error_code, LdkServerErrorCode::InvalidRequestError); + } +} diff --git a/ldk-server/src/api/mod.rs b/ldk-server/src/api/mod.rs index 6f454c49..65c8985f 100644 --- a/ldk-server/src/api/mod.rs +++ b/ldk-server/src/api/mod.rs @@ -24,6 +24,7 @@ pub(crate) mod bolt11_receive; pub(crate) mod bolt11_receive_for_hash; pub(crate) mod bolt11_receive_via_jit_channel; pub(crate) mod bolt11_send; +pub(crate) mod bolt12_create_payer_proof; pub(crate) mod bolt12_receive; pub(crate) mod bolt12_refund; pub(crate) mod bolt12_send; diff --git a/ldk-server/src/main.rs b/ldk-server/src/main.rs index f587ae51..c363da22 100644 --- a/ldk-server/src/main.rs +++ b/ldk-server/src/main.rs @@ -30,6 +30,7 @@ use ldk_node::config::{Config, ElectrumSyncConfig, EsploraSyncConfig}; use ldk_node::lightning::events::{ClosureReason, PaymentFailureReason}; use ldk_node::lightning::ln::channelmanager::PaymentId; use ldk_node::lightning::ln::types::ChannelId; +use ldk_node::lightning::util::ser::Writeable; use ldk_node::{Builder, CustomTlvRecord, Event, Node}; use ldk_server_grpc::events; use ldk_server_grpc::events::{event_envelope, EventEnvelope}; @@ -528,11 +529,17 @@ fn main() { metrics.update_all_balances(&event_node); } }, - Event::PaymentSuccessful {payment_id, ..} => { + Event::PaymentSuccessful { payment_id, payment_preimage, bolt12_invoice, .. } => { + let payment_preimage = payment_preimage.map(|p| p.to_string()); + let bolt12_invoice = bolt12_invoice.as_ref().and_then(|invoice| { + invoice.bolt12_invoice().map(|i| i.encode().to_lower_hex_string()) + }); send_event_and_upsert_payment(&payment_id, |payment_ref| event_envelope::Event::PaymentSuccessful(events::PaymentSuccessful { payment_id: payment_id.to_string(), payment: Some(payment_ref.clone()), + payment_preimage, + bolt12_invoice, }), &event_node, &event_sender, diff --git a/ldk-server/src/service.rs b/ldk-server/src/service.rs index 3657e2a6..8f9c7cc9 100644 --- a/ldk-server/src/service.rs +++ b/ldk-server/src/service.rs @@ -22,15 +22,16 @@ use ldk_server_grpc::endpoints::{ BOLT11_CLAIM_FOR_ID_PATH, BOLT11_FAIL_FOR_ID_PATH, BOLT11_RECEIVE_FOR_HASH_PATH, BOLT11_RECEIVE_PATH, BOLT11_RECEIVE_VARIABLE_AMOUNT_VIA_JIT_CHANNEL_PATH, BOLT11_RECEIVE_VIA_JIT_CHANNEL_PATH, BOLT11_SEND_PATH, BOLT11_SEND_UNDERPAYING_PATH, - BOLT12_RECEIVE_PATH, BOLT12_RECEIVE_REFUND_PATH, BOLT12_SEND_PATH, BOLT12_SEND_REFUND_PATH, - CLOSE_CHANNEL_PATH, CONNECT_PEER_PATH, DECODE_INVOICE_PATH, DECODE_OFFER_PATH, - DISCONNECT_PEER_PATH, EXPORT_PATHFINDING_SCORES_PATH, FORCE_CLOSE_CHANNEL_PATH, - GET_BALANCES_PATH, GET_METRICS_PATH, GET_NODE_INFO_PATH, GET_PAYMENT_DETAILS_PATH, - GRAPH_GET_CHANNEL_PATH, GRAPH_GET_NODE_PATH, GRAPH_LIST_CHANNELS_PATH, GRAPH_LIST_NODES_PATH, - LIST_CHANNELS_PATH, LIST_FORWARDED_PAYMENTS_PATH, LIST_PAYMENTS_PATH, LIST_PEERS_PATH, - ONCHAIN_RECEIVE_PATH, ONCHAIN_SEND_PATH, OPEN_CHANNEL_PATH, SIGN_MESSAGE_PATH, SPLICE_IN_PATH, - SPLICE_OUT_PATH, SPONTANEOUS_SEND_PATH, SUBSCRIBE_EVENTS_PATH, UNIFIED_SEND_PATH, - UPDATE_CHANNEL_CONFIG_PATH, VERIFY_SIGNATURE_PATH, + BOLT12_CREATE_PAYER_PROOF_PATH, BOLT12_RECEIVE_PATH, BOLT12_RECEIVE_REFUND_PATH, + BOLT12_SEND_PATH, BOLT12_SEND_REFUND_PATH, CLOSE_CHANNEL_PATH, CONNECT_PEER_PATH, + DECODE_INVOICE_PATH, DECODE_OFFER_PATH, DISCONNECT_PEER_PATH, EXPORT_PATHFINDING_SCORES_PATH, + FORCE_CLOSE_CHANNEL_PATH, GET_BALANCES_PATH, GET_METRICS_PATH, GET_NODE_INFO_PATH, + GET_PAYMENT_DETAILS_PATH, GRAPH_GET_CHANNEL_PATH, GRAPH_GET_NODE_PATH, + GRAPH_LIST_CHANNELS_PATH, GRAPH_LIST_NODES_PATH, LIST_CHANNELS_PATH, + LIST_FORWARDED_PAYMENTS_PATH, LIST_PAYMENTS_PATH, LIST_PEERS_PATH, ONCHAIN_RECEIVE_PATH, + ONCHAIN_SEND_PATH, OPEN_CHANNEL_PATH, SIGN_MESSAGE_PATH, SPLICE_IN_PATH, SPLICE_OUT_PATH, + SPONTANEOUS_SEND_PATH, SUBSCRIBE_EVENTS_PATH, UNIFIED_SEND_PATH, UPDATE_CHANNEL_CONFIG_PATH, + VERIFY_SIGNATURE_PATH, }; use ldk_server_grpc::events::EventEnvelope; use ldk_server_grpc::grpc::{ @@ -51,6 +52,7 @@ use crate::api::bolt11_receive_via_jit_channel::{ handle_bolt11_receive_via_jit_channel_request, }; use crate::api::bolt11_send::{handle_bolt11_send_request, handle_bolt11_send_underpaying_request}; +use crate::api::bolt12_create_payer_proof::handle_bolt12_create_payer_proof_request; use crate::api::bolt12_receive::handle_bolt12_receive_request; use crate::api::bolt12_refund::{ handle_bolt12_receive_refund_request, handle_bolt12_send_refund_request, @@ -337,6 +339,10 @@ impl Service> for NodeService { handle_grpc_unary(context, body_bytes, handle_bolt12_receive_refund_request) .await }, + BOLT12_CREATE_PAYER_PROOF_PATH => { + handle_grpc_unary(context, body_bytes, handle_bolt12_create_payer_proof_request) + .await + }, OPEN_CHANNEL_PATH => { handle_grpc_unary(context, body_bytes, handle_open_channel).await }, From 3e9a07d6b7c995a7da2d25f2231070077711fac5 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Mon, 7 Sep 2026 23:27:51 -0500 Subject: [PATCH 6/9] Use one writer for payment metrics Refresh payment counters from paginated node snapshots and remove concurrent event increments. Initialize all metrics before polling and delay the first poll to avoid repeating the startup scan. AI assistance: OpenAI Codex was used for this change. --- ldk-server/src/main.rs | 11 +++--- ldk-server/src/util/metrics.rs | 63 +++++++++++++--------------------- 2 files changed, 28 insertions(+), 46 deletions(-) diff --git a/ldk-server/src/main.rs b/ldk-server/src/main.rs index c363da22..3171ed15 100644 --- a/ldk-server/src/main.rs +++ b/ldk-server/src/main.rs @@ -333,12 +333,13 @@ fn main() { let metrics: Option> = if config_file.metrics_enabled { let poll_metrics_interval = Duration::from_secs(config_file.poll_metrics_interval.unwrap_or(60)); let metrics_node = Arc::clone(&node); - let mut interval = tokio::time::interval(poll_metrics_interval); + let first_poll = tokio::time::Instant::now() + poll_metrics_interval; + let mut interval = tokio::time::interval_at(first_poll, poll_metrics_interval); let metrics = Arc::new(Metrics::new()); let metrics_bg = Arc::clone(&metrics); - // Initialize metrics that are event-driven to ensure they start with correct values from persistence - metrics.initialize_payment_metrics(&metrics_node); + // Initialize metrics before the first delayed poll. + metrics.initialize_metrics(&metrics_node); runtime.spawn(async move { loop { @@ -546,7 +547,6 @@ fn main() { Arc::clone(&paginated_store)); if let Some(metrics) = &metrics { - metrics.update_payments_count(true); metrics.update_all_balances(&event_node); } }, @@ -562,9 +562,6 @@ fn main() { &event_sender, Arc::clone(&paginated_store)); - if let Some(metrics) = &metrics { - metrics.update_payments_count(false); - } }, Event::PaymentClaimable { payment_id, custom_records, claim_deadline, claimable_amount_msat, .. } => { send_event_and_upsert_payment( diff --git a/ldk-server/src/util/metrics.rs b/ldk-server/src/util/metrics.rs index 2cebbc53..13d2e212 100644 --- a/ldk-server/src/util/metrics.rs +++ b/ldk-server/src/util/metrics.rs @@ -16,13 +16,8 @@ //! 1. **Periodic Polling**: The `update_all_pollable_metrics` function is called at a regular //! interval (`poll_metrics_interval`) configurable via the config file but defaults to 60secs if unset, to perform a full recount of metrics like peer count, //! payments count, and channels metrics. -//! 2. **Event-Driven Updates**: For metrics that can change frequently and where a full recount -//! would be inefficient (e.g., total_successful_payments_count, balances), a hybrid approach is used. -//! - `initialize_payment_metrics` is called once at startup to get the accurate persisted state. -//! - `update_payments_count` is called incrementally whenever a relevant event (like -//! `PaymentSuccessful` or `PaymentFailed`) occurs. -//! - `update_all_balances` is called when we receive a `PaymentSuccessful` event to update all balance metrics. -//! - `update_channels_count` is called when we receive a `ChannelReady` or `ChannelClosed` event to update the channels metrics. +//! 2. **Event-Driven Updates**: Balance and total channel metrics are refreshed when related +//! events occur. Payment counts have one writer and are updated only by the periodic poll. //! //! The `gather_metrics` function collects all current metric values and formats them into the //! plain-text format that Prometheus scrapers expect. This output is exposed via an @@ -104,12 +99,11 @@ impl Metrics { self.total_peers_count.store(total_peers_count, Ordering::Relaxed); } - pub fn update_payments_count(&self, is_successful: bool) { - if is_successful { - self.total_successful_payments_count.fetch_add(1, Ordering::Relaxed); - } else { - self.total_failed_payments_count.fetch_add(1, Ordering::Relaxed); - } + fn store_payment_counts(&self, counts: PaymentCounts) { + self.total_payments_count.store(counts.total, Ordering::Relaxed); + self.total_successful_payments_count.store(counts.successful, Ordering::Relaxed); + self.total_pending_payments_count.store(counts.pending, Ordering::Relaxed); + self.total_failed_payments_count.store(counts.failed, Ordering::Relaxed); } pub fn update_channels_count(&self, is_closed: bool) { @@ -120,19 +114,26 @@ impl Metrics { } } - pub fn initialize_payment_metrics(&self, node: &Node) { + pub fn initialize_metrics(&self, node: &Node) { match payment_status_counts(node) { - Ok(counts) => { - self.total_successful_payments_count.store(counts.successful, Ordering::Relaxed); - self.total_failed_payments_count.store(counts.failed, Ordering::Relaxed); - self.total_pending_payments_count.store(counts.pending, Ordering::Relaxed); - }, + Ok(counts) => self.store_payment_counts(counts), Err(e) => error!("Failed to initialize payment metrics: {e}"), } - let channels_count = node.list_channels().len() as i64; - self.total_channels_count.store(channels_count, Ordering::Relaxed); + let all_channels = node.list_channels(); + self.total_channels_count.store(all_channels.len() as i64, Ordering::Relaxed); + + let public_channels_count = + all_channels.iter().filter(|channel_details| channel_details.is_announced).count() + as i64; + self.total_public_channels_count.store(public_channels_count, Ordering::Relaxed); + + let private_channels_count = + all_channels.iter().filter(|channel_details| !channel_details.is_announced).count() + as i64; + self.total_private_channels_count.store(private_channels_count, Ordering::Relaxed); + self.update_peer_count(node); self.update_all_balances(node); } @@ -153,11 +154,9 @@ impl Metrics { pub fn update_all_pollable_metrics(&self, node: &Node) { let all_channels = node.list_channels(); + match payment_status_counts(node) { - Ok(counts) => { - self.total_payments_count.store(counts.total, Ordering::Relaxed); - self.total_pending_payments_count.store(counts.pending, Ordering::Relaxed); - }, + Ok(counts) => self.store_payment_counts(counts), Err(e) => error!("Failed to update payment metrics: {e}"), } @@ -318,20 +317,6 @@ mod tests { assert!(result.contains("ldk_server_total_lightning_balance_sats 0")); } - #[test] - fn test_update_payments_count() { - let metrics = Metrics::new(); - - metrics.total_successful_payments_count.store(10, Ordering::Relaxed); - metrics.total_failed_payments_count.store(5, Ordering::Relaxed); - - metrics.update_payments_count(true); - metrics.update_payments_count(false); - - assert_eq!(metrics.total_successful_payments_count.load(Ordering::Relaxed), 11); - assert_eq!(metrics.total_failed_payments_count.load(Ordering::Relaxed), 6); - } - #[test] fn test_metrics_update_and_gather() { let metrics = Metrics::new(); From 9244a3bdc8cd27c32defcfd682e8fb9f727fc9d8 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Mon, 7 Sep 2026 23:28:01 -0500 Subject: [PATCH 7/9] Migrate payment history to node pagination Read payment history directly from LDK Node and stop duplicating payment records in the server database. Retain the server store for forwarded-payment history. Replace structured page tokens with opaque strings across both listing APIs and adapt CLI pagination to pass them through. Existing clients must update for the token schema change. AI assistance: OpenAI Codex was used for this change. --- docs/api-guide.md | 7 ++- docs/configuration.md | 9 +-- docs/operations.md | 4 +- ldk-server-cli/src/main.rs | 56 ++++++++++--------- ldk-server-cli/src/types.rs | 14 ++--- ldk-server-grpc/src/api.rs | 26 +++++---- ldk-server-grpc/src/proto/api.proto | 18 +++--- ldk-server-grpc/src/proto/types.proto | 6 -- ldk-server-grpc/src/types.rs | 12 ---- ldk-server-mcp/src/tools/schema.rs | 9 +-- ldk-server/src/api/list_forwarded_payments.rs | 41 ++++++++++++-- ldk-server/src/api/list_payments.rs | 48 ++-------------- ldk-server/src/io/persist/mod.rs | 4 -- ldk-server/src/main.rs | 50 ++++------------- 14 files changed, 125 insertions(+), 179 deletions(-) diff --git a/docs/api-guide.md b/docs/api-guide.md index 8ce65523..80a64946 100644 --- a/docs/api-guide.md +++ b/docs/api-guide.md @@ -271,8 +271,11 @@ force-closure of the channel. `ListPayments` and `ListForwardedPayments` support cursor-based pagination: -1. Make the first request with your desired `number_of_payments` page size. +1. Make the first request without a `page_token`. The server controls the page size. 2. If the response includes a `next_page_token`, pass it as `page_token` in the next request. 3. When `next_page_token` is absent, you have reached the end of the results. -Results are ordered by creation time (most recent first). +The page token is one opaque string. Do not parse or modify it. Results are ordered by creation +time (most recent first). + +The CLI `--number-of-payments` option combines multiple pages. It does not set the gRPC page size. diff --git a/docs/configuration.md b/docs/configuration.md index c31926ed..e045cf17 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -209,11 +209,12 @@ Two resolution methods are supported via the `mode` field: / # e.g., bitcoin/, regtest/, signet/ api_key # API key ldk-server.log # Log file - ldk_node_data.sqlite # LDK Node state (channels, on-chain wallet) - ldk_server_data.sqlite # Payment and forwarding history + ldk_node_data.sqlite # LDK Node state (channels, wallet, payments) + ldk_server_data.sqlite # Forwarded-payment history ``` The mnemonic is the node's master secret, required to recover on-chain funds. On first start, ldk-server generates a fresh 24-word BIP39 mnemonic at `/keys_mnemonic` if the file -does not already exist. `ldk_node_data.sqlite` holds channel state, both are required to recover -channel funds. See [Operations - Backups](operations.md#backups) for backup guidance. +does not already exist. `ldk_node_data.sqlite` holds channel state and payment history. Both files +are required to recover channel funds. See [Operations - Backups](operations.md#backups) for backup +guidance. diff --git a/docs/operations.md b/docs/operations.md index c32390e3..178a7226 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -53,8 +53,8 @@ the following config to `/etc/logrotate.d/ldk-server` (adjust the log path to ma | File | Priority | Description | | -------------------------------------- | ------------ | -------------------------------------------------------------------------- | | `/keys_mnemonic` | **Critical** | BIP39 mnemonic. Required to recover on-chain funds. Default for new installs. | -| `/ldk_node_data.sqlite` | **Critical** | Channel state and on-chain wallet data. Required to recover channel funds. | -| `/ldk_server_data.sqlite` | Nice-to-have | Payment and forwarding history | +| `/ldk_node_data.sqlite` | **Critical** | Channel state, on-chain wallet data, and payment history. Required to recover channel funds. | +| `/ldk_server_data.sqlite` | Nice-to-have | Forwarded-payment history | ### What is Reconstructable diff --git a/ldk-server-cli/src/main.rs b/ldk-server-cli/src/main.rs index 6b383dc5..1b951197 100644 --- a/ldk-server-cli/src/main.rs +++ b/ldk-server-cli/src/main.rs @@ -50,7 +50,7 @@ use ldk_server_client::ldk_server_grpc::api::{ }; use ldk_server_client::ldk_server_grpc::types::{ bolt11_invoice_description, Bolt11InvoiceDescription, ChannelConfig, CustomTlvRecord, - PageToken, PayerProofOptions, RouteParametersConfig, + PayerProofOptions, RouteParametersConfig, }; use ldk_server_client::{ DEFAULT_EXPIRY_SECS, DEFAULT_MAX_CHANNEL_SATURATION_POWER_OF_HALF, DEFAULT_MAX_PATH_COUNT, @@ -540,7 +540,7 @@ enum Commands { )] number_of_payments: Option, #[arg(long)] - #[arg(help = "Page token to continue from a previous page (format: token:index)")] + #[arg(help = "Opaque page token returned by a previous request")] page_token: Option, }, #[command(about = "Get details of a specific payment by its payment ID")] @@ -556,7 +556,7 @@ enum Commands { help = "Fetch at least this many forwarded payments by iterating through multiple pages. Returns combined results with the last page token. If not provided, returns only a single page." )] number_of_payments: Option, - #[arg(long, help = "Page token to continue from a previous page (format: token:index)")] + #[arg(long, help = "Opaque page token returned by a previous request")] page_token: Option, }, #[command(about = "Update the forwarding fees and CLTV expiry delta for an existing channel")] @@ -1174,9 +1174,6 @@ async fn main() { ); }, Commands::ListPayments { number_of_payments, page_token } => { - let page_token = page_token - .map(|token_str| parse_page_token(&token_str).unwrap_or_else(|e| handle_error(e))); - handle_response_result::<_, CliListPaymentsResponse>( fetch_paginated( number_of_payments, @@ -1193,9 +1190,6 @@ async fn main() { ); }, Commands::ListForwardedPayments { number_of_payments, page_token } => { - let page_token = page_token - .map(|token_str| parse_page_token(&token_str).unwrap_or_else(|e| handle_error(e))); - handle_response_result::<_, CliListForwardedPaymentsResponse>( fetch_paginated( number_of_payments, @@ -1334,9 +1328,8 @@ fn build_open_channel_config( } async fn fetch_paginated( - target_count: Option, initial_page_token: Option, - fetch_page: impl Fn(Option) -> Fut, - extract: impl Fn(R) -> (Vec, Option), + target_count: Option, initial_page_token: Option, + fetch_page: impl Fn(Option) -> Fut, extract: impl Fn(R) -> (Vec, Option), ) -> Result, LdkServerError> where Fut: std::future::Future>, @@ -1446,20 +1439,6 @@ fn parse_bolt11_invoice_description( } } -fn parse_page_token(token_str: &str) -> Result { - let parts: Vec<&str> = token_str.split(':').collect(); - if parts.len() != 2 { - return Err(LdkServerError::new( - InvalidRequestError, - "Page token must be in format 'token:index'".to_string(), - )); - } - let index = parts[1].parse::().map_err(|_| { - LdkServerError::new(InvalidRequestError, "Invalid page token index".to_string()) - })?; - Ok(PageToken { token: parts[0].to_string(), index }) -} - fn parse_custom_tlv(s: &str) -> Result<(u64, Vec), String> { let (type_str, hex_str) = s.split_once(':').ok_or_else(|| format!("expected :, got '{s}'"))?; @@ -1494,6 +1473,31 @@ fn handle_error(e: LdkServerError) -> ! { mod tests { use super::*; + #[tokio::test] + async fn fetch_paginated_collects_multiple_pages() { + let response = fetch_paginated( + Some(3), + None, + |page_token| async move { + match page_token { + None => { + Ok::<_, LdkServerError>((vec![1, 2], Some("store:v2:cursor:7".to_string()))) + }, + Some(token) => { + assert_eq!(token, "store:v2:cursor:7"); + Ok((vec![3], None)) + }, + } + }, + |response| response, + ) + .await + .unwrap(); + + assert_eq!(response.list, vec![1, 2, 3]); + assert!(response.next_page_token.is_none()); + } + #[test] fn parse_custom_tlv_accepts_valid_record() { let (type_num, value) = parse_custom_tlv("65537:deadbeef").unwrap(); diff --git a/ldk-server-cli/src/types.rs b/ldk-server-cli/src/types.rs index 895ea171..698845e8 100644 --- a/ldk-server-cli/src/types.rs +++ b/ldk-server-cli/src/types.rs @@ -17,11 +17,11 @@ use std::fmt; use std::str::FromStr; use hex_conservative::{DisplayHex, FromHex}; -use ldk_server_client::ldk_server_grpc::types::{ForwardedPayment, PageToken, Payment}; +use ldk_server_client::ldk_server_grpc::types::{ForwardedPayment, Payment}; use serde::Serialize; -/// CLI-specific wrapper for paginated responses that formats the page token -/// as "token:idx" instead of a JSON object. +/// CLI-specific wrapper for paginated responses that keeps the page token as +/// one opaque string. #[derive(Debug, Clone, Serialize)] pub struct CliPaginatedResponse { /// List of items. @@ -32,18 +32,14 @@ pub struct CliPaginatedResponse { } impl CliPaginatedResponse { - pub fn new(list: Vec, next_page_token: Option) -> Self { - Self { list, next_page_token: next_page_token.map(format_page_token) } + pub fn new(list: Vec, next_page_token: Option) -> Self { + Self { list, next_page_token } } } pub type CliListPaymentsResponse = CliPaginatedResponse; pub type CliListForwardedPaymentsResponse = CliPaginatedResponse; -fn format_page_token(token: PageToken) -> String { - format!("{}:{}", token.token, token.index) -} - /// A denomination-aware amount that stores its value internally in millisatoshis. /// /// Accepts the following formats when parsed from a string: diff --git a/ldk-server-grpc/src/api.rs b/ldk-server-grpc/src/api.rs index ed3b9259..b3507f81 100644 --- a/ldk-server-grpc/src/api.rs +++ b/ldk-server-grpc/src/api.rs @@ -896,14 +896,14 @@ pub struct GetPaymentDetailsResponse { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct ListPaymentsRequest { - /// `page_token` is a pagination token. + /// `page_token` is an opaque pagination token string. /// /// To query for the first page, `page_token` must not be specified. /// /// For subsequent pages, use the value that was returned as `next_page_token` in the previous /// page's response. - #[prost(message, optional, tag = "1")] - pub page_token: ::core::option::Option, + #[prost(string, optional, tag = "1")] + pub page_token: ::core::option::Option<::prost::alloc::string::String>, } /// The response for the `ListPayments` RPC. On failure, a gRPC error status is returned. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -915,7 +915,8 @@ pub struct ListPaymentsResponse { /// List of payments. #[prost(message, repeated, tag = "1")] pub payments: ::prost::alloc::vec::Vec, - /// `next_page_token` is a pagination token, used to retrieve the next page of results. + /// `next_page_token` is an opaque pagination token string used to retrieve the next page of + /// results. /// Use this value to query for next-page of paginated operation, by specifying /// this value as the `page_token` in the next request. /// @@ -928,8 +929,8 @@ pub struct ListPaymentsResponse { /// /// **Caution**: Clients must not assume a specific number of records to be present in a page for /// paginated response. - #[prost(message, optional, tag = "2")] - pub next_page_token: ::core::option::Option, + #[prost(string, optional, tag = "2")] + pub next_page_token: ::core::option::Option<::prost::alloc::string::String>, } /// Retrieves list of all forwarded payments. /// See more: @@ -939,14 +940,14 @@ pub struct ListPaymentsResponse { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct ListForwardedPaymentsRequest { - /// `page_token` is a pagination token. + /// `page_token` is an opaque pagination token string. /// /// To query for the first page, `page_token` must not be specified. /// /// For subsequent pages, use the value that was returned as `next_page_token` in the previous /// page's response. - #[prost(message, optional, tag = "1")] - pub page_token: ::core::option::Option, + #[prost(string, optional, tag = "1")] + pub page_token: ::core::option::Option<::prost::alloc::string::String>, } /// The response for the `ListForwardedPayments` RPC. On failure, a gRPC error status is returned. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -958,7 +959,8 @@ pub struct ListForwardedPaymentsResponse { /// List of forwarded payments. #[prost(message, repeated, tag = "1")] pub forwarded_payments: ::prost::alloc::vec::Vec, - /// `next_page_token` is a pagination token, used to retrieve the next page of results. + /// `next_page_token` is an opaque pagination token string used to retrieve the next page of + /// results. /// Use this value to query for next-page of paginated operation, by specifying /// this value as the `page_token` in the next request. /// @@ -971,8 +973,8 @@ pub struct ListForwardedPaymentsResponse { /// /// **Caution**: Clients must not assume a specific number of records to be present in a page for /// paginated response. - #[prost(message, optional, tag = "2")] - pub next_page_token: ::core::option::Option, + #[prost(string, optional, tag = "2")] + pub next_page_token: ::core::option::Option<::prost::alloc::string::String>, } /// Sign a message with the node's secret key. /// See more: diff --git a/ldk-server-grpc/src/proto/api.proto b/ldk-server-grpc/src/proto/api.proto index 0e72159c..3d79112b 100644 --- a/ldk-server-grpc/src/proto/api.proto +++ b/ldk-server-grpc/src/proto/api.proto @@ -650,13 +650,13 @@ message GetPaymentDetailsResponse { // Retrieves list of all payments. // See more: https://docs.rs/ldk-node/latest/ldk_node/struct.Node.html#method.list_payments message ListPaymentsRequest { - // `page_token` is a pagination token. + // `page_token` is an opaque pagination token string. // // To query for the first page, `page_token` must not be specified. // // For subsequent pages, use the value that was returned as `next_page_token` in the previous // page's response. - optional types.PageToken page_token = 1; + optional string page_token = 1; } // The response for the `ListPayments` RPC. On failure, a gRPC error status is returned. @@ -664,7 +664,8 @@ message ListPaymentsResponse { // List of payments. repeated types.Payment payments = 1; - // `next_page_token` is a pagination token, used to retrieve the next page of results. + // `next_page_token` is an opaque pagination token string used to retrieve the next page of + // results. // Use this value to query for next-page of paginated operation, by specifying // this value as the `page_token` in the next request. // @@ -677,19 +678,19 @@ message ListPaymentsResponse { // // **Caution**: Clients must not assume a specific number of records to be present in a page for // paginated response. - optional types.PageToken next_page_token = 2; + optional string next_page_token = 2; } // Retrieves list of all forwarded payments. // See more: https://docs.rs/ldk-node/latest/ldk_node/enum.Event.html#variant.PaymentForwarded message ListForwardedPaymentsRequest { - // `page_token` is a pagination token. + // `page_token` is an opaque pagination token string. // // To query for the first page, `page_token` must not be specified. // // For subsequent pages, use the value that was returned as `next_page_token` in the previous // page's response. - optional types.PageToken page_token = 1; + optional string page_token = 1; } // The response for the `ListForwardedPayments` RPC. On failure, a gRPC error status is returned. @@ -697,7 +698,8 @@ message ListForwardedPaymentsResponse { // List of forwarded payments. repeated types.ForwardedPayment forwarded_payments = 1; - // `next_page_token` is a pagination token, used to retrieve the next page of results. + // `next_page_token` is an opaque pagination token string used to retrieve the next page of + // results. // Use this value to query for next-page of paginated operation, by specifying // this value as the `page_token` in the next request. // @@ -710,7 +712,7 @@ message ListForwardedPaymentsResponse { // // **Caution**: Clients must not assume a specific number of records to be present in a page for // paginated response. - optional types.PageToken next_page_token = 2; + optional string next_page_token = 2; } // Sign a message with the node's secret key. diff --git a/ldk-server-grpc/src/proto/types.proto b/ldk-server-grpc/src/proto/types.proto index 0debfcf6..cea320b2 100644 --- a/ldk-server-grpc/src/proto/types.proto +++ b/ldk-server-grpc/src/proto/types.proto @@ -880,12 +880,6 @@ message AwaitingThresholdConfirmations { uint64 amount_satoshis = 5; } -// Token used to determine start of next page in paginated APIs. -message PageToken { - string token = 1; - int64 index = 2; -} - message Bolt11InvoiceDescription { oneof kind { string direct = 1; diff --git a/ldk-server-grpc/src/types.rs b/ldk-server-grpc/src/types.rs index 1ebef88a..6cef8912 100644 --- a/ldk-server-grpc/src/types.rs +++ b/ldk-server-grpc/src/types.rs @@ -1087,18 +1087,6 @@ pub struct AwaitingThresholdConfirmations { #[prost(uint64, tag = "5")] pub amount_satoshis: u64, } -/// Token used to determine start of next page in paginated APIs. -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] -#[cfg_attr(feature = "serde", serde(default))] -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct PageToken { - #[prost(string, tag = "1")] - pub token: ::prost::alloc::string::String, - #[prost(int64, tag = "2")] - pub index: i64, -} #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] #[cfg_attr(feature = "serde", serde(default))] diff --git a/ldk-server-mcp/src/tools/schema.rs b/ldk-server-mcp/src/tools/schema.rs index fada6724..9cf732d1 100644 --- a/ldk-server-mcp/src/tools/schema.rs +++ b/ldk-server-mcp/src/tools/schema.rs @@ -132,13 +132,8 @@ fn channel_config_schema() -> Value { fn page_token_schema() -> Value { json!({ - "type": "object", - "description": "Pagination token from a previous response", - "properties": { - "token": { "type": "string" }, - "index": { "type": "integer" } - }, - "required": ["token", "index"] + "type": "string", + "description": "Opaque pagination token from a previous response" }) } diff --git a/ldk-server/src/api/list_forwarded_payments.rs b/ldk-server/src/api/list_forwarded_payments.rs index f784c457..7440a414 100644 --- a/ldk-server/src/api/list_forwarded_payments.rs +++ b/ldk-server/src/api/list_forwarded_payments.rs @@ -11,11 +11,11 @@ use std::sync::Arc; use bytes::Bytes; use ldk_server_grpc::api::{ListForwardedPaymentsRequest, ListForwardedPaymentsResponse}; -use ldk_server_grpc::types::{ForwardedPayment, PageToken}; +use ldk_server_grpc::types::ForwardedPayment; use prost::Message; use crate::api::error::LdkServerError; -use crate::api::error::LdkServerErrorCode::InternalServerError; +use crate::api::error::LdkServerErrorCode::{InternalServerError, InvalidRequestError}; use crate::io::persist::{ FORWARDED_PAYMENTS_PERSISTENCE_PRIMARY_NAMESPACE, FORWARDED_PAYMENTS_PERSISTENCE_SECONDARY_NAMESPACE, @@ -25,7 +25,7 @@ use crate::service::Context; pub(crate) async fn handle_list_forwarded_payments_request( context: Arc, request: ListForwardedPaymentsRequest, ) -> Result { - let page_token = request.page_token.map(|p| (p.token, p.index)); + let page_token = request.page_token.map(parse_page_token).transpose()?; let list_response = context .paginated_kv_store .list( @@ -67,9 +67,38 @@ pub(crate) async fn handle_list_forwarded_payments_request( } let response = ListForwardedPaymentsResponse { forwarded_payments, - next_page_token: list_response - .next_page_token - .map(|(token, index)| PageToken { token, index }), + next_page_token: list_response.next_page_token.map(format_page_token), }; Ok(response) } + +fn parse_page_token(page_token: String) -> Result<(String, i64), LdkServerError> { + let (token, index) = page_token.rsplit_once(':').ok_or_else(invalid_page_token)?; + let index = index.parse::().map_err(|_| invalid_page_token())?; + Ok((token.to_string(), index)) +} + +fn format_page_token((token, index): (String, i64)) -> String { + format!("{token}:{index}") +} + +fn invalid_page_token() -> LdkServerError { + LdkServerError::new(InvalidRequestError, "Invalid page token".to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn page_token_round_trip_preserves_colons() { + let token = ("store:v2:cursor".to_string(), 7); + assert_eq!(parse_page_token(format_page_token(token.clone())).unwrap(), token); + } + + #[test] + fn parse_page_token_rejects_invalid_index() { + let error = parse_page_token("cursor:not-an-index".to_string()).unwrap_err(); + assert_eq!(error.error_code, InvalidRequestError); + } +} diff --git a/ldk-server/src/api/list_payments.rs b/ldk-server/src/api/list_payments.rs index 502d7458..6a7839dc 100644 --- a/ldk-server/src/api/list_payments.rs +++ b/ldk-server/src/api/list_payments.rs @@ -9,58 +9,22 @@ use std::sync::Arc; -use bytes::Bytes; +use ldk_node::payment::PageToken as NodePageToken; use ldk_server_grpc::api::{ListPaymentsRequest, ListPaymentsResponse}; -use ldk_server_grpc::types::{PageToken, Payment}; -use prost::Message; use crate::api::error::LdkServerError; -use crate::api::error::LdkServerErrorCode::InternalServerError; -use crate::io::persist::{ - PAYMENTS_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENTS_PERSISTENCE_SECONDARY_NAMESPACE, -}; use crate::service::Context; +use crate::util::proto_adapter::payment_to_proto; pub(crate) async fn handle_list_payments_request( context: Arc, request: ListPaymentsRequest, ) -> Result { - let page_token = request.page_token.map(|p| (p.token, p.index)); - let list_response = context - .paginated_kv_store - .list( - PAYMENTS_PERSISTENCE_PRIMARY_NAMESPACE, - PAYMENTS_PERSISTENCE_SECONDARY_NAMESPACE, - page_token, - ) - .map_err(|e| { - LdkServerError::new(InternalServerError, format!("Failed to list payments: {}", e)) - })?; + let page_token = request.page_token.map(NodePageToken::new); + let page = context.node.list_payments(page_token)?; - let mut payments: Vec = Vec::with_capacity(list_response.keys.len()); - for key in list_response.keys { - let payment_bytes = context - .paginated_kv_store - .read( - PAYMENTS_PERSISTENCE_PRIMARY_NAMESPACE, - PAYMENTS_PERSISTENCE_SECONDARY_NAMESPACE, - &key, - ) - .map_err(|e| { - LdkServerError::new( - InternalServerError, - format!("Failed to read payment data: {}", e), - ) - })?; - let payment = Payment::decode(Bytes::from(payment_bytes)).map_err(|e| { - LdkServerError::new(InternalServerError, format!("Failed to decode payment: {}", e)) - })?; - payments.push(payment); - } let response = ListPaymentsResponse { - payments, - next_page_token: list_response - .next_page_token - .map(|(token, index)| PageToken { token, index }), + payments: page.payments.into_iter().map(payment_to_proto).collect(), + next_page_token: page.next_page_token.map(|token| token.to_string()), }; Ok(response) } diff --git a/ldk-server/src/io/persist/mod.rs b/ldk-server/src/io/persist/mod.rs index 6c01795b..c4e3e4dc 100644 --- a/ldk-server/src/io/persist/mod.rs +++ b/ldk-server/src/io/persist/mod.rs @@ -13,7 +13,3 @@ pub(crate) mod sqlite_store; /// The forwarded payments will be persisted under this prefix. pub(crate) const FORWARDED_PAYMENTS_PERSISTENCE_PRIMARY_NAMESPACE: &str = "forwarded_payments"; pub(crate) const FORWARDED_PAYMENTS_PERSISTENCE_SECONDARY_NAMESPACE: &str = ""; - -/// The payments will be persisted under this prefix. -pub(crate) const PAYMENTS_PERSISTENCE_PRIMARY_NAMESPACE: &str = "payments"; -pub(crate) const PAYMENTS_PERSISTENCE_SECONDARY_NAMESPACE: &str = ""; diff --git a/ldk-server/src/main.rs b/ldk-server/src/main.rs index 3171ed15..ff86722f 100644 --- a/ldk-server/src/main.rs +++ b/ldk-server/src/main.rs @@ -47,8 +47,7 @@ use crate::io::persist::paginated_kv_store::PaginatedKVStore; use crate::io::persist::sqlite_store::SqliteStore; use crate::io::persist::{ FORWARDED_PAYMENTS_PERSISTENCE_PRIMARY_NAMESPACE, - FORWARDED_PAYMENTS_PERSISTENCE_SECONDARY_NAMESPACE, PAYMENTS_PERSISTENCE_PRIMARY_NAMESPACE, - PAYMENTS_PERSISTENCE_SECONDARY_NAMESPACE, + FORWARDED_PAYMENTS_PERSISTENCE_SECONDARY_NAMESPACE, }; use crate::service::NodeService; use crate::util::config::{load_config, ArgsConfig, ChainSource}; @@ -512,7 +511,7 @@ fn main() { .map(node_to_proto_custom_tlv) .collect(); - send_event_and_upsert_payment( + send_payment_event( &payment_id, move |payment_ref| { event_envelope::Event::PaymentReceived(events::PaymentReceived { @@ -523,7 +522,6 @@ fn main() { }, &event_node, &event_sender, - Arc::clone(&paginated_store), ); if let Some(metrics) = &metrics { @@ -535,7 +533,7 @@ fn main() { let bolt12_invoice = bolt12_invoice.as_ref().and_then(|invoice| { invoice.bolt12_invoice().map(|i| i.encode().to_lower_hex_string()) }); - send_event_and_upsert_payment(&payment_id, + send_payment_event(&payment_id, |payment_ref| event_envelope::Event::PaymentSuccessful(events::PaymentSuccessful { payment_id: payment_id.to_string(), payment: Some(payment_ref.clone()), @@ -543,8 +541,7 @@ fn main() { bolt12_invoice, }), &event_node, - &event_sender, - Arc::clone(&paginated_store)); + &event_sender); if let Some(metrics) = &metrics { metrics.update_all_balances(&event_node); @@ -552,19 +549,18 @@ fn main() { }, Event::PaymentFailed {payment_id, reason, ..} => { let proto_reason = reason.as_ref().map(payment_failure_reason_to_proto); - send_event_and_upsert_payment(&payment_id, + send_payment_event(&payment_id, move |payment_ref| event_envelope::Event::PaymentFailed(events::PaymentFailed { payment_id: payment_id.to_string(), payment: Some(payment_ref.clone()), reason: proto_reason.map(|r| r as i32), }), &event_node, - &event_sender, - Arc::clone(&paginated_store)); + &event_sender); }, Event::PaymentClaimable { payment_id, custom_records, claim_deadline, claimable_amount_msat, .. } => { - send_event_and_upsert_payment( + send_payment_event( &payment_id, |payment_ref| { event_envelope::Event::PaymentClaimable( @@ -579,7 +575,6 @@ fn main() { }, &event_node, &event_sender, - Arc::clone(&paginated_store), ); }, Event::PaymentForwarded { @@ -761,10 +756,9 @@ fn main() { log::logger().flush(); } -fn send_event_and_upsert_payment( +fn send_payment_event( payment_id: &PaymentId, payment_to_event: impl FnOnce(&Payment) -> event_envelope::Event, event_node: &Node, event_sender: &broadcast::Sender, - paginated_store: Arc, ) { match event_node.payment(payment_id) { Ok(Some(payment_details)) => { @@ -775,7 +769,9 @@ fn send_event_and_upsert_payment( debug!("No event subscribers connected, skipping event: {e}"); } - upsert_payment_details(event_node, Arc::clone(&paginated_store), &payment); + if let Err(e) = event_node.event_handled() { + error!("Failed to mark event as handled: {e}"); + } }, Ok(None) => error!("Unable to find payment with payment ID: {payment_id}"), Err(e) => error!("Failed to retrieve payment with payment ID {payment_id}: {e}"), @@ -957,30 +953,6 @@ fn closure_reason_details( } } -fn upsert_payment_details( - event_node: &Node, paginated_store: Arc, payment: &Payment, -) { - let time = - SystemTime::now().duration_since(UNIX_EPOCH).expect("Time must be > 1970").as_secs() as i64; - - match paginated_store.write( - PAYMENTS_PERSISTENCE_PRIMARY_NAMESPACE, - PAYMENTS_PERSISTENCE_SECONDARY_NAMESPACE, - &payment.payment_id, - time, - &payment.encode_to_vec(), - ) { - Ok(_) => { - if let Err(e) = event_node.event_handled() { - error!("Failed to mark event as handled: {e}"); - } - }, - Err(e) => { - error!("Failed to write payment to persistence: {e}"); - }, - } -} - /// Loads the API key from a file, or generates a new one if it doesn't exist. /// The API key file is stored with 0400 permissions (read-only for owner). fn load_or_generate_api_key(storage_dir: &Path) -> std::io::Result { From 49d3735957ada36f96da0f4f9b23a796d4c8eca9 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Mon, 7 Sep 2026 23:28:39 -0500 Subject: [PATCH 8/9] Define best-effort payment event delivery Acknowledge payment events when no subscriber is connected or required payment details cannot be read, so the node event queue can continue. Skip payment lookups when no subscriber is connected. Document live delivery limits, state reconciliation, unrecoverable payer-proof inputs, and automatic failure of unclaimed hold payments at their claim deadline. AI assistance: OpenAI Codex was used for this change. --- docs/api-guide.md | 29 ++++++++++++++++++++------ ldk-server-grpc/src/api.rs | 10 ++++++++- ldk-server-grpc/src/events.rs | 3 +++ ldk-server-grpc/src/proto/api.proto | 10 ++++++++- ldk-server-grpc/src/proto/events.proto | 3 +++ ldk-server/src/main.rs | 23 ++++++++++++++------ 6 files changed, 64 insertions(+), 14 deletions(-) diff --git a/docs/api-guide.md b/docs/api-guide.md index 80a64946..71feb52e 100644 --- a/docs/api-guide.md +++ b/docs/api-guide.md @@ -216,8 +216,21 @@ See [Pagination](#pagination) below for how to page through results. | `SpliceNegotiated` | A channel splice was negotiated and the funding transaction is pending confirmation | | `SpliceNegotiationFailed` | A channel splice negotiation round failed | -Events are broadcast to all connected subscribers. The server uses a bounded broadcast channel -(capacity 1024). A slow subscriber that falls behind will miss events. +> [!WARNING] +> `SubscribeEvents` is a best-effort stream of new events. Events are not persisted for +> subscribers, cannot be replayed after reconnecting, and have no client acknowledgement. +> Acceptance by the server's broadcast channel does not guarantee that a client received or +> processed an event. + +Events are broadcast to all currently connected subscribers. The server uses a bounded broadcast +channel (capacity 1024), so a slow subscriber that falls behind will miss events. Disconnected +clients also miss events and receive only new events after reconnecting. If the server cannot read +data required to construct a payment event, it logs the error and skips that event so the event +queue can continue processing. + +Use events as notifications. After reconnecting, reconcile recoverable state with APIs such as +`GetPaymentDetails`, `ListPayments`, `ListForwardedPayments`, and `ListChannels`. Some event fields +cannot be recovered through these APIs. ### Metrics @@ -236,7 +249,9 @@ Subscribe with `SubscribeEvents` before you send a BOLT 12 payment. Events are n When `PaymentSuccessful` arrives, retain its `payment_id`, `payment_preimage`, and `bolt12_invoice`. Pass these values to `Bolt12CreatePayerProof`. The request can also select the -optional invoice fields that the proof discloses. +optional invoice fields that the proof discloses. Payment history APIs cannot recover all the +inputs required to create a proof if this event is missed. Save these values before processing +other events. The `bolt12_invoice` field is absent for static-invoice payments. These asynchronous payments cannot produce payer proofs. @@ -263,9 +278,11 @@ stored payment amount, less any skimmed fee. It is not an exact amount check or that many millisatoshis. A larger supplied amount passes this check; omitting it skips the check. Always validate the event's amount before you claim the payment. -The payment is held in a pending state until you explicitly claim or fail it. **You must -always handle each event.** If you do not, the HTLC will eventually time out. This can cause a -force-closure of the channel. +The payment is held in a pending state until you claim it, fail it, or its `claim_deadline` is +reached. `PaymentClaimable` notifications are best-effort and are not replayed. If you miss the +event or do not act before the deadline, LDK Node automatically fails the HTLC backward and the +payment can no longer be claimed. Keep the subscriber healthy and resolve reported persistence +errors before accepting further payments. ## Pagination diff --git a/ldk-server-grpc/src/api.rs b/ldk-server-grpc/src/api.rs index b3507f81..bbb7c756 100644 --- a/ldk-server-grpc/src/api.rs +++ b/ldk-server-grpc/src/api.rs @@ -1422,7 +1422,15 @@ pub struct DecodeOfferResponse { #[prost(bool, tag = "12")] pub is_expired: bool, } -/// Subscribe to a stream of server events. +/// Subscribe to a best-effort stream of new server events. +/// +/// Events are not persisted for subscribers or replayed after reconnecting, and the server does not +/// wait for client acknowledgement. Slow or disconnected subscribers may miss events. Reconcile +/// recoverable state with the listing and detail APIs after reconnecting. Some event fields, +/// including inputs required for payer proofs, cannot be recovered through these APIs. +/// +/// If a PaymentClaimable event is missed and the payment is not otherwise claimed or failed, LDK +/// Node automatically fails the HTLC backward at its claim_deadline. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] #[cfg_attr(feature = "serde", serde(default))] diff --git a/ldk-server-grpc/src/events.rs b/ldk-server-grpc/src/events.rs index 21acae36..9ee08d5d 100644 --- a/ldk-server-grpc/src/events.rs +++ b/ldk-server-grpc/src/events.rs @@ -244,6 +244,9 @@ pub struct PaymentFailed { /// This event is only emitted for payments created via `Bolt11ReceiveForHash`. /// Handle every event by its payment ID before `claim_deadline`. /// The same invoice can produce more than one event. Fail unexpected duplicate or late payments. +/// Delivery through SubscribeEvents is best-effort and is not replayed. If the event is missed and +/// the payment is not otherwise claimed or failed, LDK Node automatically fails the HTLC backward at +/// claim_deadline. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] #[cfg_attr(feature = "serde", serde(default))] diff --git a/ldk-server-grpc/src/proto/api.proto b/ldk-server-grpc/src/proto/api.proto index 3d79112b..2f3bbab8 100644 --- a/ldk-server-grpc/src/proto/api.proto +++ b/ldk-server-grpc/src/proto/api.proto @@ -1021,7 +1021,15 @@ message DecodeOfferResponse { bool is_expired = 12; } -// Subscribe to a stream of server events. +// Subscribe to a best-effort stream of new server events. +// +// Events are not persisted for subscribers or replayed after reconnecting, and the server does not +// wait for client acknowledgement. Slow or disconnected subscribers may miss events. Reconcile +// recoverable state with the listing and detail APIs after reconnecting. Some event fields, +// including inputs required for payer proofs, cannot be recovered through these APIs. +// +// If a PaymentClaimable event is missed and the payment is not otherwise claimed or failed, LDK +// Node automatically fails the HTLC backward at its claim_deadline. message SubscribeEventsRequest {} service LightningNode { diff --git a/ldk-server-grpc/src/proto/events.proto b/ldk-server-grpc/src/proto/events.proto index 08f1efae..f5ff277a 100644 --- a/ldk-server-grpc/src/proto/events.proto +++ b/ldk-server-grpc/src/proto/events.proto @@ -184,6 +184,9 @@ message PaymentFailed { // This event is only emitted for payments created via `Bolt11ReceiveForHash`. // Handle every event by its payment ID before `claim_deadline`. // The same invoice can produce more than one event. Fail unexpected duplicate or late payments. +// Delivery through SubscribeEvents is best-effort and is not replayed. If the event is missed and +// the payment is not otherwise claimed or failed, LDK Node automatically fails the HTLC backward at +// claim_deadline. message PaymentClaimable { // The local identifier used to track the payment, in hex-encoded form. string payment_id = 1; diff --git a/ldk-server/src/main.rs b/ldk-server/src/main.rs index ff86722f..b78919d8 100644 --- a/ldk-server/src/main.rs +++ b/ldk-server/src/main.rs @@ -760,6 +760,14 @@ fn send_payment_event( payment_id: &PaymentId, payment_to_event: impl FnOnce(&Payment) -> event_envelope::Event, event_node: &Node, event_sender: &broadcast::Sender, ) { + if event_sender.receiver_count() == 0 { + debug!("No event subscribers connected, skipping payment event"); + if let Err(e) = event_node.event_handled() { + error!("Failed to mark event as handled: {e}"); + } + return; + } + match event_node.payment(payment_id) { Ok(Some(payment_details)) => { let payment = payment_to_proto(payment_details); @@ -768,13 +776,16 @@ fn send_payment_event( if let Err(e) = event_sender.send(EventEnvelope { event: Some(event) }) { debug!("No event subscribers connected, skipping event: {e}"); } - - if let Err(e) = event_node.event_handled() { - error!("Failed to mark event as handled: {e}"); - } }, - Ok(None) => error!("Unable to find payment with payment ID: {payment_id}"), - Err(e) => error!("Failed to retrieve payment with payment ID {payment_id}: {e}"), + Ok(None) => { + error!("Unable to find payment with payment ID: {payment_id}"); + }, + Err(e) => { + error!("Failed to retrieve payment with payment ID {payment_id}: {e}"); + }, + } + if let Err(e) = event_node.event_handled() { + error!("Failed to mark event as handled: {e}"); } } From 54a6192ac24f910ba2be8a7d445d620c3eb1a9a0 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Mon, 7 Sep 2026 23:28:45 -0500 Subject: [PATCH 9/9] Simplify event conversion and channel metrics Move payment values into event constructors to avoid deep clones and convert event-only fields only when the constructor is called. Share channel visibility counts between initialization and polling, deriving private counts from the same snapshot. AI assistance: OpenAI Codex was used for this change. --- ldk-server/src/main.rs | 55 +++++++++++++++++----------------- ldk-server/src/util/metrics.rs | 29 +++++++----------- 2 files changed, 38 insertions(+), 46 deletions(-) diff --git a/ldk-server/src/main.rs b/ldk-server/src/main.rs index b78919d8..0c88fac3 100644 --- a/ldk-server/src/main.rs +++ b/ldk-server/src/main.rs @@ -506,18 +506,17 @@ fn main() { payment_id, payment_hash, amount_msat ); - let proto_custom_records: Vec<_> = custom_records - .iter() - .map(node_to_proto_custom_tlv) - .collect(); - send_payment_event( &payment_id, - move |payment_ref| { + move |payment| { + let custom_records = custom_records + .iter() + .map(node_to_proto_custom_tlv) + .collect(); event_envelope::Event::PaymentReceived(events::PaymentReceived { payment_id: payment_id.to_string(), - payment: Some(payment_ref.clone()), - custom_records: proto_custom_records, + payment: Some(payment), + custom_records, }) }, &event_node, @@ -529,17 +528,19 @@ fn main() { } }, Event::PaymentSuccessful { payment_id, payment_preimage, bolt12_invoice, .. } => { - let payment_preimage = payment_preimage.map(|p| p.to_string()); - let bolt12_invoice = bolt12_invoice.as_ref().and_then(|invoice| { - invoice.bolt12_invoice().map(|i| i.encode().to_lower_hex_string()) - }); send_payment_event(&payment_id, - |payment_ref| event_envelope::Event::PaymentSuccessful(events::PaymentSuccessful { - payment_id: payment_id.to_string(), - payment: Some(payment_ref.clone()), - payment_preimage, - bolt12_invoice, - }), + move |payment| { + let payment_preimage = payment_preimage.map(|p| p.to_string()); + let bolt12_invoice = bolt12_invoice.as_ref().and_then(|invoice| { + invoice.bolt12_invoice().map(|i| i.encode().to_lower_hex_string()) + }); + event_envelope::Event::PaymentSuccessful(events::PaymentSuccessful { + payment_id: payment_id.to_string(), + payment: Some(payment), + payment_preimage, + bolt12_invoice, + }) + }, &event_node, &event_sender); @@ -550,9 +551,9 @@ fn main() { Event::PaymentFailed {payment_id, reason, ..} => { let proto_reason = reason.as_ref().map(payment_failure_reason_to_proto); send_payment_event(&payment_id, - move |payment_ref| event_envelope::Event::PaymentFailed(events::PaymentFailed { + move |payment| event_envelope::Event::PaymentFailed(events::PaymentFailed { payment_id: payment_id.to_string(), - payment: Some(payment_ref.clone()), + payment: Some(payment), reason: proto_reason.map(|r| r as i32), }), &event_node, @@ -562,10 +563,10 @@ fn main() { Event::PaymentClaimable { payment_id, custom_records, claim_deadline, claimable_amount_msat, .. } => { send_payment_event( &payment_id, - |payment_ref| { + |payment| { event_envelope::Event::PaymentClaimable( build_payment_claimable_proto( - payment_ref, + payment, &custom_records, claim_deadline, claimable_amount_msat, @@ -757,7 +758,7 @@ fn main() { } fn send_payment_event( - payment_id: &PaymentId, payment_to_event: impl FnOnce(&Payment) -> event_envelope::Event, + payment_id: &PaymentId, payment_to_event: impl FnOnce(Payment) -> event_envelope::Event, event_node: &Node, event_sender: &broadcast::Sender, ) { if event_sender.receiver_count() == 0 { @@ -772,7 +773,7 @@ fn send_payment_event( Ok(Some(payment_details)) => { let payment = payment_to_proto(payment_details); - let event = payment_to_event(&payment); + let event = payment_to_event(payment); if let Err(e) = event_sender.send(EventEnvelope { event: Some(event) }) { debug!("No event subscribers connected, skipping event: {e}"); } @@ -1004,14 +1005,14 @@ fn load_or_generate_api_key(storage_dir: &Path) -> std::io::Result { } fn build_payment_claimable_proto( - payment_ref: &Payment, custom_records: &[CustomTlvRecord], claim_deadline: Option, + payment: Payment, custom_records: &[CustomTlvRecord], claim_deadline: Option, claimable_amount_msat: u64, payment_id: String, ) -> events::PaymentClaimable { let proto_custom_records: Vec<_> = custom_records.iter().map(node_to_proto_custom_tlv).collect(); events::PaymentClaimable { payment_id, - payment: Some(payment_ref.clone()), + payment: Some(payment), custom_records: proto_custom_records, claim_deadline, claimable_amount_msat, @@ -1144,7 +1145,7 @@ mod tests { CustomTlvRecord { type_num: 65538, value: Vec::new() }, ]; let proto = build_payment_claimable_proto( - &payment, + payment, &records, Some(800_000), 42_123, diff --git a/ldk-server/src/util/metrics.rs b/ldk-server/src/util/metrics.rs index 13d2e212..a6e3d736 100644 --- a/ldk-server/src/util/metrics.rs +++ b/ldk-server/src/util/metrics.rs @@ -26,7 +26,7 @@ use std::sync::atomic::{AtomicI64, AtomicU64, Ordering}; use ldk_node::payment::PaymentStatus; -use ldk_node::{Node, NodeError}; +use ldk_node::{ChannelDetails, Node, NodeError}; use log::error; #[derive(Default)] @@ -123,20 +123,19 @@ impl Metrics { let all_channels = node.list_channels(); self.total_channels_count.store(all_channels.len() as i64, Ordering::Relaxed); - let public_channels_count = - all_channels.iter().filter(|channel_details| channel_details.is_announced).count() - as i64; - self.total_public_channels_count.store(public_channels_count, Ordering::Relaxed); - - let private_channels_count = - all_channels.iter().filter(|channel_details| !channel_details.is_announced).count() - as i64; - self.total_private_channels_count.store(private_channels_count, Ordering::Relaxed); + self.update_channel_visibility_counts(&all_channels); self.update_peer_count(node); self.update_all_balances(node); } + fn update_channel_visibility_counts(&self, channels: &[ChannelDetails]) { + let public_count = channels.iter().filter(|channel| channel.is_announced).count(); + let private_count = channels.len() - public_count; + self.total_public_channels_count.store(public_count as i64, Ordering::Relaxed); + self.total_private_channels_count.store(private_count as i64, Ordering::Relaxed); + } + pub fn update_all_balances(&self, node: &Node) { let all_balances = node.list_balances(); self.total_onchain_balance_sats @@ -160,15 +159,7 @@ impl Metrics { Err(e) => error!("Failed to update payment metrics: {e}"), } - let public_channels_count = - all_channels.iter().filter(|channel_details| channel_details.is_announced).count() - as i64; - self.total_public_channels_count.store(public_channels_count, Ordering::Relaxed); - - let private_channels_count = - all_channels.iter().filter(|channel_details| !channel_details.is_announced).count() - as i64; - self.total_private_channels_count.store(private_channels_count, Ordering::Relaxed); + self.update_channel_visibility_counts(&all_channels); self.update_peer_count(node); self.update_all_balances(node);