theseus/DESIGN-integrated-wallet.md
Local Dev 755e97b5bb Ship Theseus 0.0.8: window.bcnr dApp API + eTLD+1 permission origins
Merges a parallel session's work with the multi-source BNS story from 0.0.7.

The dApp side (parallel session)
--------------------------------
* bcnr-preload.js — installs `window.bcnr` on every page via contextBridge.
  Read-only surface: resolveName(name), isRegistered(name), getBcnrTlds(),
  getRecordVersion(name), plus getPermissionOrigin() for diagnostics. All
  Promises; a missing name returns null (not throw). No signing, no wallet
  unlock — that surface is designed but deliberately out of scope for 0.0.8
  (see TheseusNavigator/DESIGN-integrated-wallet.md).
* bcnr-origin.js — pure function that computes the eTLD+1 permission origin
  for a URL. ICANN suffixes via `psl` (same PSL Chromium uses, handles
  .co.uk / .github.io / etc); BNS names key off the on-chain TLD list so
  foo.wallet becomes a public suffix as soon as `wallet` appears there.
  Match browser cookie / MetaMask semantics: a grant on pay.merchant.com
  covers account.merchant.com but not evil.com.
* dev/bcnr-selftest.js, dev/origin-selftest.mjs — self-tests, no I/O.
* main.js wires bcnr-preload.js into session.defaultSession.setPreloads() so
  it runs BEFORE per-WebContentsView preloads; adds bcnr:* IPC handlers.
* preload.js + chrome.html — small hooks so the shell picks up window.bcnr
  the same way regular content does.
* package.json — psl dep, bcnr-preload.js/bcnr-origin.js in `files`.

Also included
-------------
* AriadneResolver/mobile/.../UpdateCheck.java — in-app update-check for the
  Android app; already active in the shipped 0.11 APK (build.ps1 -Recurse
  picked it up), formalising the source now.
* TheseusNavigator/snapshots/bns-name-snapshot.json — refreshed bundled
  starter (73 beacon txs, root c37b8596…c54e414ba).
* Site pages + manifest updated to point at 0.0.8.

  TheseusNavigator-Setup-0.0.8.exe    95.4 MB
    21939743eafdfe8742a6b7c4b987bd2782384d7bc41289cb80a7e08019dc9f02
  TheseusNavigator-0.0.8-portable.exe 92.7 MB
    2aa429fe39dc0fa4ac040fc6d6eb31b0f890c8a83175c49fcb50f052c480d39d
2026-08-31 01:38:55 +02:00

21 KiB

Theseus integrated BCH wallet — design

Companion to ROADMAP-identity-wallet.md Strand B and 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) as the signing core.
  • Reuse of the already-shipped registration/update flow (Argus/src/lib/register-tx.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). We do not want a Reown project ID as a dependency.
  • CashConnect V0. Cannot mint name certificates by design (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 <userData>/wallet-permissions.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)

bcnr.resolveName(name: string): Promise<Entry | null>
bcnr.isRegistered(name: string): Promise<boolean>
bcnr.getBcnrTlds(): Promise<string[]>              // ['bch','p2p','nav',...]
bcnr.getRecordVersion(name: string): Promise<number>  // for cache invalidation

3.2 Identity (permissioned; first call → modal)

bcnr.requestAccount(): Promise<string | null>         // returns cashaddr or null
bcnr.getAccounts():    Promise<string[]>              // [] 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)

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)

bcnr.registerName({ name: string, records?: Record<string,string>, userPrompt?: string }):
    Promise<{ txid: string, category: string }>
bcnr.updateName({ name: string, records: Record<string,string>, userPrompt?: string }):
    Promise<{ txid: string }>
bcnr.transferName({ name: string, toAddress: string, userPrompt?: string }):
    Promise<{ txid: string }>

3.5 Events

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 §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 <Uint8Array: 0x..> 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), 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: <shipped png>
    • 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 <userData>/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 <userData>/wallet.enc, encrypted the same way passwords are: PBKDF2 → AES-256-GCM under the master password (see 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)