Amends DESIGN-integrated-wallet.md §0 ("one account per profile in v1") with
a formal path to supporting external key imports (BIP39 seeds and raw WIFs)
alongside the primary HD purpose-subtree. Motivated by the user's 15-wallet
Deviant chipnet keystore now encrypted under the same master-password model
that Theseus is designed around.
Key discipline:
- SEPARATE storage file (wallet-imports.enc), same PBKDF2/AES-256-GCM crypto,
same master password, distinct KDF salt. Imports cannot compromise the
primary seed and vice versa; corruption of one file does not damage the
other.
- The imports schema mirrors Deviant's chipnet-keystore/1 shape (cashaddr /
label / category / source / path / seed / wif) so import is a 1:1 field
copy, not a translation.
- Every import stores the raw seed + BIP44 path (or the raw WIF) and derives
addresses DIRECTLY via HDKey.fromMasterSeed, bypassing the vault's
purposes/wallet HKDF subtree. This is essential: without the bypass,
pasting a mnemonic re-hashes the seed through HKDF and produces DIFFERENT
addresses than the source wallet (see memory
bchwallet-vault-root-derivation).
- No bcnr.importWallet() ever; imports are settings-page only.
- Sign modal grows an account picker with category chips; per-origin
"recently used" pre-selection with red-flag on category change.
Also introduces:
- bcnr.requestAccount({ account?, category? }) — backwards compatible
- bcnr.getAccounts({ category? })
- Stale-import detection (source-file cashaddr comparison on unlock)
- Sequencing: M.1 (generic import) → M.2 (import from Deviant keystore) →
M.3 (API extensions) → M.4 (stale detection). M.1 is load-bearing;
everything else composes.
Companion to Option A which shipped separately as the cross-repo fall-through
in Argus/src/lib/wallet.js.
21 KiB
Theseus wallet — multi-account amendment
Amendment to DESIGN-integrated-wallet.md §0
("Multi-account UX. One account per Theseus profile in v1."). Proposes lifting
that restriction under a specific discipline that keeps the single-seed HKDF
purpose-subtree model intact.
Status: draft — not accepted, not scheduled. Written 2026-09-09 as the
formal path to "Theseus recognises the master password and shows all of my
existing wallets, including externally-generated ones", raised while migrating
the Deviant Keys/chipnet-keystore.json to master-password encryption.
Companion documents:
DESIGN-integrated-wallet.md— the v1 design this amendsDESIGN-password-manager.md— the crypto discipline (PBKDF2, HKDF, purpose subtrees)../Argus/src/lib/password-vault.js— the implemented vault primitives../Argus/src/lib/wallet.js— the just-landed cross-repo loader (Option A) that resolvesloadWallet('sirius.x')from either SilentMode's ownwallets.jsonor Deviant's encryptedKeys/chipnet-keystore.json
0. Why amend now
Three facts changed since §0 was written:
- The user has 15 chipnet wallets already — 5 HD seeds plus 10 single-key
WIFs — spread across Deviant (
Keys/chipnet-keystore.json, encrypted with PBKDF2/AES-256-GCM and the master password) and SilentMode (Argus/wallets.json, plaintext). Consolidating them under Theseus is the natural next step now that both projects use the same master password. password-vault.jsalready ships the primitives that make this cheap:unlockVault(path, pw)returns a state object;seedToPurposeRoot(seed, purpose)derives an HKDF subtree deterministically;bytesToHex/hexToBytesare the wire format. There is no new crypto to design — only new state to serialise.- The Deviant wallet dApp (
shells/app/) already has multi-account UX, includingMultiKeyStore.WalletSummary { id, label, network, cashaddr, kind: 'independent' | 'hd-account' }. The alternative to amending Theseus is to host that dApp inside Theseus. This document argues the amendment is additionally worth doing, not a replacement for that path.
The core question §0 punted on was not "is multi-account complicated?" — it was "does supporting external key material contaminate the single-seed model?" This document answers: no, if we keep external material in a distinct storage lane with a separate origin tier in every UI surface.
1. What "multi-account" means here — three distinct notions
We need to keep these apart because they have different threat models:
| Kind | Storage | Origin | Recovery | Example |
|---|---|---|---|---|
| Primary account (unchanged) | HKDF subtree under silentmode/wallet/0/<index> |
Derived from the profile's single seed | Recover the seed → recover every primary account | The wallet a fresh Theseus profile creates for you |
| Primary sub-accounts (new) | HKDF subtree at index N under the same purpose root | Same seed | Same seed | "Trading", "Savings", "GMX game bets" — different indexes off the same profile seed |
| External accounts (new) | Separately encrypted blob keyed by the same master password | Independent (imported seed OR raw WIF) | Recover the external material — the profile seed does NOT recover them | Deviant's sirius.x, bns-beacon, the a-g throughput test WIFs |
The confusion §0 avoided was mixing categories 1 and 3 in one bag. The amendment keeps them in separate files on disk. The user sees one unified UI, the crypto keeps them completely isolated.
2. Threat-model deltas from §1 of the base doc
2.1 New surface: importing external key material
Every import path is a potential exfiltration event. Three attack shapes:
- Shoulder-surf on paste — someone reads the seed / WIF as it's typed or pasted. Mitigation: never render the imported string; the modal shows only a masked derived cashaddr after the import completes ("Import a wallet ending in …abc123?"). No "show me the WIF I just imported" affordance.
- Import from clipboard by malicious page — a page holding
navigator.clipboard.readText()permission could feed material INTO the import flow. Mitigation: import is a settings-page action only, never reachable fromwindow.bcnror WizardConnect. There is nobcnr.importWallet()method. Ever. - Import from disk by malicious extension — Theseus itself can't fully defend against this (§1.4 base doc), but the imported material lives in a separately-encrypted file so a compromised in-memory purpose-root leak does not leak the imports.
2.2 New surface: rendering many wallets in one UI
The user's clicks now cost more, because a wrong-account signature is irrecoverable. Mitigations:
- Explicit signer selection per sign-request. The sign modal shows
Signing as: [Sirius.x · BNS name buyer ▾]with the account picker OPEN by default (not collapsed). If a page requested a specific account viabcnr.requestAccount({ account: 'sirius.x' }), the modal still shows every account the user could have chosen — the requested one is pre-selected and the picker collapsed, but expanding it is one click. - Category badges everywhere the account appears. The wallet metadata
already carries a
categoryfield (bns,bns-infra,chipnet-test,operational, …). The modal chip is coloured by category sosirius.x(BNS) is visually distinct fromgmx(operational) even if the user only glanced at the modal. - "Recently used" is per-origin.
deviant.bchremembers which account you last signed with — but it does not silently reuse it; it pre-selects it in the modal, and a category change ("last time you used abnsaccount here, now the page wants anoperationalone") gets a red warning banner.
2.3 What we still do NOT defend against (unchanged from base doc §1.4)
Imports of a seed the user already leaked, RCE on Theseus itself, keylogger. These remain out-of-scope; the amendment does not widen the scope, only the attack surface within it.
3. Storage model
3.1 The primary vault stays as designed
<userData>/wallet.enc (from DESIGN-integrated-wallet.md §6) holds the
profile's single seed. Every primary sub-account is derived on demand via
seedToPurposeRoot(seed, "wallet/0") → BIP32-style index. Nothing about the
existing design changes.
3.2 New file: <userData>/wallet-imports.enc
Same crypto envelope as wallet.enc (PBKDF2-SHA256 → AES-256-GCM), same
master password, separate KDF salt and IV. This is the ONLY place external key
material lives. Format after decryption:
{
"version": "silentmode/wallet-imports/1",
"generated": "2026-09-09T12:00:00Z",
"accounts": {
"sirius-x": {
"kind": "seed",
"seed": "…",
"path": "m/44'/145'/0'/0/0",
"cashaddr": "bchtest:qzy92wznza359wzqz5r732wylfyrgl4djsl2agdp0p",
"label": "Sirius.x · BNS name buyer",
"category": "bns",
"source": "imported-from-D:/Dev/Deviant/Keys/chipnet-keystore.json#sirius-x",
"createdAt": 1725840000000
},
"gmx": {
"kind": "wif",
"wif": "…",
"cashaddr": "bchtest:qz040z8hza8n96llrk5kewp5dmyr5rd7csj8rasu9d",
"label": "GMX platform operator",
"category": "operational",
"source": "imported-from-D:/Dev/Deviant/Keys/chipnet-keystore.json#gmx",
"createdAt": 1725840000001
}
}
}
The accounts shape deliberately mirrors the fields already used by Deviant's
chipnet-keystore/1 format (cashaddr, label, category, source, path,
seed, wif) so a Deviant → Theseus import is a one-to-one field mapping,
not a schema translation.
Why a separate file, not one file with two sections? Blast radius. A bug
that corrupts wallet-imports.enc cannot destroy the primary seed. A revoke
"remove all imports" clears one file — the primary vault is untouched. This
is the same discipline DESIGN-password-manager.md uses to keep the password
vault distinct from the seed vault: distinct files, same master password.
3.3 One-way pointer: primary → imports, never the other way
The primary vault gets a new small field:
{ …existing wallet.enc payload…, "importsExist": true }
...updated on every import/revoke. Loading the primary vault tells Theseus
whether it should also try to decrypt wallet-imports.enc. The imports
file NEVER references the primary — an attacker who exfiltrates
wallet-imports.enc learns nothing about the profile seed, not even its
existence.
3.4 What the master password unlocks in one session
On unlockVault(masterPw):
- Derive
vaultKey(PBKDF2 withwallet.enc's salt). - Decrypt
wallet.enc→ get profile seed → derivepurposes/wallet/0,purposes/passwords/0,purposes/messenger/0. - If
importsExist: derivevaultKey'(PBKDF2 withwallet-imports.enc's DIFFERENT salt — deliberately not the same key material) and decrypt.
Both keys are held in RAM for the session; both are wiped on auto-lock. A malicious page that gets an in-memory read of the wallet purpose root learns nothing about imports.
4. UX
4.1 Settings > Wallet — the accounts panel
Replaces the base doc's "Account row: address, balance, backup phrase".
── Accounts ─────────────────────────────────────────────────────
[+ Create sub-account] [+ Import external wallet]
● Primary [rename ▎revoke]
bchtest:qxxx…yyy · category "primary"
● Trading (primary sub-account #1) [rename ▎revoke]
bchtest:qaaa…bbb · category "primary"
── Imported ─────────────────────────────────────────────────
● Sirius.x · BNS name buyer [rename ▎revoke]
bchtest:qzy…dp0p · category "bns"
source: Deviant/Keys/chipnet-keystore.json#sirius-x
● BNS TLD Beacon (.asm/.x) [rename ▎revoke]
bchtest:qzc…pmerh · category "bns-infra"
source: Deviant/Keys/chipnet-keystore.json#bns-tld-beacon
● GMX platform operator [rename ▎revoke]
bchtest:qz0…rasu9d · category "operational"
source: Deviant/Keys/chipnet-keystore.json#gmx
─────────────────────────────────────────────────────────────────
- Primary and sub-accounts always appear above imports. This is not just cosmetic — it reflects that the profile seed is the wallet's identity.
- Imports are grouped by
categorywhen there are more than 5.bns,bns-infra,operational,chipnet-testcollapse into collapsible groups. - Revoke removes an import (from
wallet-imports.enc); it does NOT touch the on-chain material and warns clearly: "The on-chain wallet still exists. Its seed/WIF is only removed from this Theseus profile." - No affordance to view seed / WIF material after import. Recovery lives in the SOURCE (Deviant's keystore, or wherever the user got it from). §2.1 shoulder-surf mitigation.
4.2 Sign-request modal — the account picker
Same modal as base doc §5, extended:
- Account row becomes
Signing as: [dropdown ▾]. - Dropdown lists every unlocked account, grouped: Primary → sub-accounts → imports.
- Each row shows label + truncated cashaddr + category chip.
- Origin's "recently used" account is pre-selected (§2.2 above).
- If the calling page passed
bcnr.requestAccount({ account: 'sirius.x' })and the account exists, it's pre-selected. If it doesn't exist, the modal says "This site is asking for an account you don't have. Sign as:" and the user picks from what they do have.
4.3 Import flow
Two entry points, both in Settings > Wallet:
-
"Import external wallet" → paste seed OR WIF. The pasted string never renders back; the modal shows only what will be stored (
kind, derivedcashaddrtruncated,labelinput,categorypicker). -
"Import from Deviant keystore" (if
D:/Dev/Deviant/Keys/chipnet-keystore.jsonis reachable). Detects the file, prompts for its master password (which must equal Theseus's — refuses otherwise with a clear message: "The Deviant keystore uses a different master password. Change one of them first."), decrypts, shows a preview list of every entry (label + cashaddr + category only), user picks which to import. Bulk-select shortcuts by category ("allbns", "alloperational").
The "different master password" rejection is a deliberate ergonomic constraint: we choose the pain of "unify your passwords once" over the pain of "which password unlocks what in this profile".
5. API surface for pages (deltas from §3 of base doc)
Backwards-compatible additions only:
bcnr.requestAccount({ account?: string, category?: string }): Promise<string | null>
// If neither `account` nor `category` given: current behaviour (returns cashaddr
// of the default account, or null if not granted).
// If `account`: requests a specific labelled account. Modal pre-selects it; if
// it doesn't exist, the modal falls back to a picker over what the user has.
// If `category`: requests any account in that category (e.g. "bns"). Useful
// for the SilentMode registrar UI which knows it wants a BNS-holding wallet
// but not which one.
bcnr.getAccounts({ category?: string }): Promise<Array<{
cashaddr: string;
label: string;
category: string;
}>>
// Returns granted accounts. Optionally filtered by category. Note: the user
// grants per-account, not per-category — this method never leaks accounts the
// origin was not granted access to.
The bcnr.signMessage / bcnr.sendPayment / bcnr.registerName signatures
gain an optional account?: string parameter that pre-selects an account in
the modal. Non-passing preserves current behaviour.
Not added: no method to import, revoke, or rename. All state-modifying account operations are settings-page only (§2.1).
6. Compatibility with Option A (the cross-repo loader that shipped 2026-09-09)
Option A lets Argus/src/lib/wallet.js:loadWallet('sirius.x') transparently
read Deviant's encrypted keystore. That path is authoritative today and stays
authoritative after this amendment ships:
- Theseus imports are copies, not sources of truth. Revoking an import in
Theseus does not affect Deviant's keystore or SilentMode's
wallets.json. - If the user rotates a key in Deviant's keystore, the corresponding Theseus
import goes stale. Theseus should detect this by comparing derived
cashaddron unlock — a mismatch surfaces a banner: "The Deviant source of Sirius.x has changed since this import. [Re-import] or [Remove]." - The two systems remain independent by design. This is not a bug; it is the same discipline that keeps password-vault seeds separate from wallet seeds (§3.4 base doc).
7. Sequencing — where this slots into the base roadmap
Adds to the base doc's §7:
| Phase | What ships | Depends on |
|---|---|---|
| B.3d | Sub-account creation (primary purpose subtree, indexes 1..N) — Trading / Savings labels |
B.3a shipped |
| M.1 | Import external seed / WIF, wallet-imports.enc file, account picker in sign modal |
B.3b + password-vault.js shipped ✓ |
| M.2 | Import from Deviant keystore (file detection + bulk-select UX) | M.1 + chipnet-keystore/2-encrypted format stable ✓ |
| M.3 | bcnr.requestAccount({ account, category }) + getAccounts({ category }) API extensions |
M.1 |
| M.4 | Stale-import detection (source-file cashaddr comparison) | M.2 |
M.1 is the load-bearing phase — everything else composes on top of the
wallet-imports.enc file. M.2 through M.4 are refinements the user's specific
setup makes worthwhile.
Do NOT ship M.2 (Deviant integration) before M.1 (generic import). The generic-first path keeps the design honest — if the amendment cannot be used by a user who never touches Deviant, we've over-fitted.
8. Open questions — decide before writing M.1 code
-
What's the primary account's "category"? Options:
primary(mirrors the model), leave the field null, or use the user-picked purpose ("Personal spending"). Recommendprimary— it's the one category name that never collides with an import. -
How does WizardConnect surface multi-account? External pairings via
wiz://— the@wizardconnect/wallet@0.2.2SDK's WalletAdapter has onegetXpub(path)and onesignTransaction(request). Neither takes an account parameter. Options: pair-per-account (each account has its ownwiz://URI); pair-per-profile with account selection at sign time (like in-page dApps); or accept that WizardConnect only exposes the primary account. Recommend option 3 for M.1 (only-primary), revisit when a user asks. Multi-account WizardConnect is a real cross-cutting design that deserves its own doc. -
Do we let the profile primary seed be replaced by an import? The proposal above says no — imports are always in the imports file, and the primary purpose subtrees stay tied to the profile seed. A user who wants "my Deviant seed as the profile seed" would create a fresh profile and restore that seed at profile-creation. Recommend keeping the restriction — it makes the sub-account / import distinction fundamental rather than optional.
-
Auto-lock behaviour with imports. Auto-lock re-encrypts the primary purpose root under a session key (§6 base doc). Do we do the same for the imports? Or wipe the imports outright and force a re-decrypt on unlock? Recommend the same treatment as primary — session-key re-encryption. The cost is symmetric.
-
Export. Can the user export an imported wallet's key back out of Theseus? Recommend no by default (§4.1 no-view-material rule), but a "Show recovery WIF for this import" behind master-password reprompt AND "why is this dangerous" panel (like the base doc §8.6 for the profile seed) is a reasonable escape hatch for users who need to move a wallet between machines.
-
Sub-account discovery. If a user restores their profile seed on a new Theseus install, do we auto-scan chipnet for used sub-account addresses? Recommend a "Scan for used sub-accounts" button in settings, not automatic. Scanning is a chain-load event; make the user ask for it.
-
Category taxonomy. The Deviant keystore uses
bns,bns-infra,chipnet-test,operational,primary,hd-general. Should Theseus enforce a fixed set or allow free-form strings? Recommend free-form strings with the six above as autocomplete suggestions. Constraining now would force a v2 migration when a user coins a new category.
9. What this amendment does NOT decide
- The wallet-imports file format after v1. Version-tag it now; iterate as
needed. The
version: "silentmode/wallet-imports/1"field exists precisely so a v2 migration is a well-defined event, not a guess. - The Deviant-side story. Deviant's
chipnet-keystore.jsoncontinues to be read by Deviant scripts (viascripts/lib/chipnet-keystore.mjs) and by SilentMode'sArgus/src/lib/wallet.js(via the Option A fall-through). This amendment adds a THIRD reader (Theseus) — it does not consolidate the two existing readers. - The mobile story. Ariadne (mobile companion, base doc §9) is out of scope.
When it lands, the same
wallet-imports.encformat ports over — that's the point of designing it version-tagged and portable.
10. Suggested acceptance path
If the reader agrees with this amendment:
- Merge M.1 into the roadmap between B.3b and B.4 in
ROADMAP-identity-wallet.md. - Move the base doc's §0 "Multi-account UX. One account per Theseus profile
in v1." to §9 "What this design does NOT decide, in v1 — this is now
addressed by
DESIGN-wallet-multi-account-amendment.md". - Address the open questions in §8 above in the roadmap discussion, not by editing this file — the design is stable, only the answers are open.
If the reader disagrees, the fallback path is what the base doc already
implies: host the Deviant wallet dApp inside Theseus, once window.bcnr
ships (base doc §4.4 — "External dApp using WizardConnect = Nostr relay",
minus the Nostr relay because the dApp is loaded IN Theseus, not paired
externally). That path requires no Theseus code changes beyond what B.2–B.4
already deliver, and it inherits Deviant's own multi-account UX for free. The
trade-off is that "my wallet" and "the Deviant wallet loaded in my wallet
browser" become two things instead of one; this amendment is the case for
one.