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();
|
closeSidebar();
|
||||||
walletSyncState[address] = 'syncing';
|
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();
|
renderSidebar();
|
||||||
renderWalletPills();
|
renderWalletPills();
|
||||||
renderLedgerFilterWallets();
|
renderLedgerFilterWallets();
|
||||||
@ -934,7 +948,7 @@ async function handleVerifyWallet(address, chain, nickname) {
|
|||||||
const result = await verifyOwnership(address, chain, nickname || w.nickname);
|
const result = await verifyOwnership(address, chain, nickname || w.nickname);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
/* Fetch data now that wallet is verified */
|
/* Fetch data now that wallet is verified */
|
||||||
if (chain === 'base') fetchWalletAaveData(address);
|
fetchWalletAaveData(address, chain);
|
||||||
} else {
|
} else {
|
||||||
/* Remove unverified wallet on failure */
|
/* Remove unverified wallet on failure */
|
||||||
wm.removeWallet(address, chain);
|
wm.removeWallet(address, chain);
|
||||||
@ -1047,8 +1061,9 @@ function renderLedgerFilterWallets() {
|
|||||||
/* ===================================================================
|
/* ===================================================================
|
||||||
Dynamic Data Fetching (Promise.all aggregation)
|
Dynamic Data Fetching (Promise.all aggregation)
|
||||||
=================================================================== */
|
=================================================================== */
|
||||||
async function fetchWalletAaveData(address) {
|
async function fetchWalletAaveData(address, chain) {
|
||||||
const endpoint = `/api/v1/portfolio/${address}/base/aave`;
|
chain = chain || 'base';
|
||||||
|
const endpoint = `/api/v1/portfolio/${address}/${chain}/aave`;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`${API_BASE}${endpoint}`);
|
const resp = await fetch(`${API_BASE}${endpoint}`);
|
||||||
if (resp.status === 400) {
|
if (resp.status === 400) {
|
||||||
@ -1060,6 +1075,11 @@ async function fetchWalletAaveData(address) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (resp.status === 404) {
|
||||||
|
walletSyncState[address] = 'pending';
|
||||||
|
renderWalletPills();
|
||||||
|
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[address] = 'synced';
|
||||||
@ -1073,11 +1093,12 @@ async function fetchWalletAaveData(address) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 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) {
|
async function _pollUntilSynced(address, timeoutMs = 60000, intervalMs = 2000, chain) {
|
||||||
|
chain = chain || 'base';
|
||||||
const deadline = Date.now() + timeoutMs;
|
const deadline = Date.now() + timeoutMs;
|
||||||
while (Date.now() < deadline) {
|
while (Date.now() < deadline) {
|
||||||
try {
|
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) {
|
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) {
|
||||||
@ -1124,10 +1145,12 @@ async function fetchSelectedWalletsData() {
|
|||||||
/* 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 = selectedAddr.map(async (addr) => {
|
||||||
const sync = walletSyncState[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 === 'pending') return null;
|
||||||
if (sync === 'synced' && addressSnapshots[addr]) return null; /* Cached */
|
if (sync === 'synced' && addressSnapshots[addr]) return null;
|
||||||
await fetchWalletAaveData(addr);
|
const w = wm.getWallets().find(w => w.address === addr);
|
||||||
|
const chain = (w || {}).chain || 'base';
|
||||||
|
await fetchWalletAaveData(addr, chain);
|
||||||
});
|
});
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
}
|
}
|
||||||
@ -1217,31 +1240,37 @@ function getOldestTransactionDate() {
|
|||||||
return oldestTs === Infinity ? Date.now() - 365 * 86400000 : oldestTs;
|
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) {
|
function buildCumulativeSeries(addresses) {
|
||||||
const dayMap = {};
|
const allDates = {};
|
||||||
addresses.forEach(addr => {
|
addresses.forEach(addr => {
|
||||||
const snaps = addressSnapshots[addr] || [];
|
const snaps = addressSnapshots[addr] || [];
|
||||||
snaps.forEach(snap => {
|
snaps.forEach(snap => {
|
||||||
const ts = new Date(snap.block_timestamp).getTime();
|
allDates[snap.block_timestamp.slice(0, 10)] = true;
|
||||||
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;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
const sorted = Object.values(dayMap).sort((a, b) => {
|
const sortedDates = Object.keys(allDates).sort();
|
||||||
const tsA = new Date(a.dateStr).getTime();
|
|
||||||
const tsB = new Date(b.dateStr).getTime();
|
const dayMap = {};
|
||||||
return tsA - tsB;
|
addresses.forEach(addr => {
|
||||||
});
|
const snaps = addressSnapshots[addr] || [];
|
||||||
const byDate = new Map();
|
const dated = snaps.map(s => ({
|
||||||
sorted.forEach(d => {
|
dateStr: s.block_timestamp.slice(0, 10),
|
||||||
if (!byDate.has(d.dateStr)) {
|
btcAmt: getTokenAmount(s?.wallet?.cbBTC, 'cbBTC') + getTokenAmount(s?.collateral?.cbBTC, 'cbBTC')
|
||||||
byDate.set(d.dateStr, d);
|
})).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() {
|
function calculateAggregatedSeries() {
|
||||||
@ -1487,7 +1516,7 @@ function renderCombinedTable() {
|
|||||||
=================================================================== */
|
=================================================================== */
|
||||||
async function fetchAllWalletData() {
|
async function fetchAllWalletData() {
|
||||||
const verifiedWallets = wm.getVerifiedWallets() || [];
|
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);
|
await Promise.all(promises);
|
||||||
const allSnaps = Object.values(addressSnapshots).flat();
|
const allSnaps = Object.values(addressSnapshots).flat();
|
||||||
if (allSnaps.length > 0) {
|
if (allSnaps.length > 0) {
|
||||||
@ -1577,38 +1606,48 @@ function setupCumulCard(cutoff) {
|
|||||||
|
|
||||||
function setupSatsCard(cutoff) {
|
function setupSatsCard(cutoff) {
|
||||||
const DAY_MS = 86400000;
|
const DAY_MS = 86400000;
|
||||||
/* Derive daily balances (latest snapshot per day) using UTC date strings */
|
/* Collect all dates across selected wallets, then forward-fill each wallet's balance */
|
||||||
const dailyBalances = {};
|
const allDates = {};
|
||||||
const selectedAddr = getSelectedAddresses() || [];
|
const selectedAddr = getSelectedAddresses() || [];
|
||||||
selectedAddr.forEach(addr => {
|
selectedAddr.forEach(addr => {
|
||||||
const snaps = addressSnapshots[addr] || [];
|
const snaps = addressSnapshots[addr] || [];
|
||||||
snaps.forEach(snap => {
|
snaps.forEach(snap => { allDates[snap.block_timestamp.slice(0, 10)] = true; });
|
||||||
const utcTs = new Date(snap.block_timestamp).getTime();
|
});
|
||||||
const dateStr = new Date(utcTs).toISOString().slice(0, 10);
|
const sortedDates = Object.keys(allDates).sort();
|
||||||
const btcVal = getTokenAmount(snap?.wallet?.cbBTC, 'cbBTC') + getTokenAmount(snap?.collateral?.cbBTC, 'cbBTC');
|
|
||||||
if (!dailyBalances[dateStr] || utcTs > dailyBalances[dateStr].ts) {
|
/* Per-wallet: forward-fill balance across all dates */
|
||||||
dailyBalances[dateStr] = { ts: utcTs, dateStr, btcVal };
|
const dailyBalances = {};
|
||||||
} else {
|
selectedAddr.forEach(addr => {
|
||||||
dailyBalances[dateStr].btcVal += btcVal;
|
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) */
|
/* Sort by date, compute daily delta (acquisition) */
|
||||||
const sortedDays = Object.values(dailyBalances).sort((a, b) => a.dateStr.localeCompare(b.dateStr));
|
|
||||||
const dailyDeltas = {};
|
const dailyDeltas = {};
|
||||||
sortedDays.forEach((d, i) => {
|
let prevTotal = 0;
|
||||||
const prevBalance = i > 0 ? sortedDays[i - 1].btcVal : 0;
|
sortedDates.forEach(ds => {
|
||||||
dailyDeltas[d.dateStr] = d.btcVal - prevBalance;
|
dailyDeltas[ds] = dailyBalances[ds] - prevTotal;
|
||||||
|
prevTotal = dailyBalances[ds];
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Compute total sats acquired and days elapsed since first snapshot */
|
/* 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 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 = 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 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 daysElapsed = Math.max(1, Math.ceil((lastUTC - firstUTC) / DAY_MS));
|
||||||
const totalBtc = sortedDays[sortedDays.length - 1]?.btcVal || 0;
|
const totalBtc = sortedDates.length > 0 ? dailyBalances[sortedDates[sortedDates.length - 1]] : 0;
|
||||||
const totalSats = Math.round(totalBtc * 1e8);
|
const totalSats = Math.round(totalBtc * 1e8);
|
||||||
const avgSatsPerDay = Math.round(totalSats / daysElapsed);
|
const avgSatsPerDay = Math.round(totalSats / daysElapsed);
|
||||||
|
|
||||||
/* Build continuous daily series: one data point per day */
|
/* Build continuous daily series: one data point per day */
|
||||||
const filteredSats = [];
|
const filteredSats = [];
|
||||||
@ -1643,23 +1682,39 @@ function setupSatsCard(cutoff) {
|
|||||||
|
|
||||||
function setupBreakdownCard(cutoff) {
|
function setupBreakdownCard(cutoff) {
|
||||||
/* Derive from snapshots: cold wallet vs collateral */
|
/* Derive from snapshots: cold wallet vs collateral */
|
||||||
const dayMap = {};
|
const allDates = {};
|
||||||
const selectedAddr = getSelectedAddresses() || [];
|
const selectedAddr = getSelectedAddresses() || [];
|
||||||
selectedAddr.forEach(addr => {
|
selectedAddr.forEach(addr => {
|
||||||
const snaps = addressSnapshots[addr] || [];
|
const snaps = addressSnapshots[addr] || [];
|
||||||
snaps.forEach(snap => {
|
snaps.forEach(snap => { allDates[snap.block_timestamp.slice(0, 10)] = true; });
|
||||||
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');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
const entries = Object.keys(dayMap).map(ts => parseInt(ts)).sort((a, b) => a - b);
|
const sortedDates = Object.keys(allDates).sort();
|
||||||
const totalSeries = entries.map(ts => {
|
|
||||||
return [ts, dayMap[ts].cold + dayMap[ts].collateral];
|
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);
|
}).filter(d => d[0] >= cutoff);
|
||||||
const aaveSeries = entries.map(ts => {
|
const aaveSeries = entries.map(dateStr => {
|
||||||
return [ts, dayMap[ts].collateral];
|
return [new Date(dateStr).getTime(), dayMap[dateStr].collateral];
|
||||||
}).filter(d => d[0] >= cutoff);
|
}).filter(d => d[0] >= cutoff);
|
||||||
|
|
||||||
const opts = {
|
const opts = {
|
||||||
@ -1730,7 +1785,7 @@ document.querySelectorAll('.filter-checkbox').forEach(cb => {
|
|||||||
=================================================================== */
|
=================================================================== */
|
||||||
async function pollSyncStatus() {
|
async function pollSyncStatus() {
|
||||||
const selectedAddr = getSelectedAddresses();
|
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;
|
if (syncing.length === 0) return;
|
||||||
const promises = syncing.map(async (addr) => {
|
const promises = syncing.map(async (addr) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user