From 77251cb64f79d0ffc3f317918f1d290bdc3c857e Mon Sep 17 00:00:00 2001 From: jxom <7336481+jxom@users.noreply.github.com> Date: Fri, 10 Jul 2026 13:21:41 +1000 Subject: [PATCH] feat: default tempo ws to http port, expose `tempoZone` private rpc --- src/instances/tempo.test.ts | 39 ++++++++++++++++++++++++++--- src/instances/tempo.ts | 17 ++++++++++--- src/instances/tempoZone.ts | 5 ++-- src/testcontainers/Instance.test.ts | 18 +++++++++++++ src/testcontainers/Instance.ts | 20 +++++++++++++-- 5 files changed, 88 insertions(+), 11 deletions(-) diff --git a/src/instances/tempo.test.ts b/src/instances/tempo.test.ts index 01ca84c..21f55f5 100644 --- a/src/instances/tempo.test.ts +++ b/src/instances/tempo.test.ts @@ -2,6 +2,7 @@ import * as os from 'node:os' import getPort from 'get-port' import { Instance } from 'prool' import { afterEach, expect, test } from 'vitest' +import { type MessageEvent, WebSocket } from 'ws' import { command } from './tempo.js' const instances: Instance.Instance[] = [] @@ -140,12 +141,34 @@ test( }, ) +test( + 'behavior: serves websocket on http port', + { timeout: slowTestTimeout }, + async () => { + const instance = defineInstance() + await instance.start() + + const ws = new WebSocket(`ws://localhost:${port}`) + await new Promise((resolve, reject) => { + ws.addEventListener('open', resolve) + ws.addEventListener('error', reject) + }) + ws.send(JSON.stringify({ id: 0, jsonrpc: '2.0', method: 'eth_chainId' })) + const { data } = await new Promise((resolve) => + ws.addEventListener('message', resolve), + ) + ws.close() + + expect(JSON.parse(data.toString()).result).toBeDefined() + }, +) + const redact = (args: string[]) => args.join(' ').replaceAll(os.tmpdir(), '') test('command: default', () => { expect(redact(command({ port: 8545 }))).toMatchInlineSnapshot( - `"node --authrpc.port 8575 --datadir /.prool/tempo.8545 --dev --dev.block-time 50ms --engine.disable-precompile-cache --engine.legacy-state-root --faucet.address 0x20c0000000000000000000000000000000000000 0x20c0000000000000000000000000000000000001 0x20c0000000000000000000000000000000000002 0x20c0000000000000000000000000000000000003 --faucet.amount 1000000000000 --faucet.enabled --faucet.private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --faucet.node-address http://localhost:8545 --http.addr 0.0.0.0 --http.api all --http.corsdomain * --http.port 8545 --port 8555 --ws.port 8565"`, + `"node --authrpc.port 8575 --datadir /.prool/tempo.8545 --dev --dev.block-time 50ms --engine.disable-precompile-cache --engine.legacy-state-root --faucet.address 0x20c0000000000000000000000000000000000000 0x20c0000000000000000000000000000000000001 0x20c0000000000000000000000000000000000002 0x20c0000000000000000000000000000000000003 --faucet.amount 1000000000000 --faucet.enabled --faucet.private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --faucet.node-address http://localhost:8545 --http.addr 0.0.0.0 --http.api all --http.corsdomain * --http.port 8545 --port 8555 --ws --ws.addr 0.0.0.0 --ws.api all --ws.port 8545"`, ) }) @@ -155,7 +178,7 @@ test('command: behavior: faucet node address', () => { command({ faucet: { nodeAddress: 'http://localhost:1337' }, port: 8545 }), ), ).toMatchInlineSnapshot( - `"node --authrpc.port 8575 --datadir /.prool/tempo.8545 --dev --dev.block-time 50ms --engine.disable-precompile-cache --engine.legacy-state-root --faucet.address 0x20c0000000000000000000000000000000000000 0x20c0000000000000000000000000000000000001 0x20c0000000000000000000000000000000000002 0x20c0000000000000000000000000000000000003 --faucet.amount 1000000000000 --faucet.enabled --faucet.private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --faucet.node-address http://localhost:1337 --http.addr 0.0.0.0 --http.api all --http.corsdomain * --http.port 8545 --port 8555 --ws.port 8565"`, + `"node --authrpc.port 8575 --datadir /.prool/tempo.8545 --dev --dev.block-time 50ms --engine.disable-precompile-cache --engine.legacy-state-root --faucet.address 0x20c0000000000000000000000000000000000000 0x20c0000000000000000000000000000000000001 0x20c0000000000000000000000000000000000002 0x20c0000000000000000000000000000000000003 --faucet.amount 1000000000000 --faucet.enabled --faucet.private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --faucet.node-address http://localhost:1337 --http.addr 0.0.0.0 --http.api all --http.corsdomain * --http.port 8545 --port 8555 --ws --ws.addr 0.0.0.0 --ws.api all --ws.port 8545"`, ) }) @@ -163,7 +186,15 @@ test('command: behavior: faucet disabled', () => { expect( redact(command({ faucet: { enabled: false }, port: 8545 })), ).toMatchInlineSnapshot( - `"node --authrpc.port 8575 --datadir /.prool/tempo.8545 --dev --dev.block-time 50ms --engine.disable-precompile-cache --engine.legacy-state-root --faucet.address 0x20c0000000000000000000000000000000000000 0x20c0000000000000000000000000000000000001 0x20c0000000000000000000000000000000000002 0x20c0000000000000000000000000000000000003 --faucet.amount 1000000000000 --faucet.enabled false --faucet.private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --http.addr 0.0.0.0 --http.api all --http.corsdomain * --http.port 8545 --port 8555 --ws.port 8565"`, + `"node --authrpc.port 8575 --datadir /.prool/tempo.8545 --dev --dev.block-time 50ms --engine.disable-precompile-cache --engine.legacy-state-root --faucet.address 0x20c0000000000000000000000000000000000000 0x20c0000000000000000000000000000000000001 0x20c0000000000000000000000000000000000002 0x20c0000000000000000000000000000000000003 --faucet.amount 1000000000000 --faucet.enabled false --faucet.private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --http.addr 0.0.0.0 --http.api all --http.corsdomain * --http.port 8545 --port 8555 --ws --ws.addr 0.0.0.0 --ws.api all --ws.port 8545"`, + ) +}) + +test('command: behavior: ws port override', () => { + expect( + redact(command({ port: 8545, ws: [true, { port: 8565 }] })), + ).toMatchInlineSnapshot( + `"node --authrpc.port 8575 --datadir /.prool/tempo.8545 --dev --dev.block-time 50ms --engine.disable-precompile-cache --engine.legacy-state-root --faucet.address 0x20c0000000000000000000000000000000000000 0x20c0000000000000000000000000000000000001 0x20c0000000000000000000000000000000000002 0x20c0000000000000000000000000000000000003 --faucet.amount 1000000000000 --faucet.enabled --faucet.private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --faucet.node-address http://localhost:8545 --http.addr 0.0.0.0 --http.api all --http.corsdomain * --http.port 8545 --port 8555 --ws --ws.port 8565"`, ) }) @@ -171,6 +202,6 @@ test('command: behavior: http port override', () => { expect( redact(command({ http: { port: 1337 }, port: 8545 })), ).toMatchInlineSnapshot( - `"node --authrpc.port 8575 --datadir /.prool/tempo.8545 --dev --dev.block-time 50ms --engine.disable-precompile-cache --engine.legacy-state-root --faucet.address 0x20c0000000000000000000000000000000000000 0x20c0000000000000000000000000000000000001 0x20c0000000000000000000000000000000000002 0x20c0000000000000000000000000000000000003 --faucet.amount 1000000000000 --faucet.enabled --faucet.private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --faucet.node-address http://localhost:1337 --http.addr 0.0.0.0 --http.api all --http.corsdomain * --http.port 1337 --port 8555 --ws.port 8565"`, + `"node --authrpc.port 8575 --datadir /.prool/tempo.8545 --dev --dev.block-time 50ms --engine.disable-precompile-cache --engine.legacy-state-root --faucet.address 0x20c0000000000000000000000000000000000000 0x20c0000000000000000000000000000000000001 0x20c0000000000000000000000000000000000002 0x20c0000000000000000000000000000000000003 --faucet.amount 1000000000000 --faucet.enabled --faucet.private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --faucet.node-address http://localhost:1337 --http.addr 0.0.0.0 --http.api all --http.corsdomain * --http.port 1337 --port 8555 --ws --ws.addr 0.0.0.0 --ws.api all --ws.port 1337"`, ) }) diff --git a/src/instances/tempo.ts b/src/instances/tempo.ts index 8b60afd..9ef7364 100644 --- a/src/instances/tempo.ts +++ b/src/instances/tempo.ts @@ -45,9 +45,13 @@ export function command(parameters: tempo.Parameters): string[] { port: port!, }, port: port! + 10, - ws: { - port: port! + 20, - }, + ws: [ + true, + { + addr: '0.0.0.0', + api: 'all', + }, + ], } const args = deepAssign(defaultParameters, rest) @@ -58,6 +62,13 @@ export function command(parameters: tempo.Parameters): string[] { if (faucet?.enabled && !faucet.nodeAddress) faucet.nodeAddress = `http://localhost:${http.port}` + // WS defaults to the http port: one endpoint (and one proxied server port) serves both protocols. + const ws = args['ws'] as [true, { port?: number | undefined }?] | undefined + if (Array.isArray(ws) && ws[0] === true) { + ws[1] ??= {} + ws[1].port ??= http.port + } + return [ 'node', ...toArgs(args, { diff --git a/src/instances/tempoZone.ts b/src/instances/tempoZone.ts index 55f218e..8e52805 100644 --- a/src/instances/tempoZone.ts +++ b/src/instances/tempoZone.ts @@ -93,13 +93,14 @@ export const tempoZone = Instance.define( })`${[binary, ...command({ ...args, port })]}`, { ...options, - // Resolve when the zone RPC server is listening (fires after provisioning). + // Resolve when the private zone RPC is listening (last server to start). resolver({ process, reject, resolve }) { let stderr = '' process.stdout.on('data', (data) => { const message = data.toString() if (log) console.log(message) - if (message.includes('RPC HTTP server started')) resolve() + if (message.includes('Private zone RPC server started')) + resolve() if (message.includes('shutting down')) reject('shutting down') }) process.stderr.on('data', (data) => { diff --git a/src/testcontainers/Instance.test.ts b/src/testcontainers/Instance.test.ts index 31f1451..d5b6a98 100644 --- a/src/testcontainers/Instance.test.ts +++ b/src/testcontainers/Instance.test.ts @@ -167,6 +167,24 @@ describe.runIf(tempoZoneImage)('tempoZone', () => { const { result: blockNumber } = await rpc('eth_blockNumber', []) expect(blockNumber).toBeDefined() + // Private RPC (auth-gated `eth_*` + `zone_*`) is exposed; reachability only (no token). + const { privateRpc } = instance._internal + expect(privateRpc).toBeDefined() + const response = await fetch( + `http://${privateRpc!.host}:${privateRpc!.port}`, + { + body: JSON.stringify({ + id: 0, + jsonrpc: '2.0', + method: 'zone_getZoneInfo', + params: [], + }), + headers: { 'Content-Type': 'application/json' }, + method: 'POST', + }, + ) + expect(response.status).toBeDefined() + await instance.stop() expect(instance.status).toEqual('stopped') }) diff --git a/src/testcontainers/Instance.ts b/src/testcontainers/Instance.ts index 0f6f270..e482dcf 100644 --- a/src/testcontainers/Instance.ts +++ b/src/testcontainers/Instance.ts @@ -182,6 +182,7 @@ export const tempoZone = Instance.define( let container: StartedTestContainer | undefined let l1Container: StartedTestContainer | undefined let network: StartedNetwork | undefined + let privateRpcPort: number | undefined async function teardown() { if (container) await container.stop().catch(() => {}) @@ -190,6 +191,7 @@ export const tempoZone = Instance.define( container = undefined l1Container = undefined network = undefined + privateRpcPort = undefined } return { @@ -203,12 +205,23 @@ export const tempoZone = Instance.define( wsPort: l1Container.getMappedPort(l1WsPort), } }, + get privateRpc() { + if (!container || !privateRpcPort) return undefined + return { + host: container.getHost(), + port: container.getMappedPort(privateRpcPort), + } + }, }, host: args.host ?? 'localhost', name, port: args.port ?? 9545, async start({ port = args.port }, { emitter, setEndpoint }) { const containerPort = port ?? 9545 + // Mirrors the `zoneCommand` default; serves authenticated `eth_*` + `zone_*`. + privateRpcPort = + (args['privateRpc'] as { port?: number } | undefined)?.port ?? + containerPort + 3 const timeout = ContainerOptions.resolveStartupTimeout(startupTimeout) const logConsumer = @@ -272,7 +285,7 @@ export const tempoZone = Instance.define( let c = new GenericContainer(image) .withPullPolicy(PullPolicy.alwaysPull()) .withPlatform('linux/x86_64') - .withExposedPorts(containerPort) + .withExposedPorts(containerPort, privateRpcPort) .withExtraHosts([ { host: 'host.docker.internal', ipAddress: 'host-gateway' }, ]) @@ -290,7 +303,10 @@ export const tempoZone = Instance.define( port: containerPort, }), ) - .withWaitStrategy(Wait.forLogMessage('RPC HTTP server started')) + // Fires after the public RPC; last server to start. + .withWaitStrategy( + Wait.forLogMessage('Private zone RPC server started'), + ) .withLogConsumer(logConsumer(promise.reject)) .withStartupTimeout(timeout) if (network) c = c.withNetwork(network)