fix: 30-day rolling average for Avg BTC acquired/day chart with continuous daily timeline

This commit is contained in:
Dione
2026-06-10 18:52:03 +00:00
parent d9e0c1b89c
commit 0ad8f03990

View File

@ -47,6 +47,11 @@ window.WalletManager = WalletManager;
.wallet-filter-toggle { appearance: none; width: 12px; height: 12px; border-radius: 2px; border: 1px solid rgba(255,255,255,0.2); cursor: pointer; position: relative; transition: all 0.2s; }
.wallet-filter-toggle:checked { background: var(--wallet-color); border-color: transparent; box-shadow: 0 0 8px color-mix(in srgb, var(--wallet-color) 40%, transparent); }
.platform-checkbox { appearance: none; width: 14px; height: 14px; border-radius: 3px; border: 1.5px solid rgba(255,255,255,0.15); cursor: pointer; position: relative; transition: all 0.2s; flex-shrink: 0; }
.platform-checkbox:checked { border-color: transparent; }
.platform-checkbox.platform-aave:checked { background: #9896FF; box-shadow: 0 0 10px rgba(152, 150, 255, 0.5); }
.platform-checkbox.platform-hyperlend:checked { background: #caeae5; box-shadow: 0 0 10px rgba(202, 234, 229, 0.5); }
.wallet-color-dot {
width: 16px; height: 16px; border-radius: 50%; cursor: pointer;
flex-shrink: 0; transition: transform 0.15s, box-shadow 0.15s;
@ -143,6 +148,10 @@ window.WalletManager = WalletManager;
<input id="input-chain-display" type="text" placeholder="—" class="w-full bg-[#05070B] border border-[#1A1F2C] rounded-lg px-3 py-2 text-white text-sm placeholder-gray-600 transition" readonly>
<input id="input-chain-hidden" type="hidden" value="base">
</div>
<div class="mb-4">
<label class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2">Lending Platform</label>
<div id="lending-platform-selector" class="flex flex-col gap-2"></div>
</div>
<div class="mb-4">
<label class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-1">Nickname</label>
<input id="input-nickname" type="text" placeholder="Whale Wallet A" class="w-full bg-[#05070B] border border-[#1A1F2C] rounded-lg px-3 py-2 text-white text-sm placeholder-gray-600 focus:outline-none focus:border-[#FF7A00]/50 transition" required>
@ -624,6 +633,8 @@ function handleRenameNickname(input) {
Add Wallet Modal
=================================================================== */
let _addWalletSelectedColor = null;
let _addWalletSelectedPlatform = 'aave';
const HYPERLIQUIDE_CHAINS = ['hyperliquide'];
function openAddWalletModal() {
if (!window.ethereum) {
@ -640,13 +651,17 @@ function openAddWalletModal() {
document.getElementById('input-chain-display').value = '';
document.getElementById('input-chain-hidden').value = 'base';
_addWalletSelectedColor = autoPickColor();
_addWalletSelectedPlatform = 'aave';
renderAddWalletColorPicker();
window.ethereum.request({ method: 'eth_chainId' })
.then(chainIdHex => {
const chainName = CHAIN_IDS_REVERSED[chainIdHex] || 'base';
document.getElementById('input-chain-display').value = chainName.charAt(0).toUpperCase() + chainName.slice(1);
document.getElementById('input-chain-hidden').value = chainName;
.then(rawChainId => {
const chainIdDecimal = parseInt(rawChainId, 16).toString();
const chainName = CHAIN_IDS_REVERSED[chainIdDecimal];
const displayText = chainName ? chainName.charAt(0).toUpperCase() + chainName.slice(1) : `Unknown (${chainIdDecimal})`;
document.getElementById('input-chain-display').value = displayText;
document.getElementById('input-chain-hidden').value = chainName || 'unknown';
renderLendingPlatformSelector(chainName || null);
});
document.getElementById('add-wallet-error').classList.add('hidden');
@ -682,8 +697,47 @@ function renderAddWalletColorPicker() {
});
}
/* Reverse map: chainId → chain name */
const CHAIN_IDS_REVERSED = { '1': 'ethereum', '8453': 'base', '42161': 'arbitrum', '10': 'optimism', '137': 'polygon', '43114': 'avalanche', '56': 'bsc', '250': 'fantom', '998': 'hyperliquide' };
function renderLendingPlatformSelector(chainName) {
const container = document.getElementById('lending-platform-selector');
container.innerHTML = '';
const isHyperliquide = HYPERLIQUIDE_CHAINS.includes(chainName);
if (isHyperliquide) {
const platform = 'hyperlend';
const logoUrl = 'https://app.hyperlend.finance/assets/header-logo-CiRKYBzy.svg';
const color = '#caeae5';
const label = 'HyperLend';
const cbClass = 'platform-hyperlend';
const item = document.createElement('label');
item.className = 'flex items-center gap-3 bg-[#05070B] border border-[#1A1F2C] rounded-lg px-3 py-2 cursor-pointer transition';
item.innerHTML =
'<input type="checkbox" class="platform-checkbox ' + cbClass + '" checked disabled>' +
'<img src="' + logoUrl + '" alt="' + label + '" class="h-4 w-auto" onerror="this.outerHTML=\'' + label + '\'">' +
'<span class="text-xs font-medium text-gray-300">' + label + '</span>';
container.appendChild(item);
_addWalletSelectedPlatform = 'hyperlend';
} else {
const platform = 'aave';
const logoUrl = 'https://app.aave.com/aave-com-logo-header.svg';
const color = '#9896FF';
const label = 'Aave';
const cbClass = 'platform-aave';
const item = document.createElement('label');
item.className = 'flex items-center gap-3 bg-[#05070B] border border-[#1A1F2C] rounded-lg px-3 py-2 cursor-pointer transition';
item.innerHTML =
'<input type="checkbox" class="platform-checkbox ' + cbClass + '" checked disabled>' +
'<img src="' + logoUrl + '" alt="' + label + '" class="h-4 w-auto" onerror="this.outerHTML=\'' + label + '\'">' +
'<span class="text-xs font-medium text-gray-300">' + label + '</span>';
container.appendChild(item);
_addWalletSelectedPlatform = 'aave';
}
}
/* 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' };
function fetchWalletAddress() {
if (!window.ethereum) {
@ -699,14 +753,18 @@ function fetchWalletAddress() {
/* Auto-detect chain from wallet */
window.ethereum.request({ method: 'eth_chainId' })
.then(chainIdHex => {
const chainName = CHAIN_IDS_REVERSED[chainIdHex] || 'base';
document.getElementById('input-chain-display').value = chainName.charAt(0).toUpperCase() + chainName.slice(1);
document.getElementById('input-chain-hidden').value = chainName;
.then(rawChainId => {
const chainIdDecimal = parseInt(rawChainId, 16).toString();
const chainName = CHAIN_IDS_REVERSED[chainIdDecimal];
const displayText = chainName ? chainName.charAt(0).toUpperCase() + chainName.slice(1) : `Unknown (${chainIdDecimal})`;
document.getElementById('input-chain-display').value = displayText;
document.getElementById('input-chain-hidden').value = chainName || 'unknown';
renderLendingPlatformSelector(chainName || null);
});
/* Auto-assign a color not used by existing wallets */
_addWalletSelectedColor = autoPickColor();
_addWalletSelectedPlatform = 'aave';
renderAddWalletColorPicker();
}
})
@ -725,6 +783,7 @@ async function handleAddWallet(e) {
let address = document.getElementById('input-address').value.trim().toLowerCase();
let chain = document.getElementById('input-chain-hidden').value || 'base';
const nickname = document.getElementById('input-nickname').value.trim();
const lendingPlatform = _addWalletSelectedPlatform || 'aave';
const errEl = document.getElementById('add-wallet-error');
/* If no address, connect wallet first */
@ -746,9 +805,12 @@ async function handleAddWallet(e) {
/* Auto-detect chain */
const chainIdHex = await window.ethereum.request({ method: 'eth_chainId' });
chain = CHAIN_IDS_REVERSED[chainIdHex] || 'base';
const chainIdDecimal = parseInt(chainIdHex, 16).toString();
const resolvedChain = CHAIN_IDS_REVERSED[chainIdDecimal];
chain = resolvedChain || 'unknown';
document.getElementById('input-chain-hidden').value = chain;
document.getElementById('input-chain-display').value = chain.charAt(0).toUpperCase() + chain.slice(1);
document.getElementById('input-chain-display').value = resolvedChain ? resolvedChain.charAt(0).toUpperCase() + resolvedChain.slice(1) : `Unknown (${chainIdDecimal})`;
renderLendingPlatformSelector(resolvedChain || null);
_addWalletSelectedColor = autoPickColor();
renderAddWalletColorPicker();
@ -772,7 +834,7 @@ async function handleAddWallet(e) {
/* Auto-verify non-EVM wallets */
if (!isEVM) {
const autoChain = isSolana ? 'solana' : 'btc';
const addResult = wm.addWallet(address, autoChain, nickname);
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';
@ -783,7 +845,7 @@ async function handleAddWallet(e) {
}
/* Register EVM wallet first so verifyOwnership can find it */
const addResult = wm.addWallet(address, chain, nickname);
const addResult = wm.addWallet(address, chain, nickname, lendingPlatform);
if (!addResult.success) { errEl.textContent = addResult.error; errEl.classList.remove('hidden'); return false; }
/* One-shot: verify or remove */
@ -811,7 +873,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: 998 };
const CHAIN_IDS = { ethereum: 1, base: 8453, arbitrum: 42161, optimism: 10, polygon: 137, avalanche: 43114, bsc: 56, fantom: 250, hyperliquide: 999 };
const VERIFY_TYPES = {
VerifyTracking: [
{ name: 'action', type: 'string' },
@ -903,6 +965,15 @@ function handleDeleteWallet(address, chain, idx) {
/* ===================================================================
Wallet Pills (main dashboard selection)
=================================================================== */
function getPlatformBadge(platform) {
if (!platform) return '';
const p = (platform || 'aave').toLowerCase();
if (p === 'hyperlend') {
return '<span class="inline-block w-2 h-2 rounded-sm bg-[#caeae5] ml-1"></span>';
}
return '<span class="inline-block w-2 h-2 rounded-sm bg-[#9896FF] ml-1"></span>';
}
function renderWalletPills() {
const wallets = wm.getWallets();
const container = document.getElementById('wallet-pills');
@ -920,10 +991,11 @@ function renderWalletPills() {
: '';
const nick = w.nickname || 'Wallet';
const isActive = isWalletInLedger(w.address);
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 + '">' +
'<input type="checkbox" class="wallet-filter-toggle" style="--wallet-color: ' + color + ';" data-address="' + w.address + '" ' + (isActive ? 'checked' : '') + ' onchange="event.stopPropagation();toggleWalletPill(\'' + w.address + '\')">' +
'<div class="flex flex-col">' +
'<span class="text-[10px] font-bold text-gray-400 uppercase tracking-wider leading-tight">' + nick + syncIndicator + '</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>' +
'</div>' +
'</label>';
@ -1498,22 +1570,50 @@ function setupCumulCard(cutoff) {
}
function setupSatsCard(cutoff) {
/* Derive daily BTC from all verified wallet snapshots */
const dailyBtc = {};
const DAY_MS = 86400000;
/* Derive daily balances (latest snapshot per day) */
const dailyBalances = {};
const selectedAddr = getSelectedAddresses() || [];
selectedAddr.forEach(addr => {
const snaps = addressSnapshots[addr] || [];
snaps.forEach(snap => {
const ts = new Date(snap.block_timestamp).getTime();
const dateStr = snap.block_timestamp.slice(0, 10);
const btcVal = getTokenAmount(snap?.wallet?.cbBTC, 'cbBTC') + getTokenAmount(snap?.collateral?.cbBTC, 'cbBTC');
if (!dailyBtc[ts]) dailyBtc[ts] = { ts, btcVal: 0, dateStr };
dailyBtc[ts].btcVal += btcVal;
const ts = new Date(snap.block_timestamp).getTime();
if (!dailyBalances[dateStr] || ts > dailyBalances[dateStr].ts) {
dailyBalances[dateStr] = { ts, dateStr, btcVal };
} else {
dailyBalances[dateStr].btcVal += btcVal;
}
});
});
const dailyEntries = Object.values(dailyBtc).sort((a, b) => a.ts - b.ts);
const filtered = dailyEntries.filter(d => d.ts >= cutoff);
const rollingAvg = filtered.map(d => [d.ts, Math.round(d.btcVal * 1e8)]);
/* Sort by date, compute daily delta (acquisition), then fill continuous timeline */
const sortedDays = Object.values(dailyBalances).sort((a, b) => a.dateStr.localeCompare(b.dateStr));
const dailyDeltas = {};
sortedDays.forEach((d, i) => {
const prevBalance = i > 0 ? sortedDays[i - 1].btcVal : 0;
dailyDeltas[d.dateStr] = d.btcVal - prevBalance;
});
/* Fill continuous daily timeline so no days are skipped */
const firstDate = new Date(sortedDays[0].dateStr).getTime();
const lastDate = new Date(sortedDays[sortedDays.length - 1].dateStr).getTime();
const continuousDays = [];
for (let t = firstDate; t <= lastDate; t += DAY_MS) {
const ds = new Date(t).toISOString().slice(0, 10);
continuousDays.push({ ts: t, dateStr: ds, delta: dailyDeltas[ds] || 0 });
}
/* 30-day rolling average of daily deltas */
const filtered = continuousDays.filter(d => d.ts >= cutoff);
const rollingAvg = [];
for (let i = 0; i < filtered.length; i++) {
const window = filtered.slice(Math.max(0, i - 29), i + 1);
const avgSats = Math.round(window.reduce((s, d) => s + d.delta, 0) / window.length * 1e8);
rollingAvg.push([filtered[i].ts, avgSats]);
}
const latestAvg = rollingAvg.length > 0 ? rollingAvg[rollingAvg.length - 1][1] : 0;
document.getElementById('avg-sats-val').innerText = new Intl.NumberFormat('en-US').format(Math.round(latestAvg));
const options = {