Repository navigation
Description
Summary
Phase B of CEP-8 (#100) replaces the deterministic fakes with real Lightning rails behind the PaymentProcessor and PaymentHandler traits Phase A froze. No trait, middleware or registration-entry-point changes: a rail is a plug-in.
Two rails, in this order. NWC (NIP-47), both sides of the wire: a processor that issues a BOLT11 invoice (make_invoice) and verifies settlement (lookup_invoice polling or payment_received notifications), and a handler that pays (pay_invoice). NIP-57 zaps, server side only: an invoice from an LNURL-pay callback carrying a kind 9734 zap request, verified against a kind 9735 receipt. LNbits, the third rail listed in #100, is dropped.
Settled before planning:
- CI does not drive a real wallet. Relay-level (NWC) and HTTP-level (LNURL) mocks are the merge gate; ts-sdk gates nothing on a wallet either. A mock wallet service over
MockRelayPooldrives the real client code rather than stubbing it. Real-wallet tests ship#[ignore]d and environment-gated, run by hand before each rail merges. - No zap handler. NIP-57 obliges the payer only to pay a BOLT11 invoice, and the provider publishes the receipt. Both rails issue
pmi: bitcoin-lightning-bolt11and handlers are keyed by PMI, so a zap offer already routes to the NWC handler, as ts-sdk'szap-integration.test.tsdoes.
Key notes:
nostr0.44.4 shipsnips::nip47andnips::nip57behind flagsnostr-sdkalready forwards, so no NIP-47 crypto or zap-request construction is written here. Thenwccrate is not used: its relay pool is private, so nothing on top of it is testable without a live wallet.RelayPoolTraithas no unsubscribe, so ts-sdk's subscription-per-request would leak one live REQ per payment. Each rail client holds ONE subscription and correlates in process by request event id.- Wallet responses are parsed permissively, ts field for field, not through
nostr's strict response structs. On a money path a missing optional field must not turn a paid invoice into a failed verification. - Bounds are the rail's job:
create_payment_requiredruns in the middleware's unbounded, cancellation-blind initiation phase;verify_paymentmust observeparams.cancelor its poller outlives the timeout; the client engine giveshandle()neither a timeout nor a cancellation token. - Two safe-direction divergences from ts-sdk, invisible on the wire: NIP-47 notifications are decrypted by kind (23196 is NIP-04, 23197 is NIP-44), and notification-mode verification falls back to polling instead of throwing when no
payment_hashis cached. - Features off by default:
nwc = ["nostr-sdk/nip47"]adds no new crate;zaps = ["nostr-sdk/nip57", "dep:reqwest"]adds the only new dependency in Phase B, because a zap fetches its invoice over HTTP.
Spec: contextvm-docs/src/content/docs/reference/ceps/cep-8.md. It is rail-agnostic; the only normative rail text anywhere is CEP-21's line that pay_req is a BOLT11 invoice string.
TS SDK reference: sdk/src/payments/nip47/, nip57/, processors/, handlers/
Phase A: #100 (11 PRs, #101 to #108 and #111 to #114, all merged)
PR B1: NIP-47 client core + mock wallet (rail infrastructure)
-
Cargo.toml-nwc = ["nostr-sdk/nip47"], off by default, no new crate -
src/payments/nip47/uri.rs-parse_nwc_uri, accepting the pathname formNostrWalletConnectURI::parserejects and ts-sdk accepts -
src/payments/nip47/types.rs- permissive{result_type, error, result}envelope plus all-optional per-method results mirroring tsnip47/types.ts;sats_to_msatswithchecked_muland a non-positive-amount rejection -
src/payments/nip47/client.rs-NwcClientoverArc<dyn RelayPoolTrait>: one subscription, one reader task, waiter map keyed by request event id,response_timeoutper call,Laggedhandling, notification decode by kind,fetch_info_notification_typesfrom kind 13194 -
src/payments/nip47/mock_wallet.rsbehindtest-utils- scripted wallet over a linkedMockRelayPool(answer, error, silence, delay, malformed) plus a settle trigger publishingpayment_received - Tests: round trip for the three methods; uncorrelated and stranger-signed responses ignored; silence times out; a wallet error is not an
Ok; a minimal settled result and an unknown error code both parse; both notification kinds decode;Laggedloses nothing; concurrent requests correlate independently; URI parsing both forms
PR B2: NWC payment processor (server side of rail 1)
-
src/payments/processors/ln_bolt11_nwc.rs-LnBolt11NwcPaymentProcessorforbitcoin-lightning-bolt11 -
create_payment_required-make_invoicein msats withexpiry, invoice topayment_hashLRU, configuredttl(the wallet'sexpires_atis ignored, ts parity), bounded byresponse_timeout -
verify_payment- in-flight dedup bypay_req(shared future overArc<Result<..>>, sincePaymentErroris notClone); ts backoff schedule floored atpoll_intervalwith jitter;NOT_FOUNDpending, other wallet errors fatal;expiredandfailedfatal; settled onstate == settledorsettled_at > 0; receipt ispayment_hashor ts'ssettled:<event_id>[:<settled_at>]fallback - Notification mode - lazy
OnceCellauto-detection from the info event, polling fallback when nopayment_hashis cached - Cancellation -
select!onparams.cancelin every wait; a cancelled verify returnsErr, never an emptyOk -
LnBolt11NwcPaymentProcessorOptions, non-exhaustive, ts defaults (300 s ttl, 1500 ms poll, 60 s timeout, 5000 in-flight, 10000 hash cache), injectable pool and client - Tests: the four ts unit tests ported; cancelled verify stops the poller; a hung wallet fails
create_payment_requiredinsideresponse_timeout; terminal states fatal andNOT_FOUNDnot; the fallback receipt; notification mode polls instead of erroring; overflow and non-positive amounts rejected before any wallet call; no log line contains the invoice - Mutation-test the money paths (cancellation arms, settlement predicate,
NOT_FOUNDbranch, dedup claim)
PR B3: NWC payment handler + wiring + docs + e2e (rail 1 capstone)
-
src/payments/handlers/ln_bolt11_nwc.rs-LnBolt11NwcPaymentHandler,pay_invoicebounded by its ownresponse_timeout; the spending decision stays withpayment_policy -
tests/payments_nwc_e2e.rs- real client and server transports over oneMockRelayPoolgroup, a mock wallet on each side, both lifecycles -
tests/payments_nwc_real_wallet.rs-#[ignore]d, environment-gated, mirroring tsnwc-integration.test.tsandnwc-paid-capability-e2e.test.ts -
docs/payments.md- the shipped NWC rail, its options, and its operational notes (a late settlement is money out with no execution; the wallet is a trust boundary; rate limiting is still upstream's job); correct the paragraph listing LNbits as planned -
README.md,Cargo.tomltest entries,CHANGELOG.md - Tests: both lifecycles end to end including
-32043while verifying; a decliningpayment_policystops the wallet call and synthesizes-32000; a never-settling wallet ends at the TTL with no acceptance; apay_invoiceerror leaves the request pending and synthesizes nothing; the handler's own timeout bounds a silent wallet - Real-wallet run executed and recorded in the PR, invoice redacted
PR B4: LNURL-pay client + zap event helpers (zap infrastructure)
-
Cargo.toml-zaps = ["nostr-sdk/nip57", "dep:reqwest"];reqwestoptional,default-features = false, features = ["rustls-tls", "json"](rustls is already in the tree; no OpenSSL). MSRV 1.88 verified across its tree as a gate item -
src/payments/nip57/lnurl.rs-parse_ln_address(LUD-16) and an injectableLnurlPayClienttrait so the rail is testable without HTTP, with a reqwest-backed default that is https-only, timed out, body-capped and does not follow cross-scheme redirects (the callback URL is remote-controlled input) -
src/payments/nip57/zap.rs- kind 9734 zap request viaZapRequestDataplusanonymous_zap_request, andbolt11_from_zap_receipt -
src/payments/nip57/mock_lnurl.rsbehindtest-utils- scripted pay params, invoice, error,allowsNostr: false, out-of-range amounts - Tests: LUD-16 parsing including ts's four rejections; the well-known URL built as ts builds it; the callback carries
amountandnostrand returnspr; areason-only response errors with the reason; non-https and oversized bodies rejected; zap request tags in ts's order; receiptbolt11extraction
PR B5: Zap payment processor + wiring + docs + e2e (rail 2 capstone)
-
src/payments/processors/ln_bolt11_zap.rs-LnBolt11ZapPaymentProcessorforbitcoin-lightning-bolt11 -
create_payment_required- fetch pay params, reject an endpoint without zap support, enforceminSendableandmaxSendable, sign the zap request with the processor's ephemeral key, request the invoice, record{expected_zapper_pubkey, since, amount_msats}keyed by the invoice, return_meta: {"rail": "nip57"} -
verify_payment- one subscription for kind 9735 from the expected zapper since the recorded timestamp, resolve on abolt11tag equal topay_req, return_meta: {"zap_receipt_event_id": ..}; in-flight dedup bypay_req; stateless fallback re-fetches pay params; cancellation and HTTP bounds as in B2 -
tests/payments_zap_e2e.rsandtests/payments_zap_real_wallet.rs(the real-wallet zap test pays with the NWC handler) -
docs/payments.md- the zap rail, its options, and three non-obvious facts: the provider publishes the receipt to the relays named in the zap request, so the configured relay set must be one it will publish to; the provider'snostrPubkeyauthorizes receipts, so it is a trust boundary; there is no client-side zap handler, and why -
README.md,Cargo.tomltest entries,CHANGELOG.md - Tests: issue and verify with the receipt published onto the mock relay; a receipt for another invoice or from another pubkey does not settle ours; out-of-range amounts fail before the callback;
allowsNostr: falsefails with ts's message; cancellation returnsErr; the stateless fallback verifies; full lifecycle with the NWC handler paying the zap invoice - Real-wallet run executed and recorded in the PR, invoice redacted
Exit criteria
- Both rails shipped and feature-gated;
--all-featuresand--no-default-featuresgreen, MSRV 1.88 -
docs/payments.mddocuments both rails and no longer describes Phase B as deferred - Both real-wallet test pairs executed and recorded
-
CHANGELOG.mdcarries a rails entry per PR
Out of scope: LNbits (listed in #100, deliberately dropped); any third rail; a client-side zap handler; changes to the Phase A traits, middlewares or entry points; multi-PMI payment_required fan-out and direct_payment / change, which stay out for ts-sdk parity as in #100.
Not Phase B's, but load-bearing once real money moves: rate limiting. Every successful offer still spawns one unbounded detached verification task, and with a real rail each holds a wallet connection and polls a wallet. max_pending_payments and the authorization store caps bound state, not tasks. Worth its own issue.
Activity
nisargpatel7042lva commentedon Oct 1, 2026
@harsh04044 I have completed PR B1. Could you please review it to check if everything is in order? Let me know if anything needs to be added or changed. In the meantime, I am starting work on PR B2.
fix(payments): harden the NIP-47 client against wallet-facing failure
feat(payments): NWC payment processor, the server side of the NWC rail
feat(payments): NWC payment handler, e2e suite and docs, completing t…
feat(payments): NWC payment processor, the server side of the NWC rail
feat(payments): NWC payment handler, e2e suite and docs, completing t…
feat(payments): NWC payment processor, the server side of the NWC rail
feat(payments): NWC payment handler, e2e suite and docs, completing t…
feat(payments): NWC payment processor, the server side of the NWC rail
feat(payments): NWC payment handler, e2e suite and docs, completing t…
feat(payments): NWC payment processor, the server side of the NWC rail
feat(payments): NWC payment handler, e2e suite and docs, completing t…
Ishaan-0706 commentedon Nov 2, 2026
is the prb1 work done?
@nisargpatel7042lva
nisargpatel7042lva commentedon Nov 3, 2026
Yes, B1 is done from my side. Everything from your Oct 8 and Oct 15 reviews is addressed in 0ef9c44, bf61f0b, 51160ab, 69212f4 and 0f3490e, including the five Oct 15 blockers.
Two things I need from you:
Ishaan-0706 commentedon Nov 4, 2026
@nisargpatel7042lva can i work on the B3 pr?
nisargpatel7042lva commentedon Nov 5, 2026
I have been working on the B3, but feel free to give it a shot at it
nisargpatel7042lva commentedon Nov 12, 2026
@Ishaan-0706 small correction to what I said above, since I do not want you duplicating work: B3 is already written and up as #119. All three sub-PRs are open and pushed.
Everything from the Oct 8 and Oct 15 reviews is in them, including the five blockers from the second review. So there is nothing in B1 to B3 that needs starting from scratch.
What is genuinely unclaimed is the second rail, B4 and B5, the NIP-57 zap side. Both are specced out in the checklist above and nobody has started either. B4 is the LNURL-pay client plus the zap event helpers, B5 is the processor and the capstone. If you want a piece of this, that is where the work actually is, and I am happy to answer anything about the shared surface since B5's processor sits behind the same
PaymentProcessortrait B2 uses.One thing to save you a mistake I made. The issue text above says
nwc = ["nostr-sdk/nip47"]"adds no new crate". That turned out to be wrong:cargo treewith and without the feature is 178 crates against 181, becausenip47pulls in NIP-04 and NIP-04 is AES-CBC, so it addsaes,cbcandcipher. I had repeated that claim in six places before it was caught in review. Worth measuring rather than assuming when you write B4's, sincezapsaddsreqwestand that one is a much bigger subtree.@harsh04044 still blocked on one thing. The workflow runs on all three PRs need approving. It has also changed since I last asked: the Oct 15 and Oct 16 pushes at least queued runs sitting at
action_required, but my Oct 23 push produced no check suites at all, so I think fork-PR runs may have stopped being queued on the repo. I cannot seeactions/permissionswith pull access, so I cannot tell from here. Everything I have reported green is my own machine on rustc 1.99.0, the same version the job uses, and it would be better to have the runners say it.