From 793375a4fb7e2ea5c8092a2a3dd6fcd41215320b Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sun, 5 Oct 2025 20:54:04 -0400 Subject: [PATCH] Document NWC implementation coverage --- docs/nwc-compliance.md | 53 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 docs/nwc-compliance.md diff --git a/docs/nwc-compliance.md b/docs/nwc-compliance.md new file mode 100644 index 00000000..fc0f0512 --- /dev/null +++ b/docs/nwc-compliance.md @@ -0,0 +1,53 @@ +# Nostr Wallet Connect Coverage + +This note cross-references Bitvid's current Nostr Wallet Connect (NIP-47) implementation with the official +specification so we can quickly spot regressions or missing features. + +## URI handling + +* `parseNwcUri()` accepts all aliases we see in the wild (`nostr+walletconnect://`, `walletconnect://`, and `nwc://`) + and rejects unknown schemes before continuing. It extracts the wallet pubkey, all relay query parameters, and the + 32-byte secret required for signing. The helper lowercases the secret, reserializes the URI in canonical form, and + derives the client public key from the secret so downstream calls can sign events without leaking user keys. + +## Connection lifecycle + +* `ensureActiveState()` caches the parsed connection and keeps WebSocket state so repeated actions reuse the same + subscription. When a new URI is supplied we tear down the previous socket, update the relay list, and reset + encryption state before reconnecting. +* `connectSocket()` opens a WebSocket to the relay, subscribes to kind `23195` responses authored by the wallet and + addressed to the client pubkey, and hooks message/error/close callbacks so we can retry or surface failures. + +## Request flow + +* `ensureWallet()` validates that the user configured an NWC URI, connects to the relay if needed, and negotiates an + encryption scheme before returning the active context. +* `encryptRequestPayload()` serializes the JSON-RPC payload, encrypts it with the selected scheme, and creates the + Nostr event with kind `23194`, `pubkey` set to the client key, a `p` tag pointing at the wallet, and an + `encryption` tag mirroring the algorithm that was used. Events are signed with the secret from the connection URI + so relays will accept them. +* `sendPayment()` constructs the `pay_invoice` request, attaches a unique `id`, and dispatches it over the relay. + +## Response flow + +* `subscribeToResponses()` issues a `REQ` for kind `23195` events authored by the wallet and tagged for the client. +* `handleSocketMessage()` decrypts responses, matches them against pending `id`s (or the `e` tag), clears pending + timeouts, and surfaces either the `result` or `error.code`/`error.message` back to callers. + +## Encryption negotiation + +* `requestInfoEvent()` fetches the wallet's replaceable kind `13194` info note and passes the result to + `getWalletSupportedEncryption()` so we only pick schemes advertised by the wallet service. +* `getEncryptionCandidates()` prefers NIP-44 (`nip44_v2`) when both sides support it and falls back to NIP-04 when + necessary. We also remember schemes that failed so future retries skip them. + +## Current gaps + +* We only connect to the first relay supplied by the URI. If a wallet publishes multiple relays we should consider + retrying additional ones when the primary is offline. +* The client currently implements the `pay_invoice` command. Other optional commands (`make_invoice`, + `get_balance`, `multi_pay_invoice`, etc.) and notification handling (kinds `23196`/`23197`) would require follow-up + work. +* Wallet responses that signal `UNSUPPORTED_ENCRYPTION` as an error today bubble straight back to the caller. The + transport already retries once when we encounter that error during submission, but we could add richer UX (e.g. + prompting the user to reconnect) in the future.