Source mirror · commit 369512f · 2026-10-26 · MIT · canonical repo: gitlab.com/jiapunk/upgrade-airlock
git clone https://upgrade-airlock.pages.dev/source/upgrade-airlock.git
upgrade-airlock / web / src / config.ts
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
13export type AppMode = 'airlock' | 'offline'
14
15export 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
36export 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}