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