diff --git a/index.html b/index.html
index ec2f668..1d5b19a 100644
--- a/index.html
+++ b/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 {