web/src/config.ts90 lines · 4978 bytes
| 1 | // Every setting of the Upgrade Airlock war room lives in this file. Plain data only (no browser APIs), so tests can |
| 2 | // import it in Node. |
| 3 | // |
| 4 | // URL parameters (switch at the venue without rebuilding): |
| 5 | // ?mode=airlock (default) live: the browser reads Ethereum Sepolia directly (read only, never signs or sends) |
| 6 | // ?mode=offline plays back the recorded run in src/recorded-run.json; opens no network connection at all |
| 7 | // ?rpc=http://127.0.0.1:19621 airlock only: read this JSON-RPC instead (a local Sepolia fork; WS on the same port). |
| 8 | // Loopback addresses only (127.x, localhost, [::1]); any other host is ignored |
| 9 | // ?lang=zh-CN | zh-TW | en |
| 10 | // ?p=<id> open on that proposal (offline: the replay loop starts with it) |
| 11 | // Presenter keys: web/README.md and the on-screen help (?). |
| 12 | |
| 13 | export type AppMode = 'airlock' | 'offline' |
| 14 | |
| 15 | export interface ChainConfig { |
| 16 | chainId: number |
| 17 | /** WebSocket endpoints, tried in turn; on a drop the page polls over HTTP and reconnects in the background */ |
| 18 | ws: string[] |
| 19 | /** HTTP JSON-RPC endpoints, rotated on failure */ |
| 20 | http: string[] |
| 21 | /** HTTP endpoints for history (eth_getLogs of past proposals), tried in this order first; rotated on failure */ |
| 22 | historyHttp: string[] |
| 23 | /** Explorer prefixes; null on a local fork (its hashes are not on the real chain) */ |
| 24 | explorerTx: string | null |
| 25 | explorerAddr: string | null |
| 26 | /** Sourcify lookup prefix (GET <prefix><address> -> { match }); null on a local fork */ |
| 27 | sourcifyApi: string | null |
| 28 | /** eth_blockNumber polling interval while the WS is down (ms) */ |
| 29 | pollMs: number |
| 30 | /** Largest eth_getLogs block range one call may ask for */ |
| 31 | logRange: number |
| 32 | /** On page load, how many blocks after each recent proposal's block are searched for its logs */ |
| 33 | historySpan: number |
| 34 | } |
| 35 | |
| 36 | export const CONFIG = { |
| 37 | defaultMode: 'airlock' as AppMode, |
| 38 | |
| 39 | /** Ethereum Sepolia. Every HTTP endpoint here was checked with real browser-origin requests: they answer CORS |
| 40 | * preflights. publicnode and drpc also serve WS (newHeads and logs subscriptions); 1rpc has no WS and caps |
| 41 | * eth_getLogs at 50 blocks, so every getLogs call stays within 50 blocks. */ |
| 42 | sepolia: { |
| 43 | chainId: 11155111, |
| 44 | ws: ['wss://ethereum-sepolia-rpc.publicnode.com', 'wss://sepolia.drpc.org'], |
| 45 | http: ['https://ethereum-sepolia-rpc.publicnode.com', 'https://sepolia.drpc.org', 'https://1rpc.io/sepolia'], |
| 46 | /** The proposals' history (eth_getLogs from each one's proposal block). publicnode at times answers logs older |
| 47 | * than about a day with "pruned history unavailable" (seen 2026-10-22) and drpc rate-limits a burst from one |
| 48 | * address, so drpc is asked first, then tenderly and thirdweb (both serve the full history and answer CORS, |
| 49 | * checked 2026-10-23); publicnode is the last resort, for recent proposals only. A failed call moves on to the |
| 50 | * next endpoint (4 tries, so each one once). */ |
| 51 | historyHttp: ['https://sepolia.drpc.org', 'https://sepolia.gateway.tenderly.co', 'https://sepolia.rpc.thirdweb.com', 'https://ethereum-sepolia-rpc.publicnode.com'], |
| 52 | explorerTx: 'https://sepolia.etherscan.io/tx/', |
| 53 | explorerAddr: 'https://sepolia.etherscan.io/address/', |
| 54 | sourcifyApi: 'https://sourcify.dev/server/v2/contract/11155111/', |
| 55 | pollMs: 3000, |
| 56 | logRange: 50, |
| 57 | historySpan: 150, |
| 58 | } satisfies ChainConfig, |
| 59 | |
| 60 | /** Sepolia block time (s/block): turns blocks into seconds when no block timestamp is at hand */ |
| 61 | blockSec: 12, |
| 62 | /** Cooldown shown before a deployment file exists (DeployEth.s.sol's default DELAY_BLOCKS) */ |
| 63 | delayBlocks: 25, |
| 64 | /** Recent proposals shown as cards and loaded on page open */ |
| 65 | recentProposals: 8, |
| 66 | /** Attack picker: when a card's newest judged proposal is older than the recent ones, read at most this many older |
| 67 | * proposal ids (getProposal only, newest first) to find it (chainwatch.ts backfillLab) */ |
| 68 | labScanMax: 40, |
| 69 | /** Sliding window (blocks) for the measured block time */ |
| 70 | blockTimeWindow: 60, |
| 71 | /** A measured block time below this is a fast-mined fork, not a real s/block */ |
| 72 | minPlausibleBlockMs: 50, |
| 73 | |
| 74 | /** Playback of recorded proposals (offline mode, and the P key in live mode) */ |
| 75 | replay: { |
| 76 | /** Virtual clock speed: ms per block (a 25-block cooldown plays in about 15 s) */ |
| 77 | msPerBlock: 600, |
| 78 | /** The clock holds on a step this long, so the screen can play the moment: the arrival, the verdict landing, |
| 79 | * the inner door opening */ |
| 80 | dwellMs: { proposed: 1500, verdict: 3200, executed: 3000 }, |
| 81 | /** Live mode: replay starts only after this long without a live event */ |
| 82 | idleMs: 60000, |
| 83 | /** Offline mode: after the viewer picks a proposal by hand, playback resumes after this long */ |
| 84 | offlineIdleMs: 20000, |
| 85 | /** Hold the final state of each proposal this long before the next one */ |
| 86 | gapMs: 4000, |
| 87 | /** prefers-reduced-motion: show the final state only, hold it this long */ |
| 88 | reducedHoldMs: 8000, |
| 89 | }, |
| 90 | } |