fix: forward-fill wallet balances in chart aggregation for multi-wallet continuity
This commit is contained in:
179
index.html
179
index.html
@ -861,7 +861,21 @@ async function handleAddWallet(e) {
|
||||
closeSidebar();
|
||||
walletSyncState[address] = 'syncing';
|
||||
|
||||
await _pollUntilSynced(address);
|
||||
/* Register the address with the backend for monitoring */
|
||||
try {
|
||||
const resp = await fetch(`${API_BASE}/api/v1/portfolio/monitor`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ address, chain, chain_id: CHAIN_IDS[chain] || 8453 })
|
||||
});
|
||||
if (!resp.ok) {
|
||||
console.warn(`Backend registration returned ${resp.status} for ${address} — continuing with polling`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`Error registering wallet ${address} with backend:`, err);
|
||||
}
|
||||
|
||||
await _pollUntilSynced(address, undefined, undefined, chain);
|
||||
renderSidebar();
|
||||
renderWalletPills();
|
||||
renderLedgerFilterWallets();
|
||||
@ -934,7 +948,7 @@ async function handleVerifyWallet(address, chain, nickname) {
|
||||
const result = await verifyOwnership(address, chain, nickname || w.nickname);
|
||||
if (result.success) {
|
||||
/* Fetch data now that wallet is verified */
|
||||
if (chain === 'base') fetchWalletAaveData(address);
|
||||
fetchWalletAaveData(address, chain);
|
||||
} else {
|
||||
/* Remove unverified wallet on failure */
|
||||
wm.removeWallet(address, chain);
|
||||
@ -1047,8 +1061,9 @@ function renderLedgerFilterWallets() {
|
||||
/* ===================================================================
|
||||
Dynamic Data Fetching (Promise.all aggregation)
|
||||
=================================================================== */
|
||||
async function fetchWalletAaveData(address) {
|
||||
const endpoint = `/api/v1/portfolio/${address}/base/aave`;
|
||||
async function fetchWalletAaveData(address, chain) {
|
||||
chain = chain || 'base';
|
||||
const endpoint = `/api/v1/portfolio/${address}/${chain}/aave`;
|
||||
try {
|
||||
const resp = await fetch(`${API_BASE}${endpoint}`);
|
||||
if (resp.status === 400) {
|
||||
@ -1060,6 +1075,11 @@ async function fetchWalletAaveData(address) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (resp.status === 404) {
|
||||
walletSyncState[address] = 'pending';
|
||||
renderWalletPills();
|
||||
return;
|
||||
}
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
const events = await resp.json();
|
||||
walletSyncState[address] = 'synced';
|
||||
@ -1073,11 +1093,12 @@ async function fetchWalletAaveData(address) {
|
||||
}
|
||||
|
||||
/* Poll until wallet data is synchronized, up to ~60s */
|
||||
async function _pollUntilSynced(address, timeoutMs = 60000, intervalMs = 2000) {
|
||||
async function _pollUntilSynced(address, timeoutMs = 60000, intervalMs = 2000, chain) {
|
||||
chain = chain || 'base';
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const resp = await fetch(`${API_BASE}/api/v1/portfolio/${address}/base/aave`);
|
||||
const resp = await fetch(`${API_BASE}/api/v1/portfolio/${address}/${chain}/aave`);
|
||||
if (resp.ok) {
|
||||
const events = await resp.json();
|
||||
if (Array.isArray(events) && events.length > 0) {
|
||||
@ -1124,10 +1145,12 @@ async function fetchSelectedWalletsData() {
|
||||
/* Only fetch for wallets that are synced or syncing */
|
||||
const promises = selectedAddr.map(async (addr) => {
|
||||
const sync = walletSyncState[addr];
|
||||
if (sync === 'syncing') return null; /* Will retry later */
|
||||
if (sync === 'syncing') return null;
|
||||
if (sync === 'pending') return null;
|
||||
if (sync === 'synced' && addressSnapshots[addr]) return null; /* Cached */
|
||||
await fetchWalletAaveData(addr);
|
||||
if (sync === 'synced' && addressSnapshots[addr]) return null;
|
||||
const w = wm.getWallets().find(w => w.address === addr);
|
||||
const chain = (w || {}).chain || 'base';
|
||||
await fetchWalletAaveData(addr, chain);
|
||||
});
|
||||
await Promise.all(promises);
|
||||
}
|
||||
@ -1217,31 +1240,37 @@ function getOldestTransactionDate() {
|
||||
return oldestTs === Infinity ? Date.now() - 365 * 86400000 : oldestTs;
|
||||
}
|
||||
|
||||
/* Build per-day cumulative BTC series from addressSnapshots */
|
||||
/* Build per-day cumulative BTC series from addressSnapshots.
|
||||
Forward-fills each wallet's balance for gap days so multi-wallet
|
||||
aggregation is continuous. */
|
||||
function buildCumulativeSeries(addresses) {
|
||||
const dayMap = {};
|
||||
const allDates = {};
|
||||
addresses.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 btcAmt = getTokenAmount(snap?.wallet?.cbBTC, 'cbBTC') + getTokenAmount(snap?.collateral?.cbBTC, 'cbBTC');
|
||||
if (!dayMap[ts]) dayMap[ts] = { total: 0, dateStr };
|
||||
dayMap[ts].total += btcAmt;
|
||||
allDates[snap.block_timestamp.slice(0, 10)] = true;
|
||||
});
|
||||
});
|
||||
const sorted = Object.values(dayMap).sort((a, b) => {
|
||||
const tsA = new Date(a.dateStr).getTime();
|
||||
const tsB = new Date(b.dateStr).getTime();
|
||||
return tsA - tsB;
|
||||
});
|
||||
const byDate = new Map();
|
||||
sorted.forEach(d => {
|
||||
if (!byDate.has(d.dateStr)) {
|
||||
byDate.set(d.dateStr, d);
|
||||
const sortedDates = Object.keys(allDates).sort();
|
||||
|
||||
const dayMap = {};
|
||||
addresses.forEach(addr => {
|
||||
const snaps = addressSnapshots[addr] || [];
|
||||
const dated = snaps.map(s => ({
|
||||
dateStr: s.block_timestamp.slice(0, 10),
|
||||
btcAmt: getTokenAmount(s?.wallet?.cbBTC, 'cbBTC') + getTokenAmount(s?.collateral?.cbBTC, 'cbBTC')
|
||||
})).sort((a, b) => a.dateStr.localeCompare(b.dateStr));
|
||||
|
||||
let balance = 0;
|
||||
for (const ds of sortedDates) {
|
||||
const match = dated.find(d => d.dateStr === ds);
|
||||
if (match) balance = match.btcAmt;
|
||||
if (!dayMap[ds]) dayMap[ds] = 0;
|
||||
dayMap[ds] += balance;
|
||||
}
|
||||
});
|
||||
return Array.from(byDate.values()).map(d => [new Date(d.dateStr).getTime(), d.total]);
|
||||
|
||||
return sortedDates.map(dateStr => [new Date(dateStr).getTime(), dayMap[dateStr]]);
|
||||
}
|
||||
|
||||
function calculateAggregatedSeries() {
|
||||
@ -1487,7 +1516,7 @@ function renderCombinedTable() {
|
||||
=================================================================== */
|
||||
async function fetchAllWalletData() {
|
||||
const verifiedWallets = wm.getVerifiedWallets() || [];
|
||||
const promises = verifiedWallets.map(async (w) => fetchWalletAaveData(w.address));
|
||||
const promises = verifiedWallets.map(async (w) => fetchWalletAaveData(w.address, w.chain));
|
||||
await Promise.all(promises);
|
||||
const allSnaps = Object.values(addressSnapshots).flat();
|
||||
if (allSnaps.length > 0) {
|
||||
@ -1577,38 +1606,48 @@ function setupCumulCard(cutoff) {
|
||||
|
||||
function setupSatsCard(cutoff) {
|
||||
const DAY_MS = 86400000;
|
||||
/* Derive daily balances (latest snapshot per day) using UTC date strings */
|
||||
const dailyBalances = {};
|
||||
/* Collect all dates across selected wallets, then forward-fill each wallet's balance */
|
||||
const allDates = {};
|
||||
const selectedAddr = getSelectedAddresses() || [];
|
||||
selectedAddr.forEach(addr => {
|
||||
const snaps = addressSnapshots[addr] || [];
|
||||
snaps.forEach(snap => {
|
||||
const utcTs = new Date(snap.block_timestamp).getTime();
|
||||
const dateStr = new Date(utcTs).toISOString().slice(0, 10);
|
||||
const btcVal = getTokenAmount(snap?.wallet?.cbBTC, 'cbBTC') + getTokenAmount(snap?.collateral?.cbBTC, 'cbBTC');
|
||||
if (!dailyBalances[dateStr] || utcTs > dailyBalances[dateStr].ts) {
|
||||
dailyBalances[dateStr] = { ts: utcTs, dateStr, btcVal };
|
||||
} else {
|
||||
dailyBalances[dateStr].btcVal += btcVal;
|
||||
}
|
||||
});
|
||||
snaps.forEach(snap => { allDates[snap.block_timestamp.slice(0, 10)] = true; });
|
||||
});
|
||||
const sortedDates = Object.keys(allDates).sort();
|
||||
|
||||
/* Per-wallet: forward-fill balance across all dates */
|
||||
const dailyBalances = {};
|
||||
selectedAddr.forEach(addr => {
|
||||
const snaps = addressSnapshots[addr] || [];
|
||||
const dated = snaps.map(s => ({
|
||||
dateStr: s.block_timestamp.slice(0, 10),
|
||||
btcVal: getTokenAmount(s?.wallet?.cbBTC, 'cbBTC') + getTokenAmount(s?.collateral?.cbBTC, 'cbBTC')
|
||||
})).sort((a, b) => a.dateStr.localeCompare(b.dateStr));
|
||||
|
||||
let balance = 0;
|
||||
for (const ds of sortedDates) {
|
||||
const match = dated.find(d => d.dateStr === ds);
|
||||
if (match) balance = match.btcVal;
|
||||
if (!dailyBalances[ds]) dailyBalances[ds] = 0;
|
||||
dailyBalances[ds] += balance;
|
||||
}
|
||||
});
|
||||
|
||||
/* Sort by date, compute daily delta (acquisition) */
|
||||
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;
|
||||
let prevTotal = 0;
|
||||
sortedDates.forEach(ds => {
|
||||
dailyDeltas[ds] = dailyBalances[ds] - prevTotal;
|
||||
prevTotal = dailyBalances[ds];
|
||||
});
|
||||
|
||||
/* Compute total sats acquired and days elapsed since first snapshot */
|
||||
const firstUTC = sortedDays.length > 0 ? Date.UTC(new Date(sortedDays[0].dateStr).getUTCFullYear(), new Date(sortedDays[0].dateStr).getUTCMonth(), new Date(sortedDays[0].dateStr).getUTCDate()) : Date.now();
|
||||
const lastUTC = sortedDays.length > 0 ? Date.UTC(new Date(sortedDays[sortedDays.length - 1].dateStr).getUTCFullYear(), new Date(sortedDays[sortedDays.length - 1].dateStr).getUTCMonth(), new Date(sortedDays[sortedDays.length - 1].dateStr).getUTCDate()) : Date.now();
|
||||
const daysElapsed = Math.max(1, Math.ceil((lastUTC - firstUTC) / DAY_MS));
|
||||
const totalBtc = sortedDays[sortedDays.length - 1]?.btcVal || 0;
|
||||
const totalSats = Math.round(totalBtc * 1e8);
|
||||
const avgSatsPerDay = Math.round(totalSats / daysElapsed);
|
||||
const firstUTC = sortedDates.length > 0 ? Date.UTC(new Date(sortedDates[0]).getUTCFullYear(), new Date(sortedDates[0]).getUTCMonth(), new Date(sortedDates[0]).getUTCDate()) : Date.now();
|
||||
const lastUTC = sortedDates.length > 0 ? Date.UTC(new Date(sortedDates[sortedDates.length - 1]).getUTCFullYear(), new Date(sortedDates[sortedDates.length - 1]).getUTCMonth(), new Date(sortedDates[sortedDates.length - 1]).getUTCDate()) : Date.now();
|
||||
const daysElapsed = Math.max(1, Math.ceil((lastUTC - firstUTC) / DAY_MS));
|
||||
const totalBtc = sortedDates.length > 0 ? dailyBalances[sortedDates[sortedDates.length - 1]] : 0;
|
||||
const totalSats = Math.round(totalBtc * 1e8);
|
||||
const avgSatsPerDay = Math.round(totalSats / daysElapsed);
|
||||
|
||||
/* Build continuous daily series: one data point per day */
|
||||
const filteredSats = [];
|
||||
@ -1643,23 +1682,39 @@ function setupSatsCard(cutoff) {
|
||||
|
||||
function setupBreakdownCard(cutoff) {
|
||||
/* Derive from snapshots: cold wallet vs collateral */
|
||||
const dayMap = {};
|
||||
const allDates = {};
|
||||
const selectedAddr = getSelectedAddresses() || [];
|
||||
selectedAddr.forEach(addr => {
|
||||
const snaps = addressSnapshots[addr] || [];
|
||||
snaps.forEach(snap => {
|
||||
const ts = new Date(snap.block_timestamp).getTime();
|
||||
if (!dayMap[ts]) dayMap[ts] = { cold: 0, collateral: 0 };
|
||||
dayMap[ts].cold += getTokenAmount(snap?.wallet?.cbBTC, 'cbBTC');
|
||||
dayMap[ts].collateral += getTokenAmount(snap?.collateral?.cbBTC, 'cbBTC');
|
||||
});
|
||||
snaps.forEach(snap => { allDates[snap.block_timestamp.slice(0, 10)] = true; });
|
||||
});
|
||||
const entries = Object.keys(dayMap).map(ts => parseInt(ts)).sort((a, b) => a - b);
|
||||
const totalSeries = entries.map(ts => {
|
||||
return [ts, dayMap[ts].cold + dayMap[ts].collateral];
|
||||
const sortedDates = Object.keys(allDates).sort();
|
||||
|
||||
const dayMap = {};
|
||||
selectedAddr.forEach(addr => {
|
||||
const snaps = addressSnapshots[addr] || [];
|
||||
const dated = snaps.map(s => ({
|
||||
dateStr: s.block_timestamp.slice(0, 10),
|
||||
cold: getTokenAmount(s?.wallet?.cbBTC, 'cbBTC'),
|
||||
collateral: getTokenAmount(s?.collateral?.cbBTC, 'cbBTC')
|
||||
})).sort((a, b) => a.dateStr.localeCompare(b.dateStr));
|
||||
|
||||
let cold = 0, coll = 0;
|
||||
for (const ds of sortedDates) {
|
||||
const match = dated.find(d => d.dateStr === ds);
|
||||
if (match) { cold = match.cold; coll = match.collateral; }
|
||||
if (!dayMap[ds]) dayMap[ds] = { cold: 0, collateral: 0 };
|
||||
dayMap[ds].cold += cold;
|
||||
dayMap[ds].collateral += coll;
|
||||
}
|
||||
});
|
||||
|
||||
const entries = sortedDates;
|
||||
const totalSeries = entries.map(dateStr => {
|
||||
return [new Date(dateStr).getTime(), dayMap[dateStr].cold + dayMap[dateStr].collateral];
|
||||
}).filter(d => d[0] >= cutoff);
|
||||
const aaveSeries = entries.map(ts => {
|
||||
return [ts, dayMap[ts].collateral];
|
||||
const aaveSeries = entries.map(dateStr => {
|
||||
return [new Date(dateStr).getTime(), dayMap[dateStr].collateral];
|
||||
}).filter(d => d[0] >= cutoff);
|
||||
|
||||
const opts = {
|
||||
@ -1730,7 +1785,7 @@ document.querySelectorAll('.filter-checkbox').forEach(cb => {
|
||||
=================================================================== */
|
||||
async function pollSyncStatus() {
|
||||
const selectedAddr = getSelectedAddresses();
|
||||
const syncing = selectedAddr.filter(a => walletSyncState[a] === 'syncing');
|
||||
const syncing = selectedAddr.filter(a => walletSyncState[a] === 'syncing' || walletSyncState[a] === 'pending');
|
||||
if (syncing.length === 0) return;
|
||||
const promises = syncing.map(async (addr) => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user