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:
288
index.html
288
index.html
@ -136,12 +136,21 @@ window.WalletManager = WalletManager;
|
||||
<form id="add-wallet-form" onsubmit="return handleAddWallet(event)">
|
||||
<div class="mb-4">
|
||||
<label class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-1">Wallet Address</label>
|
||||
<input id="input-address" type="text" placeholder="0x..." class="w-full bg-[#05070B] border border-[#1A1F2C] rounded-lg px-3 py-2 text-white text-sm placeholder-gray-600 focus:outline-none focus:border-[#FF7A00]/50 transition font-mono" required>
|
||||
<input id="input-address" type="text" placeholder="Connect wallet..." class="w-full bg-[#05070B] border border-[#1A1F2C] rounded-lg px-3 py-2 text-white text-sm placeholder-gray-600 transition font-mono" readonly>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<div class="mb-4">
|
||||
<label class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-1">Chain</label>
|
||||
<input id="input-chain-display" type="text" placeholder="—" class="w-full bg-[#05070B] border border-[#1A1F2C] rounded-lg px-3 py-2 text-white text-sm placeholder-gray-600 transition" readonly>
|
||||
<input id="input-chain-hidden" type="hidden" value="base">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-1">Nickname</label>
|
||||
<input id="input-nickname" type="text" placeholder="Whale Wallet A" class="w-full bg-[#05070B] border border-[#1A1F2C] rounded-lg px-3 py-2 text-white text-sm placeholder-gray-600 focus:outline-none focus:border-[#FF7A00]/50 transition" required>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2">Color</label>
|
||||
<div id="add-wallet-color-picker" class="flex flex-wrap gap-2"></div>
|
||||
</div>
|
||||
<div id="add-wallet-error" class="hidden mb-3 text-red-400 text-xs font-medium"></div>
|
||||
<button type="submit" class="w-full bg-[#FF7A00] hover:bg-[#FF7A00]/90 text-white font-bold px-4 py-3 rounded-lg transition">Monitor Wallet</button>
|
||||
</form>
|
||||
@ -332,6 +341,9 @@ window.WalletManager = WalletManager;
|
||||
this._wallets = raw ? JSON.parse(raw) : [];
|
||||
if (!Array.isArray(this._wallets)) this._wallets = [];
|
||||
this._wallets = this._wallets.filter(w => w && w.address && w.chain).map(w => ({ address: String(w.address), chain: String(w.chain).toLowerCase(), nickname: String(w.nickname||''), isVerified: !!(w.isVerified && w.messageData), signature: w.signature ? String(w.signature) : null, messageData: w.messageData ? w.messageData : null }));
|
||||
/* Discard unverified wallets — keep only verified EVM or non-verification-chain wallets */
|
||||
this._wallets = this._wallets.filter(w => ['btc','bitcoin','solana'].includes(w.chain) ? true : w.isVerified);
|
||||
this._persist();
|
||||
this.getVerifiedWallets = function() { return this._wallets.filter(w => ['btc','bitcoin','solana'].includes(w.chain) ? true : w.isVerified).map(w => ({...w})); };
|
||||
} catch(e) { this._wallets = []; }
|
||||
return this._wallets;
|
||||
@ -372,15 +384,13 @@ window.WalletManager = WalletManager;
|
||||
this._persist();
|
||||
return { success: true };
|
||||
}
|
||||
revokeVerification(address, chain) {
|
||||
const w = this.findWallet(address, chain);
|
||||
if (!w) return { success: false, error: 'Wallet not found' };
|
||||
w.isVerified = false;
|
||||
w.signature = null;
|
||||
w.messageData = null;
|
||||
this._persist();
|
||||
return { success: true };
|
||||
}
|
||||
revokeVerification(address, chain) {
|
||||
const idx = this._wallets.findIndex(w => w.address === address && w.chain === chain);
|
||||
if (idx === -1) return { success: false, error: 'Wallet not found' };
|
||||
this._wallets.splice(idx, 1);
|
||||
this._persist();
|
||||
return { success: true };
|
||||
}
|
||||
};
|
||||
}
|
||||
})();
|
||||
@ -623,11 +633,97 @@ function handleRenameNickname(input) {
|
||||
/* ===================================================================
|
||||
Add Wallet Modal
|
||||
=================================================================== */
|
||||
let _addWalletSelectedColor = null;
|
||||
|
||||
function openAddWalletModal() {
|
||||
document.getElementById('add-wallet-modal').classList.add('open');
|
||||
document.getElementById('add-wallet-error').classList.add('hidden');
|
||||
document.getElementById('input-address').value = '';
|
||||
document.getElementById('input-nickname').value = '';
|
||||
if (!window.ethereum) {
|
||||
alert('No web3 provider found. Install a wallet extension (MetaMask, Rabby, etc.).');
|
||||
return;
|
||||
}
|
||||
/* Request connection first, then show modal with prefilled data */
|
||||
window.ethereum.request({ method: 'eth_requestAccounts' })
|
||||
.then(accounts => {
|
||||
if (!accounts || accounts.length === 0) return;
|
||||
|
||||
document.getElementById('input-address').value = accounts[0];
|
||||
document.getElementById('input-nickname').value = '';
|
||||
document.getElementById('input-chain-display').value = '';
|
||||
document.getElementById('input-chain-hidden').value = 'base';
|
||||
_addWalletSelectedColor = autoPickColor();
|
||||
renderAddWalletColorPicker();
|
||||
|
||||
window.ethereum.request({ method: 'eth_chainId' })
|
||||
.then(chainIdHex => {
|
||||
const chainName = CHAIN_IDS_REVERSED[chainIdHex] || 'base';
|
||||
document.getElementById('input-chain-display').value = chainName.charAt(0).toUpperCase() + chainName.slice(1);
|
||||
document.getElementById('input-chain-hidden').value = chainName;
|
||||
});
|
||||
|
||||
document.getElementById('add-wallet-error').classList.add('hidden');
|
||||
document.getElementById('add-wallet-modal').classList.add('open');
|
||||
})
|
||||
.catch(() => {
|
||||
alert('Wallet connection rejected.');
|
||||
});
|
||||
}
|
||||
|
||||
function autoPickColor() {
|
||||
const usedColors = wm.getWallets().map(w => getColorForWallet(w.address));
|
||||
for (const c of WALLET_COLORS) {
|
||||
if (!usedColors.includes(c)) return c;
|
||||
}
|
||||
return '#F7931A';
|
||||
}
|
||||
|
||||
function renderAddWalletColorPicker() {
|
||||
const container = document.getElementById('add-wallet-color-picker');
|
||||
container.innerHTML = '';
|
||||
if (!_addWalletSelectedColor) _addWalletSelectedColor = autoPickColor();
|
||||
WALLET_COLORS.forEach((c) => {
|
||||
const swatch = document.createElement('div');
|
||||
swatch.className = 'color-picker-swatch' + (_addWalletSelectedColor.toLowerCase() === c.toLowerCase() ? ' selected' : '');
|
||||
swatch.style.setProperty('--swatch-color', c);
|
||||
swatch.style.background = c;
|
||||
swatch.addEventListener('click', () => {
|
||||
_addWalletSelectedColor = c;
|
||||
renderAddWalletColorPicker();
|
||||
});
|
||||
container.appendChild(swatch);
|
||||
});
|
||||
}
|
||||
|
||||
/* Reverse map: chainId → chain name */
|
||||
const CHAIN_IDS_REVERSED = { '1': 'ethereum', '8453': 'base', '42161': 'arbitrum', '10': 'optimism', '137': 'polygon', '43114': 'avalanche', '56': 'bsc', '250': 'fantom', '998': 'hyperliquide' };
|
||||
|
||||
function fetchWalletAddress() {
|
||||
if (!window.ethereum) {
|
||||
document.getElementById('add-wallet-error').textContent = 'No web3 provider found';
|
||||
document.getElementById('add-wallet-error').classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
window.ethereum.request({ method: 'eth_requestAccounts' })
|
||||
.then(accounts => {
|
||||
if (accounts && accounts.length > 0) {
|
||||
document.getElementById('input-address').value = accounts[0];
|
||||
document.getElementById('add-wallet-error').classList.add('hidden');
|
||||
|
||||
/* Auto-detect chain from wallet */
|
||||
window.ethereum.request({ method: 'eth_chainId' })
|
||||
.then(chainIdHex => {
|
||||
const chainName = CHAIN_IDS_REVERSED[chainIdHex] || 'base';
|
||||
document.getElementById('input-chain-display').value = chainName.charAt(0).toUpperCase() + chainName.slice(1);
|
||||
document.getElementById('input-chain-hidden').value = chainName;
|
||||
});
|
||||
|
||||
/* Auto-assign a color not used by existing wallets */
|
||||
_addWalletSelectedColor = autoPickColor();
|
||||
renderAddWalletColorPicker();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
document.getElementById('add-wallet-error').textContent = 'Connection rejected';
|
||||
document.getElementById('add-wallet-error').classList.remove('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
function closeAddWalletModal() {
|
||||
@ -636,74 +732,96 @@ function closeAddWalletModal() {
|
||||
|
||||
async function handleAddWallet(e) {
|
||||
e.preventDefault();
|
||||
const address = document.getElementById('input-address').value.trim().toLowerCase();
|
||||
let address = document.getElementById('input-address').value.trim().toLowerCase();
|
||||
let chain = document.getElementById('input-chain-hidden').value || 'base';
|
||||
const nickname = document.getElementById('input-nickname').value.trim();
|
||||
const errEl = document.getElementById('add-wallet-error');
|
||||
|
||||
if (!address) { errEl.textContent = 'Address is required'; errEl.classList.remove('hidden'); return false; }
|
||||
/* If no address, connect wallet first */
|
||||
if (!address) {
|
||||
if (!window.ethereum) {
|
||||
errEl.textContent = 'No web3 provider found. Install a wallet extension.';
|
||||
errEl.classList.remove('hidden');
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
|
||||
if (!accounts || accounts.length === 0) {
|
||||
errEl.textContent = 'No account returned';
|
||||
errEl.classList.remove('hidden');
|
||||
return false;
|
||||
}
|
||||
address = accounts[0].toLowerCase();
|
||||
document.getElementById('input-address').value = accounts[0];
|
||||
|
||||
/* Auto-detect chain */
|
||||
const chainIdHex = await window.ethereum.request({ method: 'eth_chainId' });
|
||||
chain = CHAIN_IDS_REVERSED[chainIdHex] || 'base';
|
||||
document.getElementById('input-chain-hidden').value = chain;
|
||||
document.getElementById('input-chain-display').value = chain.charAt(0).toUpperCase() + chain.slice(1);
|
||||
|
||||
_addWalletSelectedColor = autoPickColor();
|
||||
renderAddWalletColorPicker();
|
||||
} catch (err) {
|
||||
errEl.textContent = 'Connection rejected';
|
||||
errEl.classList.remove('hidden');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Assign color */
|
||||
const color = _addWalletSelectedColor || autoPickColor();
|
||||
setColorForWallet(address, color);
|
||||
|
||||
/* Detect chain from address format */
|
||||
let chain = 'base';
|
||||
const isEVM = /^(0x)?[0-9a-fA-F]{40}$/i.test(address);
|
||||
const isBTC = /^(bc1[a-z0-9]{25,39}|1[a-km-zA-HJ-NP-Z1-9]{25,34})$/i.test(address);
|
||||
// if (!isEVM && !isBTC) { errEl.textContent = 'Invalid address format'; errEl.classList.remove('hidden'); return false; }
|
||||
const isSolana = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(address);
|
||||
|
||||
if (!isEVM && !isBTC) chain = 'btc';
|
||||
if (!isEVM && !isBTC && !isSolana) { errEl.textContent = 'Invalid address format'; errEl.classList.remove('hidden'); return false; }
|
||||
|
||||
/* Auto-verify non-EVM wallets */
|
||||
if (!isEVM && isBTC) {
|
||||
const addResult = wm.addWallet(address, 'btc', nickname);
|
||||
if (!isEVM) {
|
||||
const autoChain = isSolana ? 'solana' : 'btc';
|
||||
const addResult = wm.addWallet(address, autoChain, nickname);
|
||||
if (!addResult.success) { errEl.textContent = addResult.error; errEl.classList.remove('hidden'); return false; }
|
||||
wm.verifyWallet(address, 'btc', '', { action: 'Auto-verified (non-verification chain)', walletAddress: address });
|
||||
wm.verifyWallet(address, autoChain, '', { action: 'Auto-verified (non-verification chain)', walletAddress: address });
|
||||
walletSyncState[address] = 'synced';
|
||||
closeAddWalletModal();
|
||||
closeSidebar();
|
||||
renderAll();
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Register EVM wallet */
|
||||
const addResult = wm.addWallet(address, 'base', nickname);
|
||||
/* Register EVM wallet first so verifyOwnership can find it */
|
||||
const addResult = wm.addWallet(address, chain, nickname);
|
||||
if (!addResult.success) { errEl.textContent = addResult.error; errEl.classList.remove('hidden'); return false; }
|
||||
|
||||
/* Attempt EIP-712 verification if MetaMask is available */
|
||||
const verifyResult = await verifyOwnership(address, nickname);
|
||||
/* One-shot: verify or remove */
|
||||
const verifyResult = await verifyOwnership(address, chain, nickname);
|
||||
if (!verifyResult.success) {
|
||||
console.log("Verification not performed:", verifyResult.error);
|
||||
/* Wallet saved but unverified — user can verify from sidebar */
|
||||
wm.removeWallet(address, chain);
|
||||
errEl.textContent = verifyResult.error;
|
||||
errEl.classList.remove('hidden');
|
||||
return false;
|
||||
}
|
||||
|
||||
/* POST to FastAPI backend to start monitoring */
|
||||
try {
|
||||
const resp = await fetch('http://localhost:8000/api/v1/portfolio/monitor', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ address: address, chain: 'base', chain_id: 8453 })
|
||||
});
|
||||
if (!resp.ok) {
|
||||
console.log(`Backend returned ${resp.status}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Backend unavailable', err);
|
||||
}
|
||||
|
||||
/* Mark as syncing initially */
|
||||
walletSyncState[address] = 'syncing';
|
||||
|
||||
closeAddWalletModal();
|
||||
closeSidebar();
|
||||
walletSyncState[address] = 'syncing';
|
||||
|
||||
await _pollUntilSynced(address);
|
||||
renderSidebar();
|
||||
renderWalletPills();
|
||||
renderLedgerFilterWallets();
|
||||
|
||||
/* Kick off a fetch for this new wallet */
|
||||
fetchWalletAaveData(address);
|
||||
updateDashboard();
|
||||
await _fetchPricesAndRender();
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ===================================================================
|
||||
EIP-712 Verification
|
||||
=================================================================== */
|
||||
const EIP712_DOMAIN = { name: "Anonymous Wallet Tracker", version: "1", chainId: 1 };
|
||||
/* Chain name → numeric chainId for EIP-712 domain */
|
||||
const CHAIN_IDS = { ethereum: 1, base: 8453, arbitrum: 42161, optimism: 10, polygon: 137, avalanche: 43114, bsc: 56, fantom: 250, hyperliquide: 998 };
|
||||
const VERIFY_TYPES = {
|
||||
VerifyTracking: [
|
||||
{ name: 'action', type: 'string' },
|
||||
@ -712,7 +830,7 @@ const VERIFY_TYPES = {
|
||||
]
|
||||
};
|
||||
|
||||
async function verifyOwnership(address, nickname) {
|
||||
async function verifyOwnership(address, chain, nickname) {
|
||||
if (!window.ethereum) {
|
||||
return { success: false, error: 'No web3 provider' };
|
||||
}
|
||||
@ -721,8 +839,9 @@ async function verifyOwnership(address, nickname) {
|
||||
walletAddress: address,
|
||||
timestamp: Math.floor(Date.now() / 1000).toString()
|
||||
};
|
||||
const domain = { name: 'Anonymous Wallet Tracker', version: '1', chainId: CHAIN_IDS[chain] || 1 };
|
||||
const typedData = {
|
||||
domain: EIP712_DOMAIN,
|
||||
domain,
|
||||
types: { EIP712Domain: [{ name: 'name', type: 'string' }, { name: 'version', type: 'string' }, { name: 'chainId', type: 'uint256' }], VerifyTracking: VERIFY_TYPES.VerifyTracking },
|
||||
primaryType: 'VerifyTracking',
|
||||
message
|
||||
@ -740,8 +859,8 @@ async function verifyOwnership(address, nickname) {
|
||||
method: 'eth_signTypedData_v4',
|
||||
params: [accounts[0], JSON.stringify(typedData)]
|
||||
});
|
||||
wm.verifyWallet(address, 'base', sig, typedData.message);
|
||||
wm.renameWallet(address, 'base', nickname);
|
||||
wm.verifyWallet(address, chain, sig, typedData.message);
|
||||
wm.renameWallet(address, chain, nickname);
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
return { success: false, error: err?.message || 'Signature failed' };
|
||||
@ -760,18 +879,22 @@ async function handleVerifyWallet(address, chain, nickname) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await verifyOwnership(address, nickname || w.nickname);
|
||||
const result = await verifyOwnership(address, chain, nickname || w.nickname);
|
||||
if (result.success) {
|
||||
/* Fetch data now that wallet is verified */
|
||||
if (chain === 'base') fetchWalletAaveData(address);
|
||||
} else {
|
||||
/* Remove unverified wallet on failure */
|
||||
wm.removeWallet(address, chain);
|
||||
alert('Verification failed: ' + result.error);
|
||||
}
|
||||
renderAll();
|
||||
}
|
||||
|
||||
function handleRevokeVerification(address, chain) {
|
||||
wm.revokeVerification(address, chain);
|
||||
wm.removeWallet(address, chain);
|
||||
delete walletSyncState[address];
|
||||
delete addressSnapshots[address];
|
||||
renderAll();
|
||||
}
|
||||
|
||||
@ -870,7 +993,7 @@ function renderLedgerFilterWallets() {
|
||||
Dynamic Data Fetching (Promise.all aggregation)
|
||||
=================================================================== */
|
||||
async function fetchWalletAaveData(address) {
|
||||
const endpoint = `/api/v1/portfolio/${address}/avve`;
|
||||
const endpoint = `/api/v1/portfolio/${address}/base/aave`;
|
||||
try {
|
||||
const resp = await fetch(`${API_BASE}${endpoint}`);
|
||||
if (resp.status === 400) {
|
||||
@ -894,6 +1017,51 @@ async function fetchWalletAaveData(address) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Poll until wallet data is synchronized, up to ~60s */
|
||||
async function _pollUntilSynced(address, timeoutMs = 60000, intervalMs = 2000) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const resp = await fetch(`${API_BASE}/api/v1/portfolio/${address}/base/aave`);
|
||||
if (resp.ok) {
|
||||
const events = await resp.json();
|
||||
if (Array.isArray(events) && events.length > 0) {
|
||||
walletSyncState[address] = 'synced';
|
||||
addressSnapshots[address] = snapshotsToDaily(events);
|
||||
renderWalletPills();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (err) { /* ignore, will retry */ }
|
||||
await new Promise(r => setTimeout(r, intervalMs));
|
||||
}
|
||||
/* Timeout — mark as synced anyway so UI is not blocked */
|
||||
walletSyncState[address] = 'syncing';
|
||||
renderWalletPills();
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Fetch price history for the new wallet's data range, then render */
|
||||
async function _fetchPricesAndRender() {
|
||||
const selectedAddr = getSelectedAddresses();
|
||||
if (selectedAddr.length === 0) {
|
||||
renderCombinedTable();
|
||||
return;
|
||||
}
|
||||
/* Find oldest transaction to determine price range */
|
||||
let oldestTs = Date.now();
|
||||
selectedAddr.forEach(addr => {
|
||||
const snaps = addressSnapshots[addr] || [];
|
||||
snaps.forEach(s => {
|
||||
const t = new Date(s.block_timestamp).getTime();
|
||||
if (t < oldestTs) oldestTs = t;
|
||||
});
|
||||
});
|
||||
await fetchPrices(Object.keys(TOKENS), oldestTs);
|
||||
renderCombinedTable();
|
||||
updateDashboard();
|
||||
}
|
||||
|
||||
async function fetchSelectedWalletsData() {
|
||||
const selectedAddr = getSelectedAddresses();
|
||||
if (selectedAddr.length === 0) return;
|
||||
@ -1465,7 +1633,7 @@ async function pollSyncStatus() {
|
||||
if (syncing.length === 0) return;
|
||||
const promises = syncing.map(async (addr) => {
|
||||
try {
|
||||
const resp = await fetch(`${API_BASE}/api/v1/portfolio/${addr}/avve`);
|
||||
const resp = await fetch(`${API_BASE}/api/v1/portfolio/${addr}/base/aave`);
|
||||
if (resp.status === 400) {
|
||||
const body = await resp.json().catch(() => ({}));
|
||||
const details = (body?.detail || body?.details || '').toString().toUpperCase();
|
||||
|
||||
37
verifier.js
37
verifier.js
@ -16,12 +16,34 @@ import { WalletManager } from './wallets.js';
|
||||
/* EIP-712 Domain */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
const EIP712_DOMAIN = {
|
||||
name: 'Anonymous Wallet Tracker',
|
||||
version: '1',
|
||||
chainId: 1,
|
||||
/* Chain name → numeric chainId for EIP-712 domain */
|
||||
const CHAIN_IDS = {
|
||||
ethereum: 1,
|
||||
base: 8453,
|
||||
arbitrum: 42161,
|
||||
optimism: 10,
|
||||
polygon: 137,
|
||||
avalanche: 43114,
|
||||
bsc: 56,
|
||||
fantom: 250,
|
||||
hyperliquide: 998,
|
||||
};
|
||||
|
||||
/**
|
||||
* Build the EIP-712 domain for a given chain.
|
||||
* @param {string} chain — chain name
|
||||
* @returns {object} Domain object
|
||||
*/
|
||||
function buildEIP712Domain(chain) {
|
||||
const normalChain = String(chain).toLowerCase();
|
||||
const chainId = CHAIN_IDS[normalChain] ?? 1;
|
||||
return {
|
||||
name: 'Anonymous Wallet Tracker',
|
||||
version: '1',
|
||||
chainId,
|
||||
};
|
||||
}
|
||||
|
||||
const EIP712_TYPES = {
|
||||
VerifyTracking: [
|
||||
{ name: 'action', type: 'string' },
|
||||
@ -126,7 +148,7 @@ export class WalletVerifier {
|
||||
address = await this._requestAccount();
|
||||
|
||||
/* ---- Step 2: Build EIP-712 payload ---- */
|
||||
const typedData = this._buildTypedData(address);
|
||||
const typedData = this._buildTypedData(address, chain);
|
||||
|
||||
/* ---- Step 3: Request signature ---- */
|
||||
signature = await this._requestSignature(address, typedData);
|
||||
@ -205,12 +227,13 @@ export class WalletVerifier {
|
||||
* Build the EIP-712 typed data payload.
|
||||
*
|
||||
* @param {string} address
|
||||
* @param {string} chain — chain name for domain chainId
|
||||
* @returns {object} Full typedData object (domain + types + message)
|
||||
*/
|
||||
_buildTypedData(address) {
|
||||
_buildTypedData(address, chain) {
|
||||
const message = this._buildMessageData(address);
|
||||
return {
|
||||
domain: EIP712_DOMAIN,
|
||||
domain: buildEIP712Domain(chain),
|
||||
types: {
|
||||
EIP712Domain: [
|
||||
{ name: 'name', type: 'string' },
|
||||
|
||||
80
wallets.js
80
wallets.js
@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user