snapshotsToDaily returns results sorted newest-first, so allSnaps[0] was the newest timestamp. This caused fetchPrices to compute a near-zero date range, resulting in $0 values for all but the most recent rows. Also removed redundant refreshPrices() call from init flow (fetchAllWalletData already fetches full-range prices). Added table data flow doc to AGENTS.md.
7.3 KiB
cbBTC Treasury Dashboard
What it is: A single-page HTML dashboard tracking cbBTC acquisition across wallets. No build step, no JS framework, no tests — just one index.html served by nginx.
How to run
docker build -t cbBTC-dashboard . && docker run -p 8080:80 cbBTC-dashboard
Or just open index.html in a browser (without the /api/ proxy, only the Kraken BTC price chart will work).
Architecture
The codebase is split into three vanilla ESM modules plus the single-page dashboard:
| Module | Purpose |
|---|---|
wallets.js |
WalletManager — localStorage state, chain-specific address validation, verification lifecycle |
verifier.js |
WalletVerifier — EIP-712 typed-data signing via window.ethereum, user rejection handling |
stream.js |
WalletStreamManager — single WebSocket with BroadcastChannel leader election, exponential backoff + jitter |
File layout
| File | Purpose |
|---|---|
index.html |
Dashboard UI. Embedded chart data, ApexCharts, TailwindCSS. Loads modules via <script type="module">. |
wallets.js |
WalletManager — anonymous localStorage wallet state, add/remove/verify/revoke |
verifier.js |
WalletVerifier — window.ethereum connection + EIP-712 signature flow |
stream.js |
WalletStreamManager — single WS, cross-tab leader election, event routing |
Dockerfile |
4 lines. Copies index.html and default.conf into nginx:alpine. |
default.conf |
Reverse proxy: /api/ → http://192.168.1.102:8000/api/. |
aave_portfolio.md / wallet_portfolio.md |
Reference data dumps. NOT consumed by the app. |
Key facts for editing index.html
- Data is embedded as JS constants near the top of the
<script>block:walletCumulData— time-series of cumulative BTC per walletdailySatsData— rolling sats/day with price infowalletsMetadata— wallet id → address, name, color, chainwalletCosts/walletBuys— cost basis per wallet
- API endpoints (proxied through nginx):
GET /api/v1/prices/{symbol}/history?range=N— price historyGET /api/v1/portfolio/{address}/base/aave— Aave portfolio snapshots
- BTC price fetched directly from Kraken:
https://api.kraken.com/0/public/OHLC?pair=XBTUSD&interval=1440 - Polling:
setIntervalevery 30s with a 60s throttle on the Aave endpoint - Tracked wallet:
0x0c1a4a060e119f981412e323104d1c134d413dba("penguin", Base) - Token decimals: cbBTC=8, WETH=18, USDC=6
Module API
Each module is loaded via <script type="module"> in index.html:
import { WalletManager } from './wallets.js';
import { WalletVerifier } from './verifier.js';
import { WalletStreamManager } from './stream.js';
const wm = new WalletManager();
wm.loadWallets();
const verifier = new WalletVerifier(wm);
const stream = new WalletStreamManager(wm);
stream.connect();
WalletManager
| Method | Description |
|---|---|
loadWallets() |
Reads from localStorage, sanitizes (always isVerified: false) |
addWallet(address, chain, nickname) |
Validates format, appends, persists |
removeWallet(address, chain) |
Drops from state, persists |
verifyWallet(address, chain, signature, messageData) |
Sets isVerified: true |
revokeVerification(address, chain) |
Resets to unverified |
findWallet(address, chain) |
Lookup helper |
getWallets() |
Returns deep copy of state |
WalletVerifier
| Method | Description |
|---|---|
triggerWalletVerification(chain, nickname) |
Full flow: eth_requestAccounts → EIP-712 sign → add + verify |
connect() |
Quick connect check (no signature) |
EIP-712 domain: { name: "Anonymous Wallet Tracker", version: "1", chainId: 1 }
WalletStreamManager
| Method | Description |
|---|---|
connect() |
Triggers BroadcastChannel leader election → opens single WS |
disconnect() |
Closes WS + BC, cancels timers |
on(event, callback) |
Register callback per event type |
subscribeToNewWallet(wallet) |
Dynamic subscribe (leader → WS, follower → BC) |
unsubscribeFromWallet(address, chain) |
Dynamic unsubscribe |
Cross-tab leader election uses BroadcastChannel("dione_shared_stream") with a 200ms timeout. Leader-death broadcast triggers failover election. Reconnect uses exponential backoff (1s → 30s) with ±25% jitter.
Gotchas
apiBase = window.location.origin— the dashboard relies on nginx proxy to reach the backend at192.168.1.102:8000. Running without Docker/proxy means the Aave table and price charts fail silently.- No linting, no type-checking, no CI. Validate changes by opening the HTML in a browser.
- Adding chart data? Update the embedded arrays (
walletCumulData, etc.) — they are plain JS arrays of[timestampMs, value]pairs. wallets.jsalways forcesisVerified: falseonloadWallets()— persisted signatures are discarded. Verification must go throughWalletVerifier.
Transaction Ledger Table — Data Flow
How the table gets its rows and values:
- Fetch:
fetchAllWalletData()→fetchWalletAaveData(address)hits/api/v1/portfolio/{address}/base/aave, returns array of snapshot events. - Deduplicate:
snapshotsToDaily(events)collapses events to one per day (keeps latestblock_timestampper day). Result is sorted newest-first. - Store: Deduplicated snapshots land in
addressSnapshots[address]. - Prices:
fetchPrices(symbols, oldestDateMs)fetches/api/v1/prices/{symbol}/history?range=Nwhere N = days between now and oldest snapshot. Critical: you must scan all snapshots for the lowestblock_timestampto compute the correct range.snapshots[0]is the newest — never assume the array is oldest-first. TheaavePriceMapis keyed[priceSymbol][dateStr] = closePrice. - Render:
renderCombinedTable()iteratesaddressSnapshots, computes row values:getTokenAmount(raw, symbol)divides by token decimals (cbBTC /1e8, WETH /1e18, USDC /1e6)priceForToken(symbol, dateStr)looks upaavePriceMapfor the matching date (falls back to nearest prior date)- Each row shows USD =
amount × pricefor wallet (cold), collateral, and debt columns
- Filter:
updateDashboard()hides/shows rows based on wallet-type and ledger-wallet checkbox filters.
If table rows show $0 for non-recent dates, check that aavePriceMap covers the full historical range — the fetchPrices() call must use the oldest snapshot timestamp, not the newest.
Coding Guidelines
You are an expert senior Web3 frontend engineer specializing in high-scale performance and stateless cryptographic security.
We are building the client-side code for an anonymous multi-wallet tracker. The application handles up to 100k concurrent connections, so code efficiency, memory management, and avoiding redundant network overhead are paramount.
- Modern TypeScript/JavaScript — clean, modular, and well-commented.
- No bloated third-party UI frameworks — stick to vanilla methods or standard state hooks.
- Production-ready error handling — bulletproof handling for wallet rejections and network drops.
After every index.html edit
Always rebuild and restart the Docker container so changes take effect:
docker build -t cbbtc-dashboard . && docker stop dione-dashboard && docker rm dione-dashboard && docker run -d --name dione-dashboard -p 8080:80 cbbtc-dashboard