From 3e75b562a0d44f79e4d6802e83c2839c11cf1648 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Thu, 28 May 2026 13:11:34 -0400 Subject: [PATCH] =?UTF-8?q?Tighten=20zap-gated=20spec=20after=20a=20review?= =?UTF-8?q?=20pass=20=E2=80=94=208=20substantive=20gaps=20+=20polish?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical additions: 1. Relay fast-path. Zap receipts live on relays forever — querying for matching receipts on feed load means a cache-wiped user (or a new device) restores their unlocks for free. Also auto-detects a half-failed unlock that eventually settled. 2. NIP-47 capability discovery via kind-13194. We subscribe to the wallet's info event on connect and verify pay_invoice is supported before allowing the user to proceed. 3. Wallet-budget-exhausted handling. Special-case QUOTA_EXCEEDED / BUDGET_EXCEEDED so we say "raise your wallet's NWC budget" not a generic failure. Also surface this guidance in the CoinOS setup flow itself ("set a budget that comfortably covers the unlocks you expect"). 4. Preimage = proof of payment. Decouple unlock UX from kind-9735 receipt arrival. The preimage hashes to the bolt11's payment hash; that's all we need to mark unlocked locally. Receipts become async verification. 5. nwcLookupInvoice for double-spend protection. Before paying any leg on retry, check whether the wallet already settled it. Combined with PARTIAL_UNLOCK_KEY persistence (now storing bolt11 + paymentHash, not just receipt id), this means cross-session retries never double-charge. 6. LNURL minSendable checks. Block at publish time if the creator's LNURL won't accept their split share (with a corrective hint showing the minimum viable zap-min). Same defensive check on the platform side. 7. Parallelize the dual-zap. Sign both zap requests in Promise.all, fetch both bolt11s in parallel, then pay sequentially with creator first. Roughly halves the user-facing latency. 8. Partial-unlock persistence + banner. If the user closes the tab mid-unlock, on next visit a top-bar banner offers Resume or Discard for unfinished unlocks. Polish: - Phase 0 bumped 2-3h → 3-5h (noble subset inlining is genuinely more work than that estimate). - NWC URI parser now handles multiple relay params, URL-encoded secrets, optional lud16 param. - "Wallet alias from get_info" instead of misleading "walletDomain". - CoinOS deep link to coinos.io/settings/connect. - Mobile copy-paste note. - Three new threat-model rows: platform-pubkey exposure (intentional), budget-exhausted-mid-flow (precise retry prompt), and forged- receipt fast-path (signature verification). - Two new open-questions: pay-above-min unlocks (creator-only bonus), currency display. - Phase 4 ship gate: "documented in commit message" since we push straight to main, no PR workflow. Co-Authored-By: Claude Opus 4.7 --- docs/ZAP_GATED_PREMIUM_FEATURE.md | 200 ++++++++++++++++++++++++------ 1 file changed, 159 insertions(+), 41 deletions(-) diff --git a/docs/ZAP_GATED_PREMIUM_FEATURE.md b/docs/ZAP_GATED_PREMIUM_FEATURE.md index fb735b8..61774c3 100644 --- a/docs/ZAP_GATED_PREMIUM_FEATURE.md +++ b/docs/ZAP_GATED_PREMIUM_FEATURE.md @@ -43,7 +43,7 @@ There is no creator-only fallback path. The phases build on each other. Each has an explicit ship gate so work can be paused between phases for review. -### Phase 0 — Crypto bundle *(~2–3h)* +### Phase 0 — Crypto bundle *(~3–5h)* The existing codebase signs/encrypts only via `window.nostr.*` (the user's NIP-07 extension). NWC requires signing kind-23194 events with @@ -69,12 +69,22 @@ keys → produces the test-vector ciphertext. **Protocol summary (NIP-47)** The wallet exposes a connection URI: ``` -nostr+walletconnect://?relay=&secret= +nostr+walletconnect://?relay=&secret=[&lud16=] ``` -- `wallet-service-pubkey` — the wallet's NIP-47 service pubkey. -- `relay` — a dedicated relay both ends listen on. +- `wallet-service-pubkey` — the wallet's NIP-47 service pubkey (hex). +- `relay` — a dedicated relay both ends listen on. May appear MORE than + once; we publish/subscribe on all of them for redundancy. - `secret` — a 32-byte hex privkey that the client uses to sign - requests (this is NOT the user's identity key). + requests (this is NOT the user's identity key). May be URL-encoded. +- `lud16` — optional Lightning address some wallets (incl. CoinOS) + include for convenience; we cache it but don't depend on it. + +**Capability discovery (kind-13194)** +The wallet publishes a kind-13194 "info" event listing supported +methods (e.g. `pay_invoice get_balance get_info lookup_invoice`). +On connect we subscribe to that event and verify `pay_invoice` is in +the list — otherwise we surface "This wallet doesn't support +payments via NWC" before the user is allowed to proceed. **Request lifecycle** 1. Build a kind-23194 event: @@ -89,11 +99,18 @@ nostr+walletconnect://?relay=&secret= or `{ "error": { "code": "...", "message": "..." } }`. **Deliverables** -- `parseNwcUri(uri)` → `{ servicePubkey, relay, secret }` or null. -- `nwcConnect(uri)` — store creds, open WS, send a `get_info` ping to - verify the wallet responds. -- `nwcPayInvoice(bolt11, { timeoutMs, hintMsats })` → resolves to - `{ preimage }` or rejects with `{ code, message }`. +- `parseNwcUri(uri)` → `{ servicePubkey, relays[], secret, lud16? }` or + null. Handles multiple `relay` params and URL-encoded secrets. +- `nwcConnect(uri)` — store creds, open WS to each relay, fetch the + kind-13194 capability list, send a `get_info` request to retrieve + the wallet's alias + pubkey. +- `nwcPayInvoice(bolt11, { timeoutMs })` → resolves to + `{ preimage }` or rejects with `{ code, message }`. Common codes + worth special-casing: `INSUFFICIENT_BALANCE`, + `QUOTA_EXCEEDED` / `BUDGET_EXCEEDED` (wallet's per-connection cap + hit), `PAYMENT_FAILED`, `NOT_FOUND` (invoice mistyped), `INTERNAL`. +- `nwcLookupInvoice(paymentHash)` → returns settlement state. Used + during retry of a half-failed unlock so we never double-pay. - Credentials live in `localStorage` under `casewrap-nwc-connection` (single-record; new paste replaces old). Trade-off acknowledged in the threat-model section. @@ -116,24 +133,37 @@ A new "Wallet" section in the Community settings, two-state: ``` The CoinOS button opens a short modal: -> **1.** Visit [coinos.io](https://coinos.io) and sign up. -> **2.** Open *Settings → Wallet Connect*. -> **3.** Copy the connection string (`nostr+walletconnect://…`). -> **4.** Paste below. +> **1.** Sign up at [coinos.io](https://coinos.io) (no install). +> **2.** Open the [Wallet Connect](https://coinos.io/settings/connect) +> page directly, or browse to *Settings → Connect → Nostr Wallet +> Connect*. +> **3.** Set a budget that comfortably covers the unlocks you expect +> — at least a few thousand sats. *(Hint inline.)* +> **4.** Copy the connection string (`nostr+walletconnect://…`). +> **5.** Paste below. > *[input]* *[Connect]* The "Paste connection string" disclosure expands to the same input with no per-wallet instructions — works for Alby Hub, Mutiny, Phoenix, Cashu.me, Boltz, etc. +**Mobile note:** on a phone, the user copies the NWC URI from their +wallet app (Alby Go, Mutiny, Cashu.me's "Connect" share sheet, etc.) +and pastes into this same field. We don't try to detect / deep-link. + *Connected:* ``` ┌────────────────────────────────────────────┐ -│ ⚡ Connected to │ +│ ⚡ Connected to │ │ Last test: