154 lines
8.2 KiB
Markdown
154 lines
8.2 KiB
Markdown
# 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
|
||
|
||
```bash
|
||
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 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`:
|
||
|
||
```js
|
||
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:
|
||
```bash
|
||
docker build -t cbbtc-dashboard . && docker stop dione-dashboard && docker rm dione-dashboard && docker run -d --name dione-dashboard -p 8080:80 cbbtc-dashboard
|
||
```
|