Repository Public
1// The REAL backend for qa-real.spec.ts and for manual QA passes, booted in one process on its OWN port and scratch database2// (nothing is shared with scripts/dev-stack.sh): the real API server with the real swap/bridge providers, the real3// isolated-signer class behind its real HMAC channel, PostgreSQL (database dropped and recreated) and the built web bundle.4// Modelled on tests/e2e/live-real-server.mts.5//6// KEYS. The wallet seed is a fresh BIP-39 phrase generated IN MEMORY; never printed, never written, gone with the process.7// No existing key, seed or secrets file is read. The signer's state goes to a temporary directory.8//9// NETWORK. Public reads only (RPC nodes, price feeds, quotes, exchanges' public data). SPW_BROADCAST is forced off, and on10// top of it every outgoing request is inspected here: a JSON-RPC call that would relay a signed transaction, and any request11// to Hyperliquid's MAINNET exchange endpoint, is refused before it leaves the process.12//13// npx tsx tests/e2e/qa-real-server.mts --port 8690 --db spw_qa_polish [--workers on] [--fork base,arbitrum] [--traffic file.json]14//15// `--fork base,arbitrum` (needs anvil) points those networks at LOCAL forks and gives the throwaway wallet 0.05 ETH on each16// and 250 USDC on Base, so the funded flows (send review, swap review, CCTP review, agent gas reserve) can be walked up to17// the passkey. They are private chains on 127.0.0.1: nothing on them exists anywhere else, and broadcasting stays off all18// the same. Fork RPC ports are the web port + 1, + 2…; `anvil_*` methods there are how a test funds an agent's vault.19//20// `--workers on` starts what main.ts starts (health, reconciliation, market data, paper derivatives, agent engine), so21// backtests find candles and paper agents trade by themselves.22import { execSync, spawn, type ChildProcess } from 'node:child_process';23import { createHash } from 'node:crypto';24import dns from 'node:dns';25import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';26import type { Server } from 'node:http';27import net from 'node:net';28import { tmpdir } from 'node:os';29import { dirname, join, resolve } from 'node:path';30import { fileURLToPath } from 'node:url';31import { generateMnemonic } from '@scure/bip39';32import { wordlist } from '@scure/bip39/wordlists/english.js';33import { deriveKeys } from '../../services/isolated-signer/src/keys.ts';34import { exchangePrices } from '../../services/isolated-signer/src/prices.ts';35import { createSignerServer } from '../../services/isolated-signer/src/server.ts';36import { Signer } from '../../services/isolated-signer/src/signer.ts';37import { Store } from '../../services/isolated-signer/src/store.ts';3839const arg = (name: string, d: string) => { const i = process.argv.indexOf(`--${name}`); return i > 0 ? process.argv[i + 1]! : d; };40const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../..');41const PORT = Number(arg('port', '8690')), DB = arg('db', 'spw_qa_polish'), TRAFFIC = arg('traffic', ''), SETUP_TOKEN = arg('setup-token', 'qa-setup-token'), WORKERS = arg('workers', 'off') === 'on';42const FORK = arg('fork', '');43const ORIGIN = `http://localhost:${PORT}`, RP = 'localhost', SECRET = 'q'.repeat(48);44if (!/^[a-z_0-9]+$/.test(DB)) throw new Error('bad database name');45dns.setDefaultResultOrder('ipv4first');46net.setDefaultAutoSelectFamilyAttemptTimeout(2500);4748// ── the guard: nothing signed may leave this process ─────────────────────────────────────────────────────────────────────49const SEND = /"(eth_sendRawTransaction|eth_sendTransaction|sendTransaction|sendrawtransaction|broadcast_tx_(sync|async|commit)|submit)"/;50const realFetch = globalThis.fetch, traffic: Record<string, number> = {};51globalThis.fetch = (async (input: Parameters<typeof fetch>[0], init?: RequestInit) => {52 const u = new URL(typeof input === 'string' ? input : input instanceof URL ? input.href : input.url);53 const refuse = (why: string) => { traffic[`REFUSED ${u.host} ${why}`] = (traffic[`REFUSED ${u.host} ${why}`] ?? 0) + 1; throw new Error(`QA stack: refusing ${why} to ${u.host}`); };54 if (u.host === 'api.hyperliquid.xyz' && u.pathname !== '/info') refuse(`a Hyperliquid MAINNET ${u.pathname} request`);55 if (typeof init?.body === 'string' && u.hostname !== '127.0.0.1' && u.hostname !== 'localhost' && SEND.test(init.body) && /"method"/.test(init.body)) refuse('a transaction broadcast');56 traffic[u.host] = (traffic[u.host] ?? 0) + 1;57 return realFetch(input, init);58}) as typeof fetch;59delete process.env.SPW_DERIV_LIVE_MAINNET;60if (TRAFFIC) setInterval(() => { try { writeFileSync(TRAFFIC, JSON.stringify(traffic, null, 1)); } catch { /* the directory may not exist yet */ } }, 2000).unref();6162const keys = deriveKeys(generateMnemonic(wordlist, 128)); // throwaway seed, in memory only63let signerSrv: Server | undefined, app: { close: () => Promise<void>; listen: (o: { port: number; host: string }) => Promise<string>; server: { closeAllConnections?: () => void } } | undefined;64const forks: ChildProcess[] = [];65const stop = async () => { for (const f of forks) f.kill(); signerSrv?.close(); app?.server.closeAllConnections?.(); await app?.close().catch(() => {}); process.exit(0); };66process.on('SIGTERM', () => void stop()); process.on('SIGINT', () => void stop());6768execSync(`dropdb --if-exists ${DB} && createdb ${DB}`);69rmSync(join(tmpdir(), 'spw-qa', `passkey-${PORT}.json`), { force: true }); // a fresh stack: the virtual passkey of the previous one is void70const dir = mkdtempSync(join(tmpdir(), 'spw-qa-'));71const store = new Store(join(dir, 'state.json'), join(dir, 'policy.json'));72store.state.setupTokenHash = createHash('sha256').update(SETUP_TOKEN).digest('hex');73if (FORK) {74 // per network: public RPC to fork from, chain id, and (Base only) the USDC contract + a holder to take 250 USDC from, on the fork75 const DEF: Record<string, { rpc: string; chain: number; usdc?: string }> = { base: { rpc: process.env.BASE_RPC ?? 'https://mainnet.base.org', chain: 8453, usdc: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' }, arbitrum: { rpc: process.env.ARBITRUM_RPC ?? 'https://arb1.arbitrum.io/rpc', chain: 42161 } };76 const WHALE = process.env.USDC_WHALE ?? '0xF977814e90dA44bFA03b6295A0616a897441aceC', override: Record<string, string[]> = {}, maxFee: Record<string, string> = {};77 const me = keys.addresses.evm, pad = (a: string) => a.toLowerCase().replace(/^0x/, '').padStart(64, '0');78 for (const [k, id] of FORK.split(',').entries()) {79 const d = DEF[id]; if (!d) throw new Error(`--fork supports ${Object.keys(DEF).join(', ')}`);80 const url = `http://127.0.0.1:${PORT + 1 + k}`;81 forks.push(spawn('anvil', ['--fork-url', d.rpc, '--port', String(PORT + 1 + k), '--chain-id', String(d.chain), '--silent'], { stdio: 'ignore', detached: true }));82 let rid = 0;83 const rpc = async (method: string, params: unknown[] = []) => { const r = await realFetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: ++rid, method, params }) }); const j = await r.json() as { result?: unknown; error?: { message: string } }; if (j.error) throw new Error(`${method}: ${j.error.message}`); return j.result; };84 for (let n = 0; n < 160; n++) { try { await rpc('eth_chainId'); break; } catch { await new Promise((r) => setTimeout(r, 250)); } }85 await rpc('anvil_setBalance', [me, '0x' + (5n * 10n ** 16n).toString(16)]);86 if (d.usdc) { await rpc('anvil_impersonateAccount', [WHALE]); await rpc('anvil_setBalance', [WHALE, '0x' + (10n ** 18n).toString(16)]);87 await rpc('eth_sendTransaction', [{ from: WHALE, to: d.usdc, data: `0xa9059cbb${pad(me)}${(250_000000n).toString(16).padStart(64, '0')}` }]); // transfer(me, 250 USDC) on the local fork88 await rpc('anvil_stopImpersonatingAccount', [WHALE]); }89 override[id] = [url, url]; maxFee[id] = '5000000000000000';90 }91 process.on('exit', () => { for (const f of forks) f.kill(); });92 process.env.SPW_RPC_OVERRIDE = JSON.stringify(override);93 // anvil reports a 1 gwei priority fee (far above the real one on these networks): raise the fee ceiling for the forks only94 store.policy = { ...store.policy, maxFee: { ...store.policy.maxFee, ...maxFee } };95}96const signerCore = new Signer(keys, store, { rpId: RP, origin: ORIGIN }, undefined, exchangePrices()); // the signer's OWN prices (Coinbase + Kraken), as in production97signerSrv = createSignerServer(signerCore, SECRET).listen(0, '127.0.0.1');98await new Promise((r) => signerSrv!.once('listening', r));99Object.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,100 SPW_SIGNER_URL: `http://127.0.0.1:${(signerSrv.address() as { port: number }).port}`, SPW_BROADCAST: 'off', SPW_WORKERS: WORKERS ? 'on' : 'off', SPW_WEB_DIR: resolve(ROOT, arg('web', 'dist/web')), SPW_RESEARCH_DIR: resolve(ROOT, 'research') });101const { config } = await import('../../services/api/src/config.ts');102if (config.broadcast) throw new Error('refusing to run: broadcasting is on');103const db = await import('../../services/api/src/db.ts');104await db.migrate();105const { mainnetLiveEnabled, startLiveWorker } = await import('../../services/api/src/derivatives/live.ts');106if (mainnetLiveEnabled()) throw new Error('refusing to run: the mainnet setting is on');107app = await (await import('../../services/api/src/server.ts')).buildServer() as never; // default providers: real public quotes108await app!.listen({ port: PORT, host: '127.0.0.1' });109if (WORKERS) {110 const { startWorkers } = await import('../../services/api/src/worker.ts');111 const { startMarketWorker, getSeries, assertFresh } = await import('../../services/api/src/market/index.ts');112 const { startDerivativesWorker } = await import('../../services/api/src/derivatives/worker.ts');113 const { startAgentEngine } = await import('../../services/api/src/agents/live.ts');114 startWorkers(); startMarketWorker(); startDerivativesWorker(); startLiveWorker();115 startAgentEngine({ series: async (s, i, limit) => (await getSeries(s as never, i as never, { limit })).bars, assertFresh: async (s, i) => { await assertFresh(s as never, i as never); } });116} else startLiveWorker();117console.log(`READY ${ORIGIN} account ${keys.addresses.evm.toLowerCase()} db ${DB} workers ${WORKERS ? 'on' : 'off'} broadcast OFF${FORK ? ` fork ${FORK} on ports ${PORT + 1}+` : ''}`);118