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
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.bcnrprovider 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. getAccountsreturns[]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.
sendPaymenthas a per-approval amount cap. Approvingexample.bchfor 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.bchis a first-class origin (backed by an on-chain certificate),alice.comis 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 }
}
}
}
capSatsis the per-approval cap the site was granted, not a cumulative budget. A payment above it re-prompts with a new modal.usedandgrantsare 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
- Page calls
bcnr.requestAccount()(or any permissioned method). - Theseus renders a connect modal as a
WebContentsViewoverlay (same pattern as popover, engine-picker, downloads). - 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) andApprove for this site(grant persisted).
- On approve, the permission is written and the promise resolves.
- 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.2for the SDK; already tested against the 0.2.4-core wire protocol.nostr-toolsandwsare already in Theseus's deps (verified 2026-08-13).- A
WalletAdapterwrapping Theseus'sBuiltInWallet. 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.2types)getXpub(path): string— reads pre-cached xpubs fromBuiltInWalletgetPublicKey(path, index)— throws; the shipped SDK never calls itsignTransaction(request): Promise<{ signedTransaction: string }>— unwraps theWcSignTransactionRequest(the bug Deviant hit and fixed — see the integration guide §4), renders the same signing modal thewindow.bcnrprovider uses, and signs viacoSignTransaction-equivalent logic inwallet-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:51WIZARDCONNECT_SIGHASHfor 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
bcnrprovider. A dApp asking for a signature viawindow.bcnrfrom 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'ssessionmechanism 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". Extendpassword-vault.js'sseedToPurposeRoot(...)— 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/sendPaymentreprises 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
- Chain switching. Chipnet only in v1 — but what does
window.bcnrdo 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. bcnr.sendPaymentrecipient by name. Ifto: "alice.bch", do we resolve thearecord (payment cashaddr) automatically, or force the caller tobcnr.resolveNamefirst and pass a cashaddr? Recommend auto-resolve; show the resolved address in the modal for the user to check.- 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.
- 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.
- Rate-limiting on read-only methods. A page loop calling
resolveNamethousands 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. - 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.
- 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.
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 - Existing registration flow:
Argus/src/lib/registrar.js,Argus/src/lib/register-tx.js - Existing dApp-side WizardConnect (SilentMode registrar):
Argus/src/lib/connect/wizardconnect.js - Existing wallet-side WizardConnect (Deviant, chipnet-validated):
../../Deviant/Parameters/WalletConnect/WIZARDCONNECT-INTEGRATION-GUIDE.md - WizardConnect LGPL analysis:
../Decentralized.DNS/WIZARDCONNECT-LICENSE-FINDING.md - Crypto discipline (purpose subtrees, PBKDF2, HKDF):
DESIGN-password-manager.md - Registration flow status:
../Decentralized.DNS/REGISTRATION-STATUS.md