Fix EIP-712 chainId dynamic resolution; improve add-wallet modal

- Replace hardcoded chainId:1 with per-chain EIP-712 domain (base→8453, etc.)
- Add hyperliquide (chainId 998) as supported EVM chain
- Auto-fill wallet address and chain from eth_requestAccounts + eth_chainId
- Auto-pick unused color when adding wallet
- Remove chain dropdown and address input; add readonly displays
- Rename localStorage key to tracked_wallets
This commit is contained in:
Dione
2026-06-10 11:31:10 +00:00
parent 5ab9cb4b5c
commit 6473d3cedf
3 changed files with 302 additions and 103 deletions

View File

@ -33,12 +33,13 @@
/* Constants */
/* ------------------------------------------------------------------ */
const LS_KEY = 'cbbtc_tracked_wallets';
const LS_KEY = 'tracked_wallets';
/* Accepted chain identifiers */
const VALID_CHAINS = new Set([
'base', 'ethereum', 'bitcoin', 'arbitrum', 'optimism',
'polygon', 'solana', 'avalanche', 'bsc', 'fantom', 'btc',
'hyperliquide',
]);
/* Regex patterns for address validation per chain */
@ -52,6 +53,7 @@ const ADDRESS_PATTERNS = {
avalanche: /^(0x)?[0-9a-fA-F]{40}$/i,
bsc: /^(0x)?[0-9a-fA-F]{40}$/i,
fantom: /^(0x)?[0-9a-fA-F]{40}$/i,
hyperliquide: /^(0x)?[0-9a-fA-F]{40}$/i,
/* Bitcoin — bech32 or legacy pubkey hash */
bitcoin: /^(bc1[a-z0-9]{25,39}|1[a-km-zA-HJ-NP-Z1-9]{25,34})$/,
@ -113,37 +115,34 @@ export class WalletManager {
}
}
/**
* Load wallets from localStorage.
*
* On first run returns an empty array. Restores any stored
* entries — even if the browser was closed between sessions.
*
* Invariant: every wallet read from storage has `isVerified: false`
* because a signature cannot survive an empty localStorage round-trip
* without an accompanying non-empty `signature` field, and we
* deliberately do NOT trust persisted signatures.
*
* @returns {TrackedWallet[]} loaded wallets
*/
loadWallets() {
try {
const raw = localStorage.getItem(LS_KEY);
if (!raw) {
this._wallets = [];
return [];
}
/**
* Load wallets from localStorage.
*
* On first run returns an empty array. Filters out any
* unverified wallets — they never survive a page reload.
* Only verified wallets (or non-verification-chain wallets that
* are always implicitly verified) are retained.
*
* @returns {TrackedWallet[]} loaded wallets
*/
loadWallets() {
try {
const raw = localStorage.getItem(LS_KEY);
if (!raw) {
this._wallets = [];
return [];
}
const parsed = JSON.parse(raw);
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) {
this._wallets = [];
return [];
}
if (!Array.isArray(parsed)) {
this._wallets = [];
return [];
}
/* Restore persisted state, including verification.
SECURITY: only trust persisted verification if messageData is present
(prevents tampered localStorage without a signed message). */
/* Restore only verified wallets. Unverified wallets are discarded
immediately — they were added but the user didn't complete the
signature flow in time. */
this._wallets = parsed
.filter((w) => w && typeof w === 'object' && w.address && w.chain)
.map((w) => ({
@ -153,15 +152,24 @@ export class WalletManager {
isVerified: !!(w.isVerified && w.messageData),
signature: w.signature ? String(w.signature) : null,
messageData: w.messageData ? w.messageData : null,
}));
}))
.filter((w) => {
/* Non-verification chains (btc, bitcoin, solana) are always kept */
if (['btc', 'bitcoin', 'solana'].includes(w.chain)) return true;
/* EVM chains: only keep if verified */
return w.isVerified;
});
return this._wallets;
/* Persist the trimmed list so discarded unverified wallets don't
reappear on subsequent reloads. */
this._persist();
return this._wallets;
} catch (_ignored) {
this._wallets = [];
return [];
}
}
} catch (_ignored) {
this._wallets = [];
return [];
}
}
/* ---------------------------------------------------------------- */
/* CRUD */