Fix wallet pill filters and drag-and-drop for multi-chain wallets
- Fix ReferenceError: _dragAddr → _dragKey in pill checkbox onchange handler,
restoring the ability to toggle wallet filters in the nav-bar
- Replace toggleWalletPill with setWalletChecked(key, this.checked) so pill
checkboxes directly update filter state, sync all checkboxes, and refresh
charts (matching the ledger-header checkbox behavior)
- Drag-and-drop reorder: switch from plain addresses to compound keys
("addr:chain") in dropWalletReorder and setWalletOrder, fixing clones
when dragging wallets that share an address on different chains
- Update inline fallback WalletManager with the same compound-key logic
This commit is contained in:
427
index.html
427
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'
|
||||
? `<span class="text-yellow-400 text-[10px] ml-2">⏳ Syncing</span>`
|
||||
: '';
|
||||
@ -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
|
||||
? ' <span class="text-[9px] text-yellow-400 animate-pulse">⏳</span>'
|
||||
: '';
|
||||
const nick = w.nickname || 'Wallet';
|
||||
const isActive = isWalletInLedger(w.address);
|
||||
const isActive = isWalletInLedger(w.address, w.chain);
|
||||
const platformBadge = getPlatformBadge(w.lendingPlatform);
|
||||
html += '<label class="pill-btn flex items-center gap-2 bg-[#090D14] border border-[#1A1F2C]/60 px-3 py-1.5 rounded-lg transition-all cursor-pointer select-none" data-address="' + w.address + '" draggable="true">' +
|
||||
'<input type="checkbox" class="wallet-filter-toggle" style="--wallet-color: ' + color + ';" data-address="' + w.address + '" ' + (isActive ? 'checked' : '') + ' onchange="if(!_dragAddr){event.stopPropagation();toggleWalletPill(\'' + w.address + '\')}">' +
|
||||
const key = walletKey(w.address, w.chain);
|
||||
html += '<label class="pill-btn flex items-center gap-2 bg-[#090D14] border border-[#1A1F2C]/60 px-3 py-1.5 rounded-lg transition-all cursor-pointer select-none" data-key="' + key + '" data-address="' + w.address + '" data-chain="' + w.chain + '" draggable="true">' +
|
||||
'<input type="checkbox" class="wallet-filter-toggle" style="--wallet-color: ' + color + ';" data-key="' + key + '" ' + (isActive ? 'checked' : '') + ' onchange="if(!_dragKey){event.stopPropagation();setWalletChecked(\'' + key + '\', this.checked)}">' +
|
||||
'<div class="flex flex-col">' +
|
||||
'<span class="text-[10px] font-bold text-gray-400 uppercase tracking-wider leading-tight">' + nick + syncIndicator + platformBadge + '</span>' +
|
||||
'<span class="text-[8px] font-medium text-gray-600 uppercase tracking-tighter leading-tight">' + w.chain + '</span>' +
|
||||
@ -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 += `<label class="filter-item" title="Show ${w.nickname || w.address}">
|
||||
<input type="checkbox" class="wallet-filter-toggle" style="--wallet-color: ${color};" data-address="${w.address}" ${checked ? 'checked' : ''}>
|
||||
<input type="checkbox" class="wallet-filter-toggle" style="--wallet-color: ${color};" data-key="${key}" ${checked ? 'checked' : ''}>
|
||||
</label>`;
|
||||
});
|
||||
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',
|
||||
|
||||
23
wallets.js
23
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user