mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-09 08:04:45 +00:00
docs(bolt12): scope NWC BOLT12 pay/receive (nwc#2) integration
Captures the design for paying BOLT12 offers over NIP-47 and — via the pay result's payer_proof — sending real kind:9736 zaps. Maps the reusable NIP-47 plumbing, breaks the work into phases, and flags the linchpin risk: nwc#2 does not guarantee payer_note lands in the BOLT12 invreq_payer_note that NIP-2421's zap binding depends on. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SpgpWLKzgD7vS9Fs4CXTR3
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
# NWC BOLT12 payments (nostr-wallet-connect/nwc#2)
|
||||
|
||||
Status: **scoping** — no code yet. Depends on NIP-2421 (this branch) and the
|
||||
unmerged `nostr-wallet-connect/nwc#2` (adds `pay`/`receive` to NIP-47).
|
||||
|
||||
## What nwc#2 adds
|
||||
|
||||
Two generalized methods replace the bolt11-only `pay_invoice`:
|
||||
|
||||
- **`pay`** — params `{ payment: "bitcoin:?lno=lno1…", amount?, payer_note?, metadata? }`.
|
||||
`payment` is a BIP321 URI (so `bitcoin:?lno=` — exactly what
|
||||
`payViaBolt12Intent` already builds — or `lightning=`/on-chain). `amount`
|
||||
(msats) is required only when the instruction has no amount. Result:
|
||||
`{ transaction_id, state, instruction_type, amount, fees_paid, payment_hash,
|
||||
preimage, payer_proof: "lnp1…", txid, failure_reason, created_at, settled_at }`.
|
||||
- **`receive`** — params `{ amount?, description?, metadata? }` → result
|
||||
`{ bip321: "bitcoin:?lightning=lnbc…&lno=lno1…", transaction_id }`. Lets a
|
||||
wallet mint our own unified offer; ties to the kind-10058 editor (auto-fill
|
||||
offers) — later phase.
|
||||
|
||||
New errors: `UNSUPPORTED_PAYMENT_INSTRUCTION`, `UNSUPPORTED_NETWORK`. Wallets
|
||||
advertise support in their kind-13194 info event + `get_info.methods`.
|
||||
|
||||
## Two capabilities this unlocks
|
||||
|
||||
**A. In-app payment of an offer** — the "extra payment instruction" half. Today
|
||||
`Bolt12PayButton` fires a `bitcoin:?lno=` intent to an external wallet. With `pay`
|
||||
we can settle the offer over the user's already-configured NWC connection, no app
|
||||
switch. No Nostr receipt.
|
||||
|
||||
**B. Sending real BOLT12 zaps** — the half deferred since the start of the branch.
|
||||
The `pay` result carries **`payer_proof: "lnp1…"`**, which is precisely the input
|
||||
`Bolt12ZapEvent.build(signedIntent, payerProof, payerPubKey)` needs. So NWC `pay`
|
||||
is the payment rail that produces the proof for a kind-9736 zap. This is the
|
||||
strategic reason to do this now.
|
||||
|
||||
## Existing NIP-47 infra we reuse (from the code map)
|
||||
|
||||
The send/await/correlate plumbing is **method-agnostic** — it keys on request id
|
||||
and dispatches the decrypted `Response`, so a new method needs no changes there:
|
||||
|
||||
- `NwcSignerState.sendNwcRequestToWallet(uri, request, onResponse)`
|
||||
(`amethyst/…/model/nip47WalletConnect/NwcSignerState.kt`) — builds the 23194,
|
||||
synchronous REQ-before-EVENT via `NWCPaymentFilterAssembler`, 60 s timeout,
|
||||
decrypt-on-arrival.
|
||||
- `NwcPaymentTracker` (`commons/…/service/nwc/`) — request↔response match with the
|
||||
author-spoof gate.
|
||||
- `LocalCache.consume(LnZapPaymentResponseEvent)` — routes 23195 back to the callback.
|
||||
- Wallet storage: `AccountSettings.nwcWallets` + `defaultPaymentSourceId`;
|
||||
`PaymentSourceResolver`.
|
||||
- Pay rail entry: `ZapPaymentHandler.zap()` → `payViaNWC()` (the `PaymentSource.Nwc`
|
||||
branch).
|
||||
|
||||
Capability discovery exists (`NwcInfoEvent.supportsMethod`, `GetInfoResult.methods`)
|
||||
but is **not** wired into the pay path — we'd add the gate ourselves.
|
||||
|
||||
## Work breakdown
|
||||
|
||||
### Phase 0 — quartz protocol (`nip47WalletConnect`)
|
||||
- `NwcMethod.PAY = "pay"`, `NwcMethod.RECEIVE = "receive"`.
|
||||
- `rpc/Request.kt`: `PayMethod` + `PayParams(payment, amount, payerNote, metadata)`
|
||||
with `create(…)`; `ReceiveMethod`/`ReceiveParams`.
|
||||
- `rpc/Response.kt`: `PaySuccessResponse` (all result fields above, `payerProof`
|
||||
nullable) and `ReceiveSuccessResponse(bip321, transactionId)`.
|
||||
- `rpc/NwcErrorCode.kt`: add the two new codes.
|
||||
- Serializer branches in **both** `Nip47RequestKSerializer` / `Nip47ResponseKSerializer`
|
||||
**and** the jvmAndroid Jackson variants.
|
||||
- Optional `Nip47Client.pay(...)` builder.
|
||||
- Tests: request/response round-trip; a real captured `pay` result fixture.
|
||||
- No new third-party deps (pure protocol) → licensing clean.
|
||||
|
||||
### Phase 1 — in-app offer payment (capability A)
|
||||
- `Account.sendNwcPayRequest(payment, amount, payerNote, onResponse)` wrapper
|
||||
(mirrors `sendZapPaymentRequestFor`, reuses `NwcSignerState`).
|
||||
- In `Bolt12PayButton`'s dialog: when an NWC wallet is configured, add a "Pay with
|
||||
connected wallet" action (amount-entry sheet, since offers are often amountless)
|
||||
→ `pay` with `payment=bitcoin:?lno=<offer>`. Keep the external-intent path as
|
||||
fallback.
|
||||
- Surface `UNSUPPORTED_PAYMENT_INSTRUCTION`/`PAYMENT_FAILED`.
|
||||
|
||||
### Phase 2 — send BOLT12 zaps (capability B)
|
||||
- New `Bolt12ZapSender` (or extend `ZapPaymentHandler`): when the recipient has a
|
||||
kind-10058 offer and the wallet supports `pay`, offer a BOLT12 zap.
|
||||
1. Build + sign kind-9737 intent (amount, offer, p, e/a/k, zap_id, content).
|
||||
2. `pay` with `payment=bitcoin:?lno=<offer>`, `amount`,
|
||||
`payer_note="nostr:nipXX:<intent-id>"`.
|
||||
3. On success with a `payer_proof`, `Bolt12ZapEvent.build(intent, payerProof,
|
||||
payerPubKey = own | null for anon)`, sign, publish to the recipient's inbox
|
||||
relays.
|
||||
4. Our own `LocalCache` consumes the 9736 and counts it.
|
||||
- Anonymous vs attributed (`P` tag) toggle, mirroring lightning-zap anonymity.
|
||||
|
||||
### Phase 3 — gating + receive (later)
|
||||
- Read `NwcInfoEvent.supportsMethod("pay")` / `get_info.methods` to show the NWC
|
||||
BOLT12 options only when supported; else fall back to the intent.
|
||||
- `receive` to mint the user's own offer and pre-fill the kind-10058 editor.
|
||||
|
||||
## Risks / open questions (decide before Phase 2)
|
||||
|
||||
1. **`payer_note` → `invreq_payer_note` is NOT guaranteed by nwc#2.** The spec only
|
||||
says "if `payer_note` is not empty, the selected instruction MUST support
|
||||
payer-provided messages" — it never states the note lands in the BOLT12
|
||||
`invreq_payer_note`. NIP-2421 binds the zap to the intent through exactly that
|
||||
field (`invreq_payer_note == nostr:nipXX:<intent-id>`). If a wallet routes
|
||||
`payer_note` elsewhere, the returned `payer_proof` fails our validator and the
|
||||
9736 is worthless. **Phase 2 feasibility hinges on this** — needs confirmation
|
||||
in the nwc thread / a reference wallet, or a follow-up to nwc#2 to nail it down.
|
||||
2. **`payer_proof` is best-effort** (`"optional if unavailable"`, no wallet mandate).
|
||||
A wallet may settle the offer and return no proof → payment succeeds but we
|
||||
can't publish a zap. Phase 1 is unaffected; Phase 2 must degrade gracefully
|
||||
("paid, but no zap receipt available").
|
||||
3. **Our verifier can't check compressed proofs yet** (see
|
||||
`quartz/plans/2026-07-23-bolt12-zap-interop-vectors.md`). Real wallet proofs are
|
||||
compressed, so a zap we send may show locally as unverified. Publishing is fine;
|
||||
local counting waits on the merkle-reconstruction work.
|
||||
4. **Maturity.** Both nwc#2 and NIP-2421 are unmerged; few/no wallets implement
|
||||
`pay` today. Gate hard on capability (Phase 3) and keep the intent fallback.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Phase 0 is safe, self-contained, and unblocks everything — do it regardless.
|
||||
Phase 1 (in-app pay) is low-risk and immediately useful. **Hold Phase 2 until
|
||||
risk #1 is resolved** — it's the high-value piece (real zap sending) but its
|
||||
correctness depends on a binding the current nwc#2 text doesn't guarantee.
|
||||
Reference in New Issue
Block a user