SPB Git

Repository Public

live-real-server.mts 5.9 KB 69 lines typescript

1// The REAL backend for futures-live-real.spec.ts, booted in one process: the real API server (live routes, market data, auth),2// the real isolated-signer class behind its real HMAC channel, PostgreSQL (scratch database, dropped and recreated), and the3// built web bundle served by the API itself (no mock in it). Modelled on scripts/hl-live-testnet.mts.4//5// KEYS. The wallet seed is a fresh BIP-39 phrase generated IN MEMORY here; it is never printed, never written, and is gone6// when the process ends. No existing key, seed or secrets file is read. The signer's state goes to a temporary directory.7//8// NETWORK. Hyperliquid TESTNET only for anything signed. Every outgoing request is counted by host and path (written to9// `--traffic <file>` once a second), and a request to the MAINNET exchange endpoint (api.hyperliquid.xyz/exchange) is refused10// here before it leaves the process — on top of the server's own lock (SPW_DERIV_LIVE_MAINNET is forced off). Mainnet is only11// ever READ (public market data, `/info`), exactly as the Futures terminal does in production.12//13//   npx tsx tests/e2e/live-real-server.mts --port 8672 --db spw_live_ui_e2e --traffic test-results/live-real-traffic.json14import { execSync } from 'node:child_process';15import { createHash } from 'node:crypto';16import { mkdtempSync, writeFileSync } from 'node:fs';17import type { Server } from 'node:http';18import { tmpdir } from 'node:os';19import { dirname, join, resolve } from 'node:path';20import { fileURLToPath } from 'node:url';21import { generateMnemonic } from '@scure/bip39';22import { wordlist } from '@scure/bip39/wordlists/english.js';23import { deriveKeys } from '../../services/isolated-signer/src/keys.ts';24import { exchangePrices } from '../../services/isolated-signer/src/prices.ts';25import { createSignerServer } from '../../services/isolated-signer/src/server.ts';26import { Signer } from '../../services/isolated-signer/src/signer.ts';27import { Store } from '../../services/isolated-signer/src/store.ts';2829const arg = (name: string, d: string) => { const i = process.argv.indexOf(`--${name}`); return i > 0 ? process.argv[i + 1]! : d; };30const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../..');31const PORT = Number(arg('port', '8672')), DB = arg('db', 'spw_live_ui_e2e'), TRAFFIC = arg('traffic', ''), SETUP_TOKEN = arg('setup-token', 'e2e-setup-token');32const ORIGIN = `http://localhost:${PORT}`, RP = 'localhost', SECRET = 'e'.repeat(48);33if (!/^[a-z_0-9]+$/.test(DB)) throw new Error('bad database name');3435// ── the guard: nothing may reach Hyperliquid's MAINNET exchange endpoint from this process ───────────────────────────────36const realFetch = globalThis.fetch, traffic: Record<string, number> = {};37globalThis.fetch = (async (input: Parameters<typeof fetch>[0], init?: RequestInit) => {38  const u = new URL(typeof input === 'string' ? input : input instanceof URL ? input.href : input.url);39  const k = `${u.host}${u.host.includes('hyperliquid') ? u.pathname : ''}`;40  if (u.host === 'api.hyperliquid.xyz' && u.pathname !== '/info') { traffic[`REFUSED ${k}`] = (traffic[`REFUSED ${k}`] ?? 0) + 1; throw new Error(`refusing a request to Hyperliquid MAINNET ${u.pathname}: only public reads (/info) leave this process`); }41  traffic[k] = (traffic[k] ?? 0) + 1;42  return realFetch(input, init);43}) as typeof fetch;44delete process.env.SPW_DERIV_LIVE_MAINNET;                                                            // the server's own mainnet lock stays closed, whatever the shell says45if (TRAFFIC) setInterval(() => { try { writeFileSync(TRAFFIC, JSON.stringify(traffic)); } catch { /* the directory may not exist yet */ } }, 1000).unref();4647const keys = deriveKeys(generateMnemonic(wordlist, 128));                                             // throwaway seed, in memory only48let signerSrv: Server | undefined, app: { close: () => Promise<void>; listen: (o: { port: number; host: string }) => Promise<string>; server: { closeAllConnections?: () => void } } | undefined;49const stop = async () => { signerSrv?.close(); app?.server.closeAllConnections?.(); await app?.close().catch(() => {}); if (TRAFFIC) { try { writeFileSync(TRAFFIC, JSON.stringify(traffic)); } catch { /* ignore */ } } process.exit(0); };50process.on('SIGTERM', () => void stop()); process.on('SIGINT', () => void stop());5152execSync(`dropdb --if-exists ${DB} && createdb ${DB}`);53const dir = mkdtempSync(join(tmpdir(), 'spw-live-ui-'));54const store = new Store(join(dir, 'state.json'), join(dir, 'policy.json'));55store.state.setupTokenHash = createHash('sha256').update(SETUP_TOKEN).digest('hex');56const signerCore = new Signer(keys, store, { rpId: RP, origin: ORIGIN }, undefined, exchangePrices());   // the signer's OWN prices (Coinbase + Kraken), as in production57signerSrv = createSignerServer(signerCore, SECRET).listen(0, '127.0.0.1');58await new Promise((r) => signerSrv!.once('listening', r));59Object.assign(process.env, { SPW_ORIGIN: ORIGIN, SPW_RP_ID: RP, PORT: String(PORT), HOST: '127.0.0.1', DATABASE_URL: `postgres:///${DB}`, SPW_SIGNER_SECRET: SECRET,60  SPW_SIGNER_URL: `http://127.0.0.1:${(signerSrv.address() as { port: number }).port}`, SPW_BROADCAST: 'off', SPW_WORKERS: 'off', SPW_WEB_DIR: resolve(ROOT, 'dist/web') });61const db = await import('../../services/api/src/db.ts');62await db.migrate();63const { mainnetLiveEnabled, startLiveWorker } = await import('../../services/api/src/derivatives/live.ts');64if (mainnetLiveEnabled()) throw new Error('refusing to run: the mainnet setting is on');65app = await (await import('../../services/api/src/server.ts')).buildServer({ swapProviders: [] }) as never;66startLiveWorker();                                                                                    // the reconciliation worker: it only reads the venue (and has nothing to do for a READ_ONLY account)67await app!.listen({ port: PORT, host: '127.0.0.1' });68console.log(`READY ${ORIGIN} account ${keys.addresses.evm.toLowerCase()} db ${DB}`);69
Files main
Menu

Theme