Files
dione/AGENTS.md

8.2 KiB
Raw Permalink 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.

Remote Git

Repository: https://git.kapuscinski.pl/p1otek/dione.git

Credentials are stored in ~/.config/opencode/.env:

GIT_USERNAME=p1otek
GIT_TOKEN=<PAT>
GIT_ADDRESS=https://git.kapuscinski.pl/

All git push/pull commands must use these credentials — do not hardcode the token in scripts or commands. Use the token via environment variable GIT_TOKEN or via the remote URL (https://p1otek:${GIT_TOKEN}@git.kapuscinski.pl/p1otek/dione.git).

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}/{chain}/aave — Aave portfolio snapshots (chain-parameterized)
  • 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 — preserves isVerified only when messageData is present; btc/bitcoin/solana always kept; unverified EVM wallets discarded
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
setWalletOrder(addresses) Reorders internal array to match address order, persists

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 } where chainId is per-chain (ethereum=1, base=8453, arbitrum=42161, optimism=10, polygon=137, avalanche=43114, bsc=56, fantom=250, hyperevm=999; fallback 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 preserves isVerified on loadWallets() only when messageData is present — unverified EVM wallets are discarded on reload. btc/bitcoin/solana wallets are always kept. Verification must go through WalletVerifier.

Transaction Ledger Table — Data Flow

How the table gets its rows and values:

  1. Fetch: fetchWalletAaveData(address, chain) hits /api/v1/portfolio/{address}/{chain}/aave (chain-parameterized, defaults base), 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