fix: 30-day rolling average for Avg BTC acquired/day chart with continuous daily timeline
This commit is contained in:
148
index.html
148
index.html
@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user