diff --git a/index.html b/index.html index f599b2d..5d366a2 100644 --- a/index.html +++ b/index.html @@ -477,19 +477,20 @@ window.WalletManager = WalletManager; this._persist(); return { success: true }; } - setWalletOrder(addresses) { - const addrSet = this._wallets.map(w => w.address); - const ordered = []; - for (const addr of addresses) { - if (addrSet.includes(addr)) { - const w = this._wallets.find(x => x.address === addr); - if (w) ordered.push(w); - } - } - const remaining = this._wallets.filter(w => !ordered.includes(w)); - this._wallets = [...ordered, ...remaining]; - this._persist(); - } + setWalletOrder(keys) { + const keySet = this._wallets.map(w => w.address + ':' + w.chain); + const ordered = []; + for (const k of keys) { + if (keySet.includes(k)) { + const [addr, chain] = k.split(':'); + const w = this._wallets.find(x => x.address === addr && x.chain === chain); + if (w) ordered.push(w); + } + } + const remaining = this._wallets.filter(w => !ordered.includes(w)); + this._wallets = [...ordered, ...remaining]; + this._persist(); + } }; } })(); @@ -501,7 +502,12 @@ const orangeBrandColor = '#FF7A00'; const blueBrandColor = '#3b82f6'; const API_BASE = window.location.origin; const WALLET_COLORS = ['#F7931A','#FF007F','#39FF14','#00FFFF','#CCFF00','#9D00FF','#FF0033','#00FFCC','#FF00FF','#007FFF','#DEFF0A','#FF5E00','#8A2BE2','#00FF66','#FF1493','#7B00FF']; -const TOKENS = {"cbBTC": {"decimals": 8, "priceSymbol": "BTC"}, "WBTC": {"decimals": 8, "priceSymbol": "BTC"}, "WETH": {"decimals": 18, "priceSymbol": "WETH"}, "USDC": {"decimals": 6, "priceSymbol": "USDC"}}; +const TOKENS = {"cbBTC": {"decimals": 8, "priceSymbol": "BTC"}, "WBTC": {"decimals": 8, "priceSymbol": "BTC"}, "WETH": {"decimals": 18, "priceSymbol": "WETH"}, "USDC": {"decimals": 6, "priceSymbol": "USDC"}, "USDCn": {"decimals": 6, "priceSymbol": "USDC"}, "wHYPE": {"decimals": 18, "priceSymbol": "HYPE"}}; +function symbolDisplay(s) { return s === 'USDCn' ? 'USDC' : s; } + +/* Compound key for wallet identity: "{address}:{chain}" */ +function walletKey(address, chain) { return address + ':' + chain; } +function walletKeyFromAddress(address) { return walletKey(address, 'base'); } /* =================================================================== App State @@ -536,38 +542,39 @@ let _csvImportRecords = []; /* Assigned color index for dynamic wallets */ let _colorIdx = 0; -function getColorForWallet(address) { - const c = localStorage.getItem('cbbtc_color_' + address); +function getColorForWallet(address, chain) { + const key = walletKey(address, chain || 'base'); + const c = localStorage.getItem('cbbtc_color_' + key); if (c) return c; const color = WALLET_COLORS[_colorIdx % WALLET_COLORS.length]; _colorIdx++; - localStorage.setItem('cbbtc_color_' + address, color); + localStorage.setItem('cbbtc_color_' + key, color); return color; } -function setColorForWallet(address, color) { - localStorage.setItem('cbbtc_color_' + address, color); +function setColorForWallet(address, chain, color) { + localStorage.setItem('cbbtc_color_' + walletKey(address, chain || 'base'), color); renderAll(); } -function cycleWalletColor(address) { - const next = getNextColor(address); - setColorForWallet(address, next); +function cycleWalletColor(address, chain) { + const next = getNextColor(address, chain); + setColorForWallet(address, chain, next); } -function getNextColor(address) { - const current = getColorForWallet(address); +function getNextColor(address, chain) { + const current = getColorForWallet(address, chain); const idx = WALLET_COLORS.indexOf(current); const nextIdx = (idx + 1) % WALLET_COLORS.length; return WALLET_COLORS[nextIdx]; } /* Color Picker */ -let _colorPickerTargetAddr = null; +let _colorPickerTarget = null; /* {address, chain} */ -function openColorPicker(event, address) { +function openColorPicker(event, address, chain) { event.stopPropagation(); - _colorPickerTargetAddr = address; + _colorPickerTarget = { address, chain }; const overlay = document.getElementById('color-picker-overlay'); const popup = document.getElementById('color-picker-popup'); @@ -578,7 +585,7 @@ function openColorPicker(event, address) { newPopup.className = 'color-picker-popup'; newPopup.style.left = event.clientX + 'px'; newPopup.style.top = event.clientY + 'px'; - const currentColor = getColorForWallet(address); + const currentColor = getColorForWallet(address, chain); WALLET_COLORS.forEach((c, i) => { const swatch = document.createElement('div'); swatch.className = 'color-picker-swatch' + (c.toLowerCase() === currentColor.toLowerCase() ? ' selected' : ''); @@ -586,7 +593,7 @@ function openColorPicker(event, address) { swatch.style.background = c; swatch.addEventListener('click', (e) => { e.stopPropagation(); - selectColor(c); + if (_colorPickerTarget) setColorForWallet(_colorPickerTarget.address, _colorPickerTarget.chain, c); closeColorPicker(); }); newPopup.appendChild(swatch); @@ -611,21 +618,25 @@ function selectColor(color) { } } -/* Wallet filter state — stores UNCHECKED addresses. Empty = all checked. */ +/* Wallet filter state — stores UNCHECKED compound keys. Empty = all checked. */ function getWalletFilter() { - return JSON.parse(localStorage.getItem('cbbtc_ledger_wallets') || '[]'); + const raw = JSON.parse(localStorage.getItem('cbbtc_ledger_wallets') || '[]'); + /* Migrate old plain-address entries to compound keys */ + return raw.filter(k => k.includes(':')).concat( + raw.filter(k => !k.includes(':')).map(k => walletKeyFromAddress(k)) + ); } -function isWalletInLedger(address) { - return !getWalletFilter().includes(address); /* unchecked not in list => checked */ +function isWalletInLedger(address, chain) { + return !getWalletFilter().includes(walletKey(address, chain || 'base')); } -function setWalletChecked(address, checked) { +function setWalletChecked(key, checked) { let unchecked = getWalletFilter(); if (!checked) { - if (!unchecked.includes(address)) unchecked.push(address); + if (!unchecked.includes(key)) unchecked.push(key); } else { - const idx = unchecked.indexOf(address); + const idx = unchecked.indexOf(key); if (idx >= 0) unchecked.splice(idx, 1); } @@ -639,20 +650,21 @@ function setWalletChecked(address, checked) { updateDashboard(); } -function toggleWalletFilter(address) { - setWalletChecked(address, !isWalletInLedger(address)); +function toggleWalletFilter(key) { + setWalletChecked(key, !getWalletFilter().includes(key)); } -function syncWalletCheckboxes(address, checked) { - document.querySelectorAll(`.wallet-filter-toggle[data-address="${address}"]`).forEach(cb => { +function syncWalletCheckboxes(key, checked) { + document.querySelectorAll(`.wallet-filter-toggle[data-key="${key}"]`).forEach(cb => { cb.checked = checked; }); } function syncAllWalletCheckboxes() { wm.getWallets().forEach(w => { - const checked = isWalletInLedger(w.address); - document.querySelectorAll(`.wallet-filter-toggle[data-address="${w.address}"]`).forEach(cb => { + const key = walletKey(w.address, w.chain); + const checked = isWalletInLedger(w.address, w.chain); + document.querySelectorAll(`.wallet-filter-toggle[data-key="${key}"]`).forEach(cb => { cb.checked = checked; }); }); @@ -688,9 +700,9 @@ function renderSidebar() { } let html = ''; wallets.forEach((w, i) => { - const color = getColorForWallet(w.address); + const color = getColorForWallet(w.address, w.chain); const shortAddr = w.address.slice(0, 6) + '...' + w.address.slice(-4); - const syncState = walletSyncState[w.address]; + const syncState = walletSyncState[walletKey(w.address, w.chain)]; const syncBadge = syncState === 'syncing' || syncState === 'pending' ? `⏳ Syncing` : ''; @@ -737,7 +749,7 @@ function handleRenameNickname(input) { =================================================================== */ let _addWalletSelectedColor = null; let _addWalletSelectedPlatform = 'aave'; -const HYPERLIQUIDE_CHAINS = ['hyperliquide']; +const HYPEREVM_CHAINS = ['hyperevm']; function openAddWalletModal() { if (!window.ethereum) { @@ -776,7 +788,7 @@ function openAddWalletModal() { } function autoPickColor() { - const usedColors = wm.getWallets().map(w => getColorForWallet(w.address)); + const usedColors = wm.getWallets().map(w => getColorForWallet(w.address, w.chain)); for (const c of WALLET_COLORS) { if (!usedColors.includes(c)) return c; } @@ -804,9 +816,9 @@ function renderLendingPlatformSelector(chainName) { const container = document.getElementById('lending-platform-selector'); container.innerHTML = ''; - const isHyperliquide = HYPERLIQUIDE_CHAINS.includes(chainName); + const isHyperEVM = HYPEREVM_CHAINS.includes(chainName); - if (isHyperliquide) { + if (isHyperEVM) { const platform = 'hyperlend'; const logoUrl = 'https://app.hyperlend.finance/assets/header-logo-CiRKYBzy.svg'; const color = '#caeae5'; @@ -840,7 +852,7 @@ function renderLendingPlatformSelector(chainName) { } /* Reverse map: chainId (decimal) → chain name */ -const CHAIN_IDS_REVERSED = { '1': 'ethereum', '8453': 'base', '42161': 'arbitrum', '10': 'optimism', '137': 'polygon', '43114': 'avalanche', '56': 'bsc', '250': 'fantom', '999': 'hyperliquide' }; +const CHAIN_IDS_REVERSED = { '1': 'ethereum', '8453': 'base', '42161': 'arbitrum', '10': 'optimism', '137': 'polygon', '43114': 'avalanche', '56': 'bsc', '250': 'fantom', '999': 'hyperevm' }; function fetchWalletAddress() { if (!window.ethereum) { @@ -940,7 +952,7 @@ async function handleAddWallet(e) { const addResult = wm.addWallet(address, autoChain, nickname, lendingPlatform); if (!addResult.success) { errEl.textContent = addResult.error; errEl.classList.remove('hidden'); return false; } wm.verifyWallet(address, autoChain, '', { action: 'Auto-verified (non-verification chain)', walletAddress: address }); - walletSyncState[address] = 'synced'; + walletSyncState[walletKey(address, autoChain)] = 'synced'; closeAddWalletModal(); closeSidebar(); renderAll(); @@ -962,7 +974,7 @@ async function handleAddWallet(e) { closeAddWalletModal(); closeSidebar(); - walletSyncState[address] = 'syncing'; + walletSyncState[walletKey(address, chain)] = 'syncing'; /* Register the address with the backend for monitoring */ try { @@ -990,7 +1002,7 @@ async function handleAddWallet(e) { EIP-712 Verification =================================================================== */ /* 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: 999 }; +const CHAIN_IDS = { ethereum: 1, base: 8453, arbitrum: 42161, optimism: 10, polygon: 137, avalanche: 43114, bsc: 56, fantom: 250, hyperevm: 999 }; const VERIFY_TYPES = { VerifyTracking: [ { name: 'action', type: 'string' }, @@ -1064,12 +1076,13 @@ async function handleVerifyWallet(address, chain, nickname) { Delete Wallet =================================================================== */ function handleDeleteWallet(address, chain, idx) { + const key = walletKey(address, chain); wm.removeWallet(address, chain); - delete walletSyncState[address]; - delete addressSnapshots[address]; + delete walletSyncState[key]; + delete addressSnapshots[key]; /* Remove from unchecked list */ let unchecked = getWalletFilter(); - const ui = unchecked.indexOf(address); + const ui = unchecked.indexOf(key); if (ui >= 0) unchecked.splice(ui, 1); if (unchecked.length === 0) { localStorage.removeItem('cbbtc_ledger_wallets'); @@ -1100,17 +1113,18 @@ function renderWalletPills() { } let html = ''; wallets.forEach((w) => { - const color = getColorForWallet(w.address); - const syncState = walletSyncState[w.address]; + const color = getColorForWallet(w.address, w.chain); + const syncState = walletSyncState[walletKey(w.address, w.chain)]; const isSyncing = syncState === 'syncing' || syncState === 'pending'; const syncIndicator = isSyncing ? ' ⏳' : ''; const nick = w.nickname || 'Wallet'; - const isActive = isWalletInLedger(w.address); + const isActive = isWalletInLedger(w.address, w.chain); const platformBadge = getPlatformBadge(w.lendingPlatform); - html += '' + - '' + + const key = walletKey(w.address, w.chain); + html += '' + + '' + '' + '' + nick + syncIndicator + platformBadge + '' + '' + w.chain + '' + @@ -1129,16 +1143,16 @@ function toggleWalletPill(address) { /* =================================================================== Drag Reorder (wallet pills) =================================================================== */ -let _dragAddr = null; -let _dropTargetAddr = null; +let _dragKey = null; +let _dropTargetKey = null; let _dropBefore = true; function startPillDrag(e) { - if (!e.currentTarget.dataset.address) return; - _dragAddr = e.currentTarget.dataset.address; + if (!e.currentTarget.dataset.key) return; + _dragKey = e.currentTarget.dataset.key; if (e.dataTransfer) { e.dataTransfer.effectAllowed = 'move'; - e.dataTransfer.setData('text/plain', _dragAddr); + e.dataTransfer.setData('text/plain', _dragKey); } e.stopPropagation(); e.currentTarget.classList.add('pill-dragging'); @@ -1150,36 +1164,36 @@ function endPillDrag(e) { } function cleanupDrag() { - _dragAddr = null; - _dropTargetAddr = null; + _dragKey = null; + _dropTargetKey = null; _dropBefore = true; document.querySelectorAll('.pill-dragging').forEach(el => el.classList.remove('pill-dragging')); document.querySelectorAll('.drag-insert-line').forEach(el => el.remove()); } -function dropWalletReorder(e, targetAddress) { +function dropWalletReorder(e, targetKey) { e.preventDefault(); e.stopPropagation(); - if (!_dragAddr) return cleanupDrag(); + if (!_dragKey) return cleanupDrag(); const wallets = wm.getWallets(); - const addrs = wallets.map(w => w.address); - const dragIdx = addrs.indexOf(_dragAddr); + const keys = wallets.map(w => walletKey(w.address, w.chain)); + const dragIdx = keys.indexOf(_dragKey); if (dragIdx === -1) return cleanupDrag(); - if (targetAddress && targetAddress !== _dragAddr) { - const targetIdx = addrs.indexOf(targetAddress); + if (targetKey && targetKey !== _dragKey) { + const targetIdx = keys.indexOf(targetKey); if (targetIdx !== -1) { - const moved = addrs.splice(dragIdx, 1)[0]; + const moved = keys.splice(dragIdx, 1)[0]; const insertIdx = _dropBefore ? targetIdx : targetIdx + 1; - addrs.splice(insertIdx, 0, moved); - wm.setWalletOrder(addrs); + keys.splice(insertIdx, 0, moved); + wm.setWalletOrder(keys); renderAll(); } - } else if (!_dropTargetAddr) { - const moved = addrs.splice(dragIdx, 1)[0]; - addrs.push(moved); - wm.setWalletOrder(addrs); + } else if (!_dropTargetKey) { + const moved = keys.splice(dragIdx, 1)[0]; + keys.push(moved); + wm.setWalletOrder(keys); renderAll(); } cleanupDrag(); @@ -1193,24 +1207,24 @@ function setupPillDragListeners(container) { e.preventDefault(); e.stopPropagation(); e.dataTransfer.dropEffect = 'move'; - _dropTargetAddr = pill.dataset.address; + _dropTargetKey = pill.dataset.key; const rect = pill.getBoundingClientRect(); _dropBefore = e.clientX < rect.left + rect.width / 2; }); pill.addEventListener('dragleave', () => { document.querySelector('.drag-insert-line')?.remove(); }); - pill.addEventListener('drop', (e) => { dropWalletReorder(e, pill.dataset.address); }); + pill.addEventListener('drop', (e) => { dropWalletReorder(e, pill.dataset.key); }); }); } const pillsContainer = document.getElementById('wallet-pills'); pillsContainer.addEventListener('dragover', (e) => { - if (!_dragAddr) return; + if (!_dragKey) return; document.querySelector('.drag-insert-line')?.remove(); const children = Array.from(pillsContainer.children).filter(c => c.classList && c.classList.contains('pill-btn')); for (const child of children) { - if (child.dataset.address === _dragAddr) continue; + if (child.dataset.key === _dragKey) continue; const rect = child.getBoundingClientRect(); if (e.clientX < rect.left + rect.width / 2) { const line = document.createElement('div'); @@ -1222,16 +1236,16 @@ pillsContainer.addEventListener('dragover', (e) => { }); pillsContainer.addEventListener('drop', (e) => { - if (!_dragAddr || e.target.classList.contains('pill-btn')) return; + if (!_dragKey || e.target.classList.contains('pill-btn')) return; e.preventDefault(); e.stopPropagation(); const wallets = wm.getWallets(); - const addrs = wallets.map(w => w.address); - const dragIdx = addrs.indexOf(_dragAddr); + const keys = wallets.map(w => walletKey(w.address, w.chain)); + const dragIdx = keys.indexOf(_dragKey); if (dragIdx !== -1) { - const moved = addrs.splice(dragIdx, 1)[0]; - addrs.push(moved); - wm.setWalletOrder(addrs); + const moved = keys.splice(dragIdx, 1)[0]; + keys.push(moved); + wm.setWalletOrder(keys); renderAll(); } cleanupDrag(); @@ -1240,14 +1254,8 @@ pillsContainer.addEventListener('drop', (e) => { function getSelectedAddresses() { const unchecked = getWalletFilter(); const wallets = wm.getWallets(); - if (unchecked.length === 0) return wallets.map(w => w.address); - return wallets.filter(w => !unchecked.includes(w.address)).map(w => w.address); -} - -function syncWalletCheckboxes(address, checked) { - document.querySelectorAll(`.wallet-filter-toggle[data-address="${address}"]`).forEach(cb => { - cb.checked = checked; - }); + const filtered = wallets.filter(w => !unchecked.includes(walletKey(w.address, w.chain))); + return filtered.map(w => ({ address: w.address, chain: w.chain })); } /* =================================================================== @@ -1258,17 +1266,18 @@ function renderLedgerFilterWallets() { const container = document.getElementById('ledger-filter-wallets'); let html = ''; wallets.forEach(w => { - const color = getColorForWallet(w.address); - const checked = isWalletInLedger(w.address); + const color = getColorForWallet(w.address, w.chain); + const key = walletKey(w.address, w.chain); + const checked = isWalletInLedger(w.address, w.chain); html += ` - + `; }); container.innerHTML = html; container.querySelectorAll('.wallet-filter-toggle').forEach(cb => { cb.addEventListener('change', () => { - setWalletChecked(cb.dataset.address, cb.checked); + setWalletChecked(cb.dataset.key, cb.checked); }); }); } @@ -1278,6 +1287,7 @@ function renderLedgerFilterWallets() { =================================================================== */ async function fetchWalletAaveData(address, chain) { chain = chain || 'base'; + const key = walletKey(address, chain); const endpoint = `/api/v1/portfolio/${address}/${chain}/aave`; try { const resp = await fetch(`${API_BASE}${endpoint}`); @@ -1285,24 +1295,24 @@ async function fetchWalletAaveData(address, chain) { const body = await resp.json().catch(() => ({})); const details = (body?.detail || body?.details || '').toString().toUpperCase(); if (details.includes('PENDING') || details.includes('SYNCING')) { - walletSyncState[address] = 'syncing'; + walletSyncState[key] = 'syncing'; renderWalletPills(); return; } } if (resp.status === 404) { - walletSyncState[address] = 'pending'; + walletSyncState[key] = 'pending'; renderWalletPills(); return; } if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const events = await resp.json(); - walletSyncState[address] = 'synced'; - addressSnapshots[address] = snapshotsToDaily(events); + walletSyncState[key] = 'synced'; + addressSnapshots[key] = snapshotsToDaily(events); renderWalletPills(); } catch (err) { - console.error(`Failed to fetch data for ${address}:`, err); - walletSyncState[address] = 'pending'; + console.error(`Failed to fetch data for ${key}:`, err); + walletSyncState[key] = 'pending'; renderWalletPills(); } } @@ -1310,6 +1320,7 @@ async function fetchWalletAaveData(address, chain) { /* Poll until wallet data is synchronized, up to ~60s */ async function _pollUntilSynced(address, timeoutMs = 60000, intervalMs = 2000, chain) { chain = chain || 'base'; + const key = walletKey(address, chain); const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { try { @@ -1317,8 +1328,8 @@ async function _pollUntilSynced(address, timeoutMs = 60000, intervalMs = 2000, c if (resp.ok) { const events = await resp.json(); if (Array.isArray(events) && events.length > 0) { - walletSyncState[address] = 'synced'; - addressSnapshots[address] = snapshotsToDaily(events); + walletSyncState[key] = 'synced'; + addressSnapshots[key] = snapshotsToDaily(events); renderWalletPills(); return true; } @@ -1327,22 +1338,22 @@ async function _pollUntilSynced(address, timeoutMs = 60000, intervalMs = 2000, c await new Promise(r => setTimeout(r, intervalMs)); } /* Timeout — mark as synced anyway so UI is not blocked */ - walletSyncState[address] = 'syncing'; + walletSyncState[key] = '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) { + const selectedWallets = getSelectedAddresses(); + if (selectedWallets.length === 0) { renderCombinedTable(); return; } /* Find oldest transaction to determine price range */ let oldestTs = Date.now(); - selectedAddr.forEach(addr => { - const snaps = addressSnapshots[addr] || []; + selectedWallets.forEach(w => { + const snaps = addressSnapshots[walletKey(w.address, w.chain)] || []; snaps.forEach(s => { const t = new Date(s.block_timestamp).getTime(); if (t < oldestTs) oldestTs = t; @@ -1354,18 +1365,17 @@ async function _fetchPricesAndRender() { } async function fetchSelectedWalletsData() { - const selectedAddr = getSelectedAddresses(); - if (selectedAddr.length === 0) return; + const selectedWallets = getSelectedAddresses(); + if (selectedWallets.length === 0) return; /* Only fetch for wallets that are synced or syncing */ - const promises = selectedAddr.map(async (addr) => { - const sync = walletSyncState[addr]; + const promises = selectedWallets.map(async (wallet) => { + const key = walletKey(wallet.address, wallet.chain); + const sync = walletSyncState[key]; if (sync === 'syncing') return null; if (sync === 'pending') return null; - if (sync === 'synced' && addressSnapshots[addr]) return null; - const w = wm.getWallets().find(w => w.address === addr); - const chain = (w || {}).chain || 'base'; - await fetchWalletAaveData(addr, chain); + if (sync === 'synced' && addressSnapshots[key]) return null; + await fetchWalletAaveData(wallet.address, wallet.chain); }); await Promise.all(promises); } @@ -1451,11 +1461,11 @@ function snapshotsToDaily(events) { Dashboard Update =================================================================== */ function getOldestTransactionDate() { - const selectedAddr = getSelectedAddresses(); - if (selectedAddr.length === 0) return Date.now() - 365 * 86400000; + const selectedWallets = getSelectedAddresses(); + if (selectedWallets.length === 0) return Date.now() - 365 * 86400000; let oldestTs = Infinity; - selectedAddr.forEach(addr => { - const snaps = addressSnapshots[addr] || []; + selectedWallets.forEach(w => { + const snaps = addressSnapshots[walletKey(w.address, w.chain)] || []; snaps.forEach(snap => { const sTs = new Date(snap.block_timestamp).getTime(); if (sTs < oldestTs) oldestTs = sTs; @@ -1466,16 +1476,17 @@ function getOldestTransactionDate() { /* Build per-day cumulative BTC series from addressSnapshots. Returns both aggregated total and per-wallet series. */ -function buildCumulativeSeries(addresses) { +function buildCumulativeSeries(wallets) { const allDates = {}; - addresses.forEach(addr => { - const snaps = addressSnapshots[addr] || []; + const keys = wallets.map(w => walletKey(w.address, w.chain)); + keys.forEach(key => { + const snaps = addressSnapshots[key] || []; snaps.forEach(snap => { allDates[snap.block_timestamp.slice(0, 10)] = true; }); }); const snapshotDates = Object.keys(allDates).sort(); - if (snapshotDates.length === 0) return { total: [], perWallet: [] }; + if (snapshotDates.length === 0) return { total: [], perWallet: {} }; /* Build per-wallet forward-filled balance for every calendar day */ const firstUTC = Date.UTC(new Date(snapshotDates[0]).getUTCFullYear(), new Date(snapshotDates[0]).getUTCMonth(), new Date(snapshotDates[0]).getUTCDate()); @@ -1484,9 +1495,9 @@ function buildCumulativeSeries(addresses) { /* Pre-compute per-wallet dated arrays (oldest-first) */ const walletDated = {}; - addresses.forEach(addr => { - const snaps = addressSnapshots[addr] || []; - walletDated[addr] = snaps.map(s => ({ + keys.forEach(key => { + const snaps = addressSnapshots[key] || []; + walletDated[key] = snaps.map(s => ({ dateStr: s.block_timestamp.slice(0, 10), btcAmt: getTotalBTC(s?.wallet) + getTotalBTC(s?.collateral) })).sort((a, b) => a.dateStr.localeCompare(b.dateStr)); @@ -1495,20 +1506,20 @@ function buildCumulativeSeries(addresses) { /* Walk every calendar day, forward-fill each wallet, aggregate */ const totalResult = []; const perWalletResult = {}; - addresses.forEach(addr => { perWalletResult[addr] = []; }); + keys.forEach(key => { perWalletResult[key] = []; }); for (let t = firstUTC; t <= lastUTC; t += DAY_MS) { const ds = new Date(t).toISOString().slice(0, 10); let dayTotal = 0; - addresses.forEach(addr => { - const dated = walletDated[addr]; + keys.forEach(key => { + const dated = walletDated[key]; let balance = 0; for (const d of dated) { if (d.dateStr <= ds) balance = d.btcAmt; else break; } dayTotal += balance; - perWalletResult[addr].push([t, balance]); + perWalletResult[key].push([t, balance]); }); totalResult.push([t, dayTotal]); } @@ -1517,31 +1528,32 @@ function buildCumulativeSeries(addresses) { } function calculateAggregatedSeries() { - const selectedAddr = getSelectedAddresses(); + const selectedWallets = getSelectedAddresses(); currentBuyCost = 0; currentBuyAmount = 0; - if (selectedAddr.length === 0) { + if (selectedWallets.length === 0) { currentAggregatedSeries = []; - currentWalletSeries = []; + currentWalletSeries = {}; return; } - const result = buildCumulativeSeries(selectedAddr); + const result = buildCumulativeSeries(selectedWallets); currentAggregatedSeries = result.total; currentWalletSeries = result.perWallet; } function calculateCurrentHoldings() { - const selectedAddr = getSelectedAddresses(); + const selectedWallets = getSelectedAddresses(); currentNetHeld = 0; currentBuyCost = 0; currentBuyAmount = 0; - if (selectedAddr.length === 0) return; + if (selectedWallets.length === 0) return; /* Sum latest cbBTC balance across selected wallets */ - selectedAddr.forEach(addr => { - const snaps = addressSnapshots[addr] || []; + selectedWallets.forEach(w => { + const key = walletKey(w.address, w.chain); + const snaps = addressSnapshots[key] || []; if (snaps.length > 0) { const latest = snaps[0]; currentNetHeld += getTotalBTC(latest?.wallet); @@ -1550,8 +1562,9 @@ function calculateCurrentHoldings() { }); /* Derive buy cost from snapshot deltas: positive BTC changes, oldest first */ - selectedAddr.forEach(addr => { - const snaps = addressSnapshots[addr] || []; + selectedWallets.forEach(w => { + const key = walletKey(w.address, w.chain); + const snaps = addressSnapshots[key] || []; if (snaps.length === 0) return; let prevBtc = 0; snaps.slice().reverse().forEach(snap => { @@ -1591,9 +1604,9 @@ function updateDashboard() { } /* Filter transaction table */ - const selectedAddr = getSelectedAddresses(); + const selectedWallets = getSelectedAddresses(); const activeTypes = Array.from(document.querySelectorAll('.filter-checkbox:checked')).map(cb => cb.dataset.filter); - const ledgerWallets = Array.from(document.querySelectorAll('.wallet-filter-toggle:checked')).map(cb => cb.dataset.address); + const ledgerWallets = Array.from(document.querySelectorAll('.wallet-filter-toggle:checked')).map(cb => cb.dataset.key || cb.dataset.address); let visibleCount = 0; document.querySelectorAll('#transaction-table tbody tr').forEach(row => { @@ -1644,15 +1657,17 @@ function renderCombinedTable() { const walletNickMap = {}; const wallets = wm.getWallets(); wallets.forEach(w => { - walletColorMap[w.address] = getColorForWallet(w.address); - walletNickMap[w.address] = w.nickname || 'Wallet'; + const key = walletKey(w.address, w.chain); + walletColorMap[key] = getColorForWallet(w.address, w.chain); + walletNickMap[key] = w.nickname || 'Wallet'; }); /* Build unified snapshot list from dynamic sources only */ const unified = []; - for (const [addr, snaps] of Object.entries(addressSnapshots)) { + for (const [key, snaps] of Object.entries(addressSnapshots)) { + const [addr] = key.split(':'); (snaps || []).forEach(snap => { - unified.push({ ...snap, _walletAddress: addr }); + unified.push({ ...snap, _walletAddress: key, _walletAddr: addr }); }); } @@ -1686,7 +1701,7 @@ function renderCombinedTable() { const usd = amt * price; coldTotal += usd; coldItems.push({ - symbol: sym, amountStr: amt < 1 ? amt.toFixed(6).replace(/0+$/, '') : amt.toFixed(2), + symbol: symbolDisplay(sym), amountStr: amt < 1 ? amt.toFixed(6).replace(/0+$/, '') : amt.toFixed(2), priceStr: price > 0 ? new Intl.NumberFormat('en-US',{style:'currency','currency':'USD',maximumFractionDigits:0}).format(price) : '$0', usdStr: new Intl.NumberFormat('en-US',{style:'currency','currency':'USD',minimumFractionDigits:0,maximumFractionDigits:0}).format(usd), usd }); @@ -1701,7 +1716,7 @@ function renderCombinedTable() { const usd = amt * price; collateralTotal += usd; collateralItems.push({ - symbol: sym, amountStr: amt < 1 ? amt.toFixed(6).replace(/0+$/, '') : amt.toFixed(4), + symbol: symbolDisplay(sym), amountStr: amt < 1 ? amt.toFixed(6).replace(/0+$/, '') : amt.toFixed(4), priceStr: price > 0 ? new Intl.NumberFormat('en-US',{style:'currency','currency':'USD',maximumFractionDigits:0}).format(price) : '$0', usdStr: new Intl.NumberFormat('en-US',{style:'currency','currency':'USD',minimumFractionDigits:0,maximumFractionDigits:0}).format(usd), usd }); @@ -1716,7 +1731,7 @@ function renderCombinedTable() { const usd = amt * price; borrowTotal += usd; borrowItems.push({ - symbol: sym, amountStr: amt < 1 ? amt.toFixed(6).replace(/0+$/, '') : amt.toFixed(2), + symbol: symbolDisplay(sym), amountStr: amt < 1 ? amt.toFixed(6).replace(/0+$/, '') : amt.toFixed(2), priceStr: price > 0 ? new Intl.NumberFormat('en-US',{style:'currency','currency':'USD',maximumFractionDigits:0}).format(price) : '$0', usdStr: new Intl.NumberFormat('en-US',{style:'currency','currency':'USD',minimumFractionDigits:0,maximumFractionDigits:0}).format(usd), usd }); @@ -1845,13 +1860,13 @@ function updateBtcUI(price, percent) { } function setupCumulCard(cutoff) { - const selectedAddr = getSelectedAddresses(); + const selectedWallets = getSelectedAddresses(); const wallets = wm.getWallets(); - const series = selectedAddr.map(addr => { - const w = wallets.find(x => x.address === addr); - const nickname = w ? (w.nickname || addr.slice(0, 6)) : addr.slice(0, 6); - const ws = currentWalletSeries[addr] || []; + const series = selectedWallets.map(w => { + const wm = wallets.find(x => x.address === w.address && x.chain === w.chain); + const nickname = wm ? (wm.nickname || w.address.slice(0, 6)) : w.address.slice(0, 6); + const ws = currentWalletSeries[walletKey(w.address, w.chain)] || []; const filtered = ws.filter(d => d[0] >= cutoff); /* Extend to today */ if (filtered.length > 0) { @@ -1864,7 +1879,7 @@ function setupCumulCard(cutoff) { return { name: nickname, data: filtered }; }); - const colors = selectedAddr.map(addr => getColorForWallet(addr)); + const colors = selectedWallets.map(w => getColorForWallet(w.address, w.chain)); const gradientStops = colors.map(c => [ { offset: 0, color: c, opacity: 0.35 }, @@ -1921,9 +1936,9 @@ function setupSatsCard(cutoff) { const DAY_MS = 86400000; /* Collect all dates across selected wallets, then forward-fill each wallet's balance */ const allDates = {}; - const selectedAddr = getSelectedAddresses() || []; - selectedAddr.forEach(addr => { - const snaps = addressSnapshots[addr] || []; + const selectedWallets = getSelectedAddresses() || []; + selectedWallets.forEach(w => { + const snaps = addressSnapshots[walletKey(w.address, w.chain)] || []; snaps.forEach(snap => { allDates[snap.block_timestamp.slice(0, 10)] = true; }); }); const sortedDates = Object.keys(allDates).sort(); @@ -1931,9 +1946,10 @@ function setupSatsCard(cutoff) { const wallets = wm.getWallets(); /* Per-wallet forward-fill */ const walletDailyBalances = {}; - selectedAddr.forEach(addr => { - walletDailyBalances[addr] = {}; - const snaps = addressSnapshots[addr] || []; + selectedWallets.forEach(w => { + const key = walletKey(w.address, w.chain); + walletDailyBalances[key] = {}; + const snaps = addressSnapshots[key] || []; const dated = snaps.map(s => ({ dateStr: s.block_timestamp.slice(0, 10), btcVal: getTotalBTC(s?.wallet) + getTotalBTC(s?.collateral) @@ -1942,18 +1958,19 @@ function setupSatsCard(cutoff) { for (const ds of sortedDates) { const match = dated.find(d => d.dateStr === ds); if (match) balance = match.btcVal; - walletDailyBalances[addr][ds] = balance; + walletDailyBalances[key][ds] = balance; } }); /* Per-wallet daily deltas */ const walletDailyDeltas = {}; - selectedAddr.forEach(addr => { - walletDailyDeltas[addr] = {}; + selectedWallets.forEach(w => { + const key = walletKey(w.address, w.chain); + walletDailyDeltas[key] = {}; let prev = 0; for (const ds of sortedDates) { - const b = walletDailyBalances[addr][ds] || 0; - walletDailyDeltas[addr][ds] = b - prev; + const b = walletDailyBalances[key][ds] || 0; + walletDailyDeltas[key][ds] = b - prev; prev = b; } }); @@ -1964,14 +1981,15 @@ function setupSatsCard(cutoff) { const daysElapsed = Math.max(1, Math.ceil((lastUTC - firstUTC) / DAY_MS)); /* Per-wallet series: running avg sats/day */ - const series = selectedAddr.map(addr => { - const w = wallets.find(x => x.address === addr); - const nickname = w ? (w.nickname || addr.slice(0, 6)) : addr.slice(0, 6); + const series = selectedWallets.map(w => { + const key = walletKey(w.address, w.chain); + const wm = wallets.find(x => x.address === w.address && x.chain === w.chain); + const nickname = wm ? (wm.nickname || w.address.slice(0, 6)) : w.address.slice(0, 6); const data = []; let runningTotalBtc = 0; for (let t = firstUTC; t <= lastUTC; t += DAY_MS) { const ds = new Date(t).toISOString().slice(0, 10); - runningTotalBtc += walletDailyDeltas[addr][ds] || 0; + runningTotalBtc += walletDailyDeltas[key][ds] || 0; const daysSoFar = Math.max(1, Math.ceil((t - firstUTC) / DAY_MS) + 1); const runningAvgSats = Math.round((runningTotalBtc * 1e8) / daysSoFar); if (t >= cutoff) { @@ -1983,13 +2001,13 @@ function setupSatsCard(cutoff) { const latestTotal = sortedDates.length > 0 ? (() => { let s = 0; - selectedAddr.forEach(addr => { s += walletDailyBalances[addr][sortedDates[sortedDates.length - 1]] || 0; }); + selectedWallets.forEach(w => { s += walletDailyBalances[walletKey(w.address, w.chain)][sortedDates[sortedDates.length - 1]] || 0; }); return s; })() : 0; const avgSatsPerDay = Math.round((latestTotal * 1e8) / daysElapsed); document.getElementById('avg-sats-val').innerText = new Intl.NumberFormat('en-US').format(avgSatsPerDay); - const colors = selectedAddr.map(addr => getColorForWallet(addr)); + const colors = selectedWallets.map(w => getColorForWallet(w.address, w.chain)); const gradientStops = colors.map(c => [ { offset: 0, color: c, opacity: 0.35 }, { offset: 100, color: c, opacity: 0 } @@ -2037,9 +2055,9 @@ function setupSatsCard(cutoff) { function setupBreakdownCard(cutoff) { /* Derive from snapshots: cold wallet vs collateral */ const allDates = {}; - const selectedAddr = getSelectedAddresses() || []; - selectedAddr.forEach(addr => { - const snaps = addressSnapshots[addr] || []; + const selectedWallets = getSelectedAddresses() || []; + selectedWallets.forEach(w => { + const snaps = addressSnapshots[walletKey(w.address, w.chain)] || []; snaps.forEach(snap => { allDates[snap.block_timestamp.slice(0, 10)] = true; }); }); const snapshotDates = Object.keys(allDates).sort(); @@ -2062,9 +2080,10 @@ function setupBreakdownCard(cutoff) { /* Pre-compute per-wallet dated arrays (oldest-first) */ const walletDated = {}; - selectedAddr.forEach(addr => { - const snaps = addressSnapshots[addr] || []; - walletDated[addr] = snaps.map(s => ({ + selectedWallets.forEach(w => { + const key = walletKey(w.address, w.chain); + const snaps = addressSnapshots[key] || []; + walletDated[key] = snaps.map(s => ({ dateStr: s.block_timestamp.slice(0, 10), cold: getTotalBTC(s?.wallet), collateral: getTotalBTC(s?.collateral) @@ -2077,8 +2096,9 @@ function setupBreakdownCard(cutoff) { for (let t = firstUTC; t <= lastUTC; t += DAY_MS) { const ds = new Date(t).toISOString().slice(0, 10); let dayCold = 0, dayColl = 0; - selectedAddr.forEach(addr => { - const dated = walletDated[addr]; + selectedWallets.forEach(w => { + const key = walletKey(w.address, w.chain); + const dated = walletDated[key]; let c = 0, co = 0; for (const d of dated) { if (d.dateStr <= ds) { c = d.cold; co = d.collateral; } @@ -2164,27 +2184,31 @@ async function pollVerifiedWallets() { if (now - lastFetchMs < 30000) return; lastFetchMs = now; const verified = wm.getVerifiedWallets() || []; + let anyUpdated = false; for (const w of verified) { try { + const key = walletKey(w.address, w.chain); const resp = await fetch(`${API_BASE}/api/v1/portfolio/${w.address}/${w.chain}/aave`); if (!resp.ok) continue; const events = await resp.json(); - const newestTs = events[events.length - 1].block_timestamp; - const existing = addressSnapshots[w.address]; + if (events.length === 0) continue; + const newestTs = events.reduce((m, e) => (e.block_timestamp > m ? e.block_timestamp : m), events[0].block_timestamp); + const existing = addressSnapshots[key]; if (existing && existing.length > 0) { if (newestTs > existing[0].block_timestamp) { - addressSnapshots[w.address] = snapshotsToDaily(events); - walletSyncState[w.address] = 'synced'; + addressSnapshots[key] = snapshotsToDaily(events); + walletSyncState[key] = 'synced'; + anyUpdated = true; } } else { - addressSnapshots[w.address] = snapshotsToDaily(events); - walletSyncState[w.address] = 'synced'; + addressSnapshots[key] = snapshotsToDaily(events); + walletSyncState[key] = 'synced'; + anyUpdated = true; } } catch (err) { - console.error("Poll update failed for " + w.address, err); + console.error("Poll update failed for " + key, err); } } - const anyUpdated = Object.keys(addressSnapshots).length > 0; if (anyUpdated) { await refreshPrices(); renderCombinedTable(); @@ -2434,10 +2458,11 @@ async function handleImportCsv(e) { const skipped = result.skipped || 0; /* Update frontend state */ + const key = walletKey(address, chain); if (result.ledger && Array.isArray(result.ledger)) { - addressSnapshots[address] = snapshotsToDaily(result.ledger); + addressSnapshots[key] = snapshotsToDaily(result.ledger); } else { - addressSnapshots[address] = snapshotsToDaily(records.map(r => ({ + addressSnapshots[key] = snapshotsToDaily(records.map(r => ({ block_timestamp: r.block_timestamp, wallet: { [r.token_address]: r.amount }, collateral: {}, @@ -2445,11 +2470,11 @@ async function handleImportCsv(e) { tx_hash: r.tx_hash || null, }))); } - walletSyncState[address] = 'synced'; + walletSyncState[key] = 'synced'; /* Assign color and mark synced */ - setColorForWallet(address, color); - walletSyncState[address] = 'synced'; + setColorForWallet(address, chain, color); + walletSyncState[key] = 'synced'; showToast( 'Import Complete', diff --git a/wallets.js b/wallets.js index 6b8eb82..958285c 100644 --- a/wallets.js +++ b/wallets.js @@ -39,7 +39,7 @@ const LS_KEY = 'tracked_wallets'; const VALID_CHAINS = new Set([ 'base', 'ethereum', 'bitcoin', 'arbitrum', 'optimism', 'polygon', 'solana', 'avalanche', 'bsc', 'fantom', 'btc', - 'hyperliquide', + 'hyperevm', ]); /* Regex patterns for address validation per chain */ @@ -53,7 +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, + hyperevm: /^(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})$/, @@ -363,17 +363,18 @@ export class WalletManager { } /** - * Reorder wallets to match the given address sequence. - * Addresses listed first are moved to front; remaining keep their original relative order. - * Only recognized addresses are kept in the new order. - * @param {string[]} addresses + * Reorder wallets to match the given compound-key sequence ("addr:chain"). + * Keys listed first are moved to front; remaining keep their original relative order. + * Also accepts plain addresses for backward compatibility. + * @param {string[]} keys */ - setWalletOrder(addresses) { - const addrSet = this._wallets.map(w => w.address); + setWalletOrder(keys) { + const keySet = this._wallets.map(w => `${w.address}:${w.chain}`); const ordered = []; - for (const addr of addresses) { - if (addrSet.includes(addr)) { - const w = this._wallets.find(x => x.address === addr); + for (const k of keys) { + if (keySet.includes(k)) { + const [addr, chain] = k.split(':'); + const w = this._wallets.find(x => x.address === addr && x.chain === chain); if (w) ordered.push(w); } }