web/src/runtime.ts82 lines · 3846 bytes
| 1 | // This run's settings, from config.ts plus the URL (browser side). |
| 2 | import { CONFIG, type AppMode, type ChainConfig } from './config' |
| 3 | |
| 4 | const params = new URLSearchParams(typeof location === 'undefined' ? '' : location.search) |
| 5 | |
| 6 | export function pickMode(raw: string | null): AppMode { |
| 7 | return raw === 'offline' || raw === 'airlock' ? raw : CONFIG.defaultMode |
| 8 | } |
| 9 | |
| 10 | export const MODE: AppMode = pickMode(params.get('mode')) |
| 11 | |
| 12 | /** Offline playback: no fetch, no WebSocket; everything comes from src/recorded-run.json */ |
| 13 | export const OFFLINE = MODE === 'offline' |
| 14 | |
| 15 | /** ?rpc= override: must be an http(s) URL, otherwise ignored */ |
| 16 | export function parseRpcOverride(raw: string | null | undefined): string | null { |
| 17 | if (!raw) return null |
| 18 | const s = raw.trim() |
| 19 | if (!/^https?:\/\/[^\s/?#]+(?:[/?#]\S*)?$/i.test(s)) return null |
| 20 | return s.replace(/\/+$/, '') |
| 21 | } |
| 22 | |
| 23 | /** Loopback address (local fork): 127.0.0.1, localhost, [::1] */ |
| 24 | export function isLoopback(url: string): boolean { |
| 25 | return /^https?:\/\/(?:127\.\d+\.\d+\.\d+|localhost|\[::1\])(?::\d+)?(?:[/?#]|$)/i.test(url) |
| 26 | } |
| 27 | |
| 28 | /** The ?rpc= override this page uses: a local fork (a loopback address) only. Any other host is ignored, so a link to |
| 29 | * the public site can never make it read someone else's JSON-RPC and draw that under "Live · Ethereum Sepolia" with |
| 30 | * Etherscan links. Offline mode has no endpoints at all. */ |
| 31 | export function rpcOverrideOf(raw: string | null | undefined, offline = false): string | null { |
| 32 | if (offline) return null |
| 33 | const s = parseRpcOverride(raw) |
| 34 | return s !== null && isLoopback(s) ? s : null |
| 35 | } |
| 36 | |
| 37 | /** Chain settings with an optional ?rpc= override: only that endpoint is used (WS on the same host and port, as |
| 38 | * anvil serves both). A local fork has no explorer and no Sourcify (its addresses and hashes are not on Sepolia). */ |
| 39 | export function chainWithRpc(base: ChainConfig, rpc: string | null): ChainConfig { |
| 40 | if (!rpc) return { ...base, ws: [...base.ws], http: [...base.http], historyHttp: [...base.historyHttp] } |
| 41 | const local = isLoopback(rpc) |
| 42 | return { |
| 43 | ...base, |
| 44 | ws: [rpc.replace(/^http/i, 'ws')], |
| 45 | http: [rpc], |
| 46 | historyHttp: [rpc], |
| 47 | explorerTx: local ? null : base.explorerTx, |
| 48 | explorerAddr: local ? null : base.explorerAddr, |
| 49 | sourcifyApi: local ? null : base.sourcifyApi, |
| 50 | pollMs: local ? 1000 : base.pollMs, |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | const RPC_OVERRIDE = rpcOverrideOf(params.get('rpc'), OFFLINE) |
| 55 | |
| 56 | /** Live view of a local Sepolia fork (?rpc= on a loopback address): numbers are labelled "local fork" */ |
| 57 | export const FORK = RPC_OVERRIDE !== null |
| 58 | |
| 59 | /** Offline mode has no endpoints and no outbound links at all, so nothing can open a connection by accident */ |
| 60 | export const CHAIN: ChainConfig = OFFLINE |
| 61 | ? { ...CONFIG.sepolia, ws: [], http: [], historyHttp: [], explorerTx: null, explorerAddr: null, sourcifyApi: null } |
| 62 | : chainWithRpc(CONFIG.sepolia, RPC_OVERRIDE) |
| 63 | |
| 64 | /** ?p=<id>: open on that proposal (a positive integer, else ignored) */ |
| 65 | export function parsePick(raw: string | null | undefined): string | null { |
| 66 | const s = (raw ?? '').trim() |
| 67 | return /^[1-9]\d{0,8}$/.test(s) ? s : null |
| 68 | } |
| 69 | export const PICK = parsePick(params.get('p')) |
| 70 | |
| 71 | /** ?clean=1 (or a bare ?clean): recording mode. The presenter chrome (language switch, Details, ?, key hints) is not |
| 72 | * drawn, so a recorded video shows only the product. ?clean=0 or no parameter: the normal page */ |
| 73 | export function parseClean(raw: string | null | undefined): boolean { |
| 74 | if (raw === null || raw === undefined) return false |
| 75 | const s = raw.trim().toLowerCase() |
| 76 | return s === '' || s === '1' || s === 'true' || s === 'yes' |
| 77 | } |
| 78 | export const CLEAN = parseClean(params.get('clean')) |
| 79 | |
| 80 | /** Guard cards in deployment order: guardians = [AirlockReceiver (Chainlink CRE), emergency guardian (human)] */ |
| 81 | export const GUARDS = ['cre', 'breakglass'] as const |
| 82 | export type GuardId = (typeof GUARDS)[number] |