Files
dione/AGENTS.md
Dione e28e66b29f fix: scan all snapshots for oldest timestamp in fetchAllWalletData
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.
2026-06-10 19:52:11 +00:00

7.3 KiB
Raw Blame History

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 WalletVerifierwindow.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 wallet
    • dailySatsData — rolling sats/day with price info
    • walletsMetadata — wallet id → address, name, color, chain
    • walletCosts / walletBuys — cost basis per wallet
  • API endpoints (proxied through nginx):
    • GET /api/v1/prices/{symbol}/history?range=N — price history
    • GET /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: setInterval every 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 at 192.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.js always forces isVerified: false on loadWallets() — persisted signatures are discarded. Verification must go through WalletVerifier.

Transaction Ledger Table — Data Flow

How the table gets its rows and values:

  1. Fetch: fetchAllWalletData()fetchWalletAaveData(address) hits /api/v1/portfolio/{address}/base/aave, returns array of snapshot events.
  2. Deduplicate: snapshotsToDaily(events) collapses events to one per day (keeps latest block_timestamp per day). Result is sorted newest-first.
  3. Store: Deduplicated snapshots land in addressSnapshots[address].
  4. Prices: fetchPrices(symbols, oldestDateMs) fetches /api/v1/prices/{symbol}/history?range=N where N = days between now and oldest snapshot. Critical: you must scan all snapshots for the lowest block_timestamp to compute the correct range. snapshots[0] is the newest — never assume the array is oldest-first. The aavePriceMap is keyed [priceSymbol][dateStr] = closePrice.
  5. Render: renderCombinedTable() iterates addressSnapshots, computes row values:
    • getTokenAmount(raw, symbol) divides by token decimals (cbBTC /1e8, WETH /1e18, USDC /1e6)
    • priceForToken(symbol, dateStr) looks up aavePriceMap for the matching date (falls back to nearest prior date)
    • Each row shows USD = amount × price for wallet (cold), collateral, and debt columns
  6. 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.

  1. Modern TypeScript/JavaScript — clean, modular, and well-commented.
  2. No bloated third-party UI frameworks — stick to vanilla methods or standard state hooks.
  3. 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