# Theseus integrated BCH wallet — design Companion to [`ROADMAP-identity-wallet.md`](ROADMAP-identity-wallet.md) Strand B and [`DESIGN-password-manager.md`](DESIGN-password-manager.md) (same crypto discipline, distinct purpose subtree). Same shape as the password design doc: threat model → permission model → API → UI → sequencing → open questions. The reference point is deliberate: **MetaMask in Brave.** A wallet the user already trusts, injected as `window.bcnr` into every page, permissioned per origin, and additionally exposed to *outside* dApps via WizardConnect so a mobile-only site can pair Theseus like a hardware wallet. Everything below is chipnet only until explicitly directed otherwise. --- ## 0. Scope **In:** - `window.bcnr` provider injected on every page (read-only immediately; permissioned methods gated per origin). - The user's wallet lives in Theseus. No external key material. - Modal-confirmed operations: `signMessage`, `sendPayment`, `registerName`, `updateName`, `transferName`. - WizardConnect **responder**: dApps outside Theseus (or inside it) can pair via `wiz://` URI + QR to sign transactions with the Theseus wallet. - Settings → Wallet section: connected sites, per-site/per-method revoke, transaction history. - Reuse of the already-shipped `BuiltInWallet` ([Argus/src/lib/wallet-web.js:BuiltInWallet](../Argus/src/lib/wallet-web.js)) as the signing core. - Reuse of the already-shipped registration/update flow ([Argus/src/lib/register-tx.js](../Argus/src/lib/register-tx.js) + [Argus/src/lib/registrar.js](../Argus/src/lib/registrar.js)). **Out (deliberate, deferred, or handled elsewhere):** - Ethereum / Solana / other-chain. BCH only in this strand. - Hardware-wallet abstraction (Ledger/Trezor). Phase 4 conversation. - WalletConnect v2. Explicitly dropped 2026-07-27 in favour of WizardConnect (see [`Decentralized.DNS/REGISTRATION-STATUS.md`](../Decentralized.DNS/REGISTRATION-STATUS.md)). We do not want a Reown project ID as a dependency. - CashConnect V0. Cannot mint name certificates by design ([`REGISTRATION-STATUS.md`](../Decentralized.DNS/REGISTRATION-STATUS.md) § *CashConnect cannot mint*). - Nostr messenger (NIP-07 client for arbitrary Nostr apps). Separate purpose subtree, Strand B.5 in the roadmap, not this design. - Multi-account UX. One account per Theseus profile in v1. --- ## 1. Threat model The provider becomes malware infrastructure the moment its permission model fails. Three attack surfaces to lock down: ### 1.1 Malicious page reading the provider surface Every loaded page sees `window.bcnr`. It must not be possible to enumerate addresses, balances, or names without an explicit user grant. - **Read-only methods** (`resolveName`, `getBcnrTlds`, `isRegistered`) are chain queries — they leak nothing about the user. No permission. - **All identifying methods** (`getAccounts`, `signMessage`, `sendPayment`, `registerName`, `updateName`, `transferName`) require an origin permission that started with a modal. - **`getAccounts` returns `[]` for un-granted origins**, not a rejection. Rejection is a signal; empty is opaque. ### 1.2 Compromised page after permission granted An origin can be captured (XSS, subdomain takeover, malicious ad on a trusted host) after the user granted a permission. Assume this. - **Per-method scope**, not just per-origin. Granting "sign message" does not grant "send payment". A page has to earn each modal separately. - **`sendPayment` has a per-approval amount cap.** Approving `example.bch` for payments does not approve unlimited spending — the modal shows an amount, and a bigger amount re-prompts. "Approve unlimited" is not a UI option. - **Every mint / update / transfer is modal-confirmed every time.** On-chain writes are irrevocable; there is no "trust for the session" mode. ### 1.3 Origin confusion (phishing, homoglyph, subdomain) - Origin is bound to the **eTLD+1 via the public suffix list snapshot** shipped with Theseus (same rule as password autofill, A.2 in the roadmap). No runtime fetch, no third-party dependency. - **BCNR names get a distinct origin tier** in the confirmation modal: `alice.bch` is a first-class origin (backed by an on-chain certificate), `alice.com` is a second-class origin (registrar-leased). The modal shows the distinction so the user knows what they're trusting. - **The recipient's cash address is shown verbatim in the confirmation.** If the address changes between two confirmations from the same origin during one session, a red warning banner appears — "the address this site is asking you to sign for has changed since your last approval." ### 1.4 What we are NOT defending against - A user manually copying their recovery phrase into a phishing site. No wallet can defend against this and pretending otherwise misleads users. - A user who unlocks the wallet on a shared machine and walks away. Auto-lock helps but is not a substitute. - Compromise of Theseus itself (RCE, malicious extension, keylogger). The password vault and the wallet share the same threat model here — game over. --- ## 2. Permission model ### 2.1 Per-origin, per-method, per-limit Stored in `/wallet-permissions.json`: ```json { "silentmode.bch": { "grantedAt": 1728000000000, "methods": { "signMessage": { "grants": 12 }, "sendPayment": { "capSats": 100000, "used": 45000, "grants": 3 }, "registerName": { "grants": 1 } } } } ``` - `capSats` is the **per-approval** cap the site was granted, not a cumulative budget. A payment above it re-prompts with a new modal. - `used` and `grants` are for the Settings audit trail. The user sees "this site has spent X and signed Y messages" — accountability. - **No wildcard origins.** Every subdomain is a separate grant. ### 2.2 First-use flow 1. Page calls `bcnr.requestAccount()` (or any permissioned method). 2. Theseus renders a **connect modal** as a `WebContentsView` overlay (same pattern as popover, engine-picker, downloads). 3. Modal shows: - Origin, with the BCNR-vs-ICANN badge. - What is being asked (specific method, specific parameters). - "Connect to this site with account `bchtest:qxxx…yyy`" (the wallet's receive address 0, truncated with copy-full option). - **Two buttons**: `Approve once` (single-shot, no permission stored) and `Approve for this site` (grant persisted). 4. On approve, the permission is written and the promise resolves. 5. On reject, the promise resolves with the same "empty result" pattern as an un-granted origin (no distinguishing signal to the page). ### 2.3 Revocation Settings > Wallet > Connected sites. Table: `origin | granted | last used | scopes | [revoke]`. Per-scope revoke checkboxes, plus a nuclear "revoke all". Revocation clears `wallet-permissions.json` and takes effect on the origin's next call (no restart needed). ### 2.4 What the provider CAN'T do - Read the vault, seed, or any other purpose subtree. - Sign anything without a modal. - Enumerate connected sites (that's a settings-scope API, not page-scope). - Watch the wallet balance across origins (each origin sees only its own granted accounts — usually one). --- ## 3. `window.bcnr` provider — the API Injected by a preload extension registered via `session.defaultSession.setPreloads(...)` so it is present on every page. The preload uses `contextBridge.exposeInMainWorld("bcnr", …)` and forwards to main via IPC on a dedicated `bcnr:*` channel. ### 3.1 Read-only (no permission) ```ts bcnr.resolveName(name: string): Promise bcnr.isRegistered(name: string): Promise bcnr.getBcnrTlds(): Promise // ['bch','p2p','nav',...] bcnr.getRecordVersion(name: string): Promise // for cache invalidation ``` ### 3.2 Identity (permissioned; first call → modal) ```ts bcnr.requestAccount(): Promise // returns cashaddr or null bcnr.getAccounts(): Promise // [] if not granted bcnr.signMessage({ message: string, address?: string, userPrompt?: string }): Promise<{ signature: string, address: string }> // BIP-137 ``` ### 3.3 Payments (permissioned; per-amount modal) ```ts bcnr.sendPayment({ to: string, // cashaddr OR BCNR name resolvable to an `a` record amountSats: bigint, memo?: string, userPrompt?: string, }): Promise<{ txid: string }> ``` ### 3.4 Name operations (each modal-confirmed) ```ts bcnr.registerName({ name: string, records?: Record, userPrompt?: string }): Promise<{ txid: string, category: string }> bcnr.updateName({ name: string, records: Record, userPrompt?: string }): Promise<{ txid: string }> bcnr.transferName({ name: string, toAddress: string, userPrompt?: string }): Promise<{ txid: string }> ``` ### 3.5 Events ```ts bcnr.on('accountsChanged', (accounts: string[]) => void) bcnr.on('permissionsRevoked', (methods: string[]) => void) bcnr.on('lock', () => void) // wallet auto-locked ``` ### 3.6 Match with WizardConnect / WC2 shape where cheap `signMessage` returns `{signature, address}` — same shape as `bch_signMessage` in the wc2-bch-bcr namespace (see [Parameters/WalletConnect/WalletConnect_integration_reference.md](../../Deviant/Parameters/WalletConnect/WalletConnect_integration_reference.md) §4.2). Pages that already speak WC2 can adapt with a two-line shim. We do NOT re-expose the WC2 wire format (`stringify` with `` markers) at the provider layer — that is a WC2 transport concern, not a provider API concern. The provider takes plain values; the WizardConnect responder (below) is where the wire format matters. --- ## 4. WizardConnect responder The MetaMask analogue is "the wallet is here in the browser." The extra thing we get from WizardConnect is "a dApp on another device — a mobile site, a desktop app — can pair with the Theseus wallet by scanning a `wiz://` QR." This is exactly the responder side of what Deviant is finishing ([`Parameters/WalletConnect/WIZARDCONNECT-INTEGRATION-GUIDE.md`](../../Deviant/Parameters/WalletConnect/WIZARDCONNECT-INTEGRATION-GUIDE.md)), adapted to Theseus. ### 4.1 Wallet-side pieces Same shape as Deviant: - `@wizardconnect/wallet@0.2.2` for the SDK; already tested against the 0.2.4-core wire protocol. `nostr-tools` and `ws` are already in Theseus's deps (verified 2026-08-13). - A **`WalletAdapter`** wrapping Theseus's `BuiltInWallet`. Theseus's wallet is sync-in-main-process behind the unlock modal, which is simpler than Deviant's async biometric-KeyStore case: - `walletName: "Theseus"`, `walletIcon: ` - `getRelayPrivateKey(uri): Uint8Array` = `sha256(sha256(utf8(uri)))` (identical to Deviant's implementation — deterministic per URI so a reconnect keeps the same Nostr identity, different dApps cannot correlate; matches `@wizardconnect/wallet@0.2.2` types) - `getXpub(path): string` — reads pre-cached xpubs from `BuiltInWallet` - `getPublicKey(path, index)` — throws; the shipped SDK never calls it - `signTransaction(request): Promise<{ signedTransaction: string }>` — unwraps the `WcSignTransactionRequest` (the bug Deviant hit and fixed — see the integration guide §4), renders the same signing modal the `window.bcnr` provider uses, and signs via `coSignTransaction`-equivalent logic in `wallet-web.js` - Signing uses **SIGHASH_ALL | FORKID | UTXOS (0x61)**. Normative in the WizardConnect spec; Theseus's signer must accept this flag alongside 0x41. See Deviant `signer.ts:51` `WIZARDCONNECT_SIGHASH` for the reference. ### 4.2 Pairing UX - Address-bar chip when a `wiz://` URI is present on a page (paste-detect). - Settings > Wallet > "Pair with a dApp" — QR scanner (uses the camera if available; otherwise paste), takes a `wiz://` URI, opens the wallet-side connection. - **Sign-request modal is the SAME modal** as the in-Theseus `bcnr` provider. A dApp asking for a signature via `window.bcnr` from a loaded page and a dApp asking via WizardConnect from a paired external site both hit the same confirmation surface. This is the property that makes the responder feel "already part of the browser" rather than a second wallet UX. ### 4.3 Session persistence - Reuse `@wizardconnect/dapp`'s `session` mechanism on the wallet side: the Nostr identity per URI is deterministic, so a reconnect after a Theseus restart is transparent. - Persist active pairings in `/wallet-wz-sessions.json`. - Settings > Wallet > Paired dApps: list, revoke, "disconnect all". ### 4.4 The distinction to keep visible in code **In-Theseus dApp using `window.bcnr` = local IPC.** No relay, no Nostr, no transport. Fast and private. **External dApp using WizardConnect = Nostr relay.** Encrypted end-to-end (NIP-17 gift-wrap), but the relay sees timing and message sizes. Users should know they picked the second one when they scanned a QR. The wallet doesn't need to distinguish for *signing purposes* — same modal, same signer, same permission model — but the address-bar chip and the settings page should visibly separate `Connected sites` from `Paired dApps`. --- ## 5. UI surfaces New WebContentsView overlays, each needing a preload extension registered and listed in `TheseusNavigator/package.json` `build.files` (per `GOTCHAS.md`): | Overlay | Preload | Purpose | |---|---|---| | `wallet-connect-modal.html` | `wallet-connect-preload.js` | first-use connect prompt | | `wallet-sign-modal.html` | `wallet-sign-preload.js` | signature/payment/name-op confirmation | | `wallet-pair-modal.html` | `wallet-pair-preload.js` | WizardConnect QR scan / URI paste | The **address-bar chip** grows an icon when: - The current origin has an active permission (green key) - The current page contains a `wiz://` URI (purple wand) - The wallet is locked and the page is asking for something (grey with a lock) Click the chip → shows a summary: which origin, which methods granted, spend so far, revoke options. **Settings > Wallet** — a new pane alongside Passwords: - Account row: address, balance, "backup phrase" (behind master-password re-prompt) - Connected sites table - Paired dApps table (WizardConnect) - Transaction history (local; the on-chain source of truth is queryable via the chip-per-tx explorer link) - Auto-lock timeout - **Danger zone**: reset wallet (wipes local state, does not touch on-chain registrations — the certificates stay in the on-chain wallet the seed controls) --- ## 6. Storage & unlock - **Wallet key material lives in `/wallet.enc`**, encrypted the same way passwords are: PBKDF2 → AES-256-GCM under the master password (see [DESIGN-password-manager.md](DESIGN-password-manager.md) for the discipline). - **Same master password** unlocks both the vault and the wallet. Unlocking one unlocks the other for the session. The user picks the password once. - **Distinct purpose subtree** — the seed derives a wallet purpose root via HKDF with `info = "silentmode/wallet/0"`, independent of the password purpose root at `"silentmode/passwords/0"`. Extend `password-vault.js`'s `seedToPurposeRoot(...)` — it already takes a purpose string. - **The signing key is derived on demand** from the purpose root, not held in RAM long-term. Each `signMessage` / `sendPayment` reprises the derivation from the unlocked purpose root. - **Auto-lock** re-encrypts the purpose root under a fresh session key that is discarded on lock. Unlock re-derives from the master password. Cross-strand rule: **never derive a wallet key from the password purpose root (or vice versa).** This is the discipline that keeps a website-XSS-driven password leak from also leaking wallet keys. Documented explicitly in `DESIGN-password-manager.md`. --- ## 7. Sequencing — the roadmap in order | Phase | Roadmap ref | What ships | Depends on | |---|---|---|---| | B.1 | this doc | design doc, user aligned on permission model | — | | B.2a | roadmap B.2 | preload injection scaffold, read-only APIs only | — | | B.2b | roadmap B.2 | eTLD+1 origin binding (share PSL snapshot with A.2) | — | | B.3a | roadmap B.3 | wallet unlock flow reused from password vault | A.2 shipped | | B.3b | roadmap B.3 | `signMessage` + connect modal + settings-connected-sites | B.2 + B.3a | | B.3c | roadmap B.3 | `sendPayment` with per-amount cap | B.3b | | B.4 | roadmap B.4 | `registerName` / `updateName` / `transferName` | B.3b | | **W.1** | *(new)* | WizardConnect responder — WalletAdapter + pair modal | B.3b | | **W.2** | *(new)* | Settings-paired-dApps, session persistence, revoke | W.1 | WizardConnect is new to the roadmap — the existing `B.6 — Advanced connect- wallet UX` bullet was vague and `B.5 — Nostr messenger` conflates transport with client. Suggest updating the roadmap: **replace `B.6` with `W.1`+`W.2` above**, keep `B.5` for a Nostr messenger client if that's still wanted, but they are independent projects that happen to share a transport. W.1 depends on B.3b (the sign modal) so the same modal serves both surfaces. Do NOT ship W.1 before there is a working sign modal, or the WizardConnect responder will ship its own approval UI and the two will drift. --- ## 8. Open questions — decide before writing B.2+ code 1. **Chain switching.** Chipnet only in v1 — but what does `window.bcnr` do on a mainnet dApp that expects mainnet? Options: (a) Error with a clear message, (b) present a "this is a chipnet wallet" modal, (c) support both networks with a per-origin chain preference. *Recommend (a) for v1; revisit at mainnet switch.* 2. **`bcnr.sendPayment` recipient by name.** If `to: "alice.bch"`, do we resolve the `a` record (payment cashaddr) automatically, or force the caller to `bcnr.resolveName` first and pass a cashaddr? *Recommend auto-resolve; show the resolved address in the modal for the user to check.* 3. **QR scanner for pairing.** Camera-based (Electron desktop can access camera, but macOS permission is friction) vs paste-only. *Recommend paste-first, camera as an enhancement in W.2 or later.* 4. **BCNR-name provenance in the modal.** Show a "registered on-chain since height X, by category Y" line for BCNR-native origins? Helpful for advanced users, noise for others. *Recommend a collapsed disclosure that opens on click, not a default-visible field.* 5. **Rate-limiting on read-only methods.** A page loop calling `resolveName` thousands of times could DoS the resolver pool. *Recommend a per-origin token bucket in the preload — 20 requests/second, drops the excess with a normal-looking error.* 6. **Backup phrase display.** Behind the master password re-prompt, plain text or blur-until-hover? *Recommend blur-until-hover with a "why is this dangerous" panel that must be dismissed once before the phrase reveals.* 7. **Transaction history storage.** Local JSON, encrypted like the vault, or just derived from chain queries on demand? *Recommend derived-on-demand for v1 — no new storage, no sync surface, and the chain is authoritative.* 8. **`transferName` — does it need commit-reveal?** No, because a certificate transfer is just a token send; the commit-reveal spec applies to *registrations*. But the modal should still say "this sends the on-chain ownership token to a stranger and cannot be undone." --- ## 9. What this design does NOT decide - The exact modal visual design. Ship a functional MVP; polish in a UX pass. - The wallet's home screen (balance, recent transactions). Settings > Wallet is sufficient in v1; a dedicated pane is a v2 conversation. - On-chain price display (fiat quotes). Deliberate — Silent Mode does not run a fiat oracle, and pulling from an external price API undoes half the point of the project. - Multi-account UX. Single account per profile; sub-accounts / vanity accounts are a future strand. - Whether the wallet ships to Ariadne mobile as a companion. That is a separate mobile project; the design here informs it but does not require it. --- ## 10. References (do not re-derive) - Existing wallet: [`Argus/src/lib/wallet-web.js:BuiltInWallet`](../Argus/src/lib/wallet-web.js) - Existing registration flow: [`Argus/src/lib/registrar.js`](../Argus/src/lib/registrar.js), [`Argus/src/lib/register-tx.js`](../Argus/src/lib/register-tx.js) - Existing dApp-side WizardConnect (SilentMode registrar): [`Argus/src/lib/connect/wizardconnect.js`](../Argus/src/lib/connect/wizardconnect.js) - Existing wallet-side WizardConnect (Deviant, chipnet-validated): [`../../Deviant/Parameters/WalletConnect/WIZARDCONNECT-INTEGRATION-GUIDE.md`](../../Deviant/Parameters/WalletConnect/WIZARDCONNECT-INTEGRATION-GUIDE.md) - WizardConnect LGPL analysis: [`../Decentralized.DNS/WIZARDCONNECT-LICENSE-FINDING.md`](../Decentralized.DNS/WIZARDCONNECT-LICENSE-FINDING.md) - Crypto discipline (purpose subtrees, PBKDF2, HKDF): [`DESIGN-password-manager.md`](DESIGN-password-manager.md) - Registration flow status: [`../Decentralized.DNS/REGISTRATION-STATUS.md`](../Decentralized.DNS/REGISTRATION-STATUS.md)