prool 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Instance.d.ts +1 -0
- package/dist/Instance.d.ts.map +1 -1
- package/dist/Instance.js +1 -0
- package/dist/Instance.js.map +1 -1
- package/dist/instances/tempo.d.ts +20 -0
- package/dist/instances/tempo.d.ts.map +1 -1
- package/dist/instances/tempo.js +7 -1
- package/dist/instances/tempo.js.map +1 -1
- package/dist/instances/tempoZone.d.ts +151 -0
- package/dist/instances/tempoZone.d.ts.map +1 -0
- package/dist/instances/tempoZone.js +116 -0
- package/dist/instances/tempoZone.js.map +1 -0
- package/dist/processes/execa.d.ts.map +1 -1
- package/dist/processes/execa.js +4 -3
- package/dist/processes/execa.js.map +1 -1
- package/dist/testcontainers/Instance.d.ts +70 -0
- package/dist/testcontainers/Instance.d.ts.map +1 -1
- package/dist/testcontainers/Instance.js +157 -1
- package/dist/testcontainers/Instance.js.map +1 -1
- package/package.json +1 -1
- package/src/Instance.ts +1 -0
- package/src/instances/tempo.test.ts +87 -0
- package/src/instances/tempo.ts +19 -1
- package/src/instances/tempoZone.test.ts +60 -0
- package/src/instances/tempoZone.ts +207 -0
- package/src/processes/execa.ts +4 -3
- package/src/testcontainers/Instance.test.ts +93 -1
- package/src/testcontainers/Instance.ts +226 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import getPort from 'get-port'
|
|
2
2
|
import { Instance } from 'prool/testcontainers'
|
|
3
|
-
import { afterEach, expect, test } from 'vitest'
|
|
3
|
+
import { afterEach, describe, expect, test } from 'vitest'
|
|
4
4
|
|
|
5
5
|
const instances: Instance.Instance[] = []
|
|
6
6
|
const slowTestTimeout = 30_000
|
|
@@ -79,3 +79,95 @@ test('behavior: can subscribe to stdout', async () => {
|
|
|
79
79
|
})
|
|
80
80
|
|
|
81
81
|
test.skip('behavior: can subscribe to stderr', () => {})
|
|
82
|
+
|
|
83
|
+
test('behavior: faucet funds address', { timeout: 120_000 }, async () => {
|
|
84
|
+
const instance = defineInstance({ startupTimeout: 60_000 })
|
|
85
|
+
await instance.start()
|
|
86
|
+
|
|
87
|
+
const rpc = async (method: string, params: unknown[]) => {
|
|
88
|
+
const response = await fetch(`http://${instance.host}:${instance.port}`, {
|
|
89
|
+
body: JSON.stringify({ id: 0, jsonrpc: '2.0', method, params }),
|
|
90
|
+
headers: { 'Content-Type': 'application/json' },
|
|
91
|
+
method: 'POST',
|
|
92
|
+
})
|
|
93
|
+
return await response.json()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Funding races the first wall-clock block; expiring-nonce validation rejects until one exists.
|
|
97
|
+
let json: { result?: string[] } = {}
|
|
98
|
+
for (let i = 0; i < 100; i++) {
|
|
99
|
+
json = await rpc('tempo_fundAddress', [
|
|
100
|
+
'0x000000000000000000000000000000000000beef',
|
|
101
|
+
])
|
|
102
|
+
if (json.result) break
|
|
103
|
+
await new Promise((resolve) => setTimeout(resolve, 500))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
expect(
|
|
107
|
+
JSON.stringify(json).replaceAll(/0x[0-9a-f]{64}/g, '<hash>'),
|
|
108
|
+
).toMatchInlineSnapshot(
|
|
109
|
+
`"{"jsonrpc":"2.0","id":0,"result":["<hash>","<hash>","<hash>","<hash>"]}"`,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
// balanceOf(0x...beef) on the first faucet token.
|
|
113
|
+
let balance = 0n
|
|
114
|
+
for (let i = 0; i < 100; i++) {
|
|
115
|
+
const { result } = await rpc('eth_call', [
|
|
116
|
+
{
|
|
117
|
+
data: '0x70a08231000000000000000000000000000000000000000000000000000000000000beef',
|
|
118
|
+
to: '0x20c0000000000000000000000000000000000000',
|
|
119
|
+
},
|
|
120
|
+
'latest',
|
|
121
|
+
])
|
|
122
|
+
balance = BigInt(result ?? 0)
|
|
123
|
+
if (balance > 0n) break
|
|
124
|
+
await new Promise((resolve) => setTimeout(resolve, 500))
|
|
125
|
+
}
|
|
126
|
+
expect(balance).toBeGreaterThan(0n)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
// Requires a `tempo-zone` image with the `dev` command and GHCR access
|
|
130
|
+
// (the package is private), e.g. `VITE_TEMPO_ZONE_IMAGE=ghcr.io/tempoxyz/tempo-zone:latest`.
|
|
131
|
+
const tempoZoneImage = process.env['VITE_TEMPO_ZONE_IMAGE']
|
|
132
|
+
const zonePort = await getPort()
|
|
133
|
+
|
|
134
|
+
describe.runIf(tempoZoneImage)('tempoZone', () => {
|
|
135
|
+
const defineZoneInstance = (
|
|
136
|
+
parameters: Instance.tempoZone.Parameters = {},
|
|
137
|
+
) => {
|
|
138
|
+
const instance = Instance.tempoZone({
|
|
139
|
+
image: tempoZoneImage,
|
|
140
|
+
port: zonePort,
|
|
141
|
+
startupTimeout: 120_000,
|
|
142
|
+
...parameters,
|
|
143
|
+
})
|
|
144
|
+
instances.push(instance)
|
|
145
|
+
return instance
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
test('default', { timeout: 300_000 }, async () => {
|
|
149
|
+
const instance = defineZoneInstance()
|
|
150
|
+
|
|
151
|
+
await instance.start()
|
|
152
|
+
expect(instance.status).toEqual('started')
|
|
153
|
+
|
|
154
|
+
const rpc = async (method: string, params: unknown[]) => {
|
|
155
|
+
const response = await fetch(`http://${instance.host}:${instance.port}`, {
|
|
156
|
+
body: JSON.stringify({ id: 0, jsonrpc: '2.0', method, params }),
|
|
157
|
+
headers: { 'Content-Type': 'application/json' },
|
|
158
|
+
method: 'POST',
|
|
159
|
+
})
|
|
160
|
+
return await response.json()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Zone chain IDs are derived: ZONE_CHAIN_ID_BASE + (zone_id % range).
|
|
164
|
+
const { result: chainId } = await rpc('eth_chainId', [])
|
|
165
|
+
expect(BigInt(chainId)).toBeGreaterThanOrEqual(421_700_000n)
|
|
166
|
+
|
|
167
|
+
const { result: blockNumber } = await rpc('eth_blockNumber', [])
|
|
168
|
+
expect(blockNumber).toBeDefined()
|
|
169
|
+
|
|
170
|
+
await instance.stop()
|
|
171
|
+
expect(instance.status).toEqual('stopped')
|
|
172
|
+
})
|
|
173
|
+
})
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
GenericContainer,
|
|
3
|
+
Network,
|
|
3
4
|
PullPolicy,
|
|
5
|
+
type StartedNetwork,
|
|
4
6
|
type StartedTestContainer,
|
|
5
7
|
Wait,
|
|
6
8
|
} from 'testcontainers'
|
|
7
9
|
import * as Instance from '../Instance.js'
|
|
8
10
|
import { command, type tempo as core_tempo } from '../instances/tempo.js'
|
|
11
|
+
import {
|
|
12
|
+
type tempoZone as core_tempoZone,
|
|
13
|
+
command as zoneCommand,
|
|
14
|
+
} from '../instances/tempoZone.js'
|
|
9
15
|
import * as ContainerOptions from './containerOptions.js'
|
|
10
16
|
|
|
11
17
|
export type { Instance, InstanceOptions } from '../Instance.js'
|
|
@@ -131,3 +137,223 @@ export declare namespace tempo {
|
|
|
131
137
|
port?: number | undefined
|
|
132
138
|
}
|
|
133
139
|
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Defines a Tempo Zone instance.
|
|
143
|
+
*
|
|
144
|
+
* Starts a Tempo dev L1 container and a zone container (`tempo-zone dev`) on a
|
|
145
|
+
* shared network, provisioning a fresh zone against the L1. Pass `l1.rpcUrl`
|
|
146
|
+
* to attach to an existing L1 instead (the URL must be reachable from inside
|
|
147
|
+
* the container, e.g. `ws://host.docker.internal:8546`).
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```ts
|
|
151
|
+
* const instance = Instance.tempoZone({ port: 9545 })
|
|
152
|
+
* await instance.start()
|
|
153
|
+
* // ...
|
|
154
|
+
* await instance.stop()
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
export const tempoZone = Instance.define(
|
|
158
|
+
(parameters?: tempoZone.Parameters) => {
|
|
159
|
+
const {
|
|
160
|
+
containerName = `tempo-zone.${crypto.randomUUID()}`,
|
|
161
|
+
image = 'ghcr.io/tempoxyz/tempo-zone:latest',
|
|
162
|
+
l1: l1Parameters,
|
|
163
|
+
log: log_,
|
|
164
|
+
startupTimeout,
|
|
165
|
+
...args
|
|
166
|
+
} = parameters || {}
|
|
167
|
+
|
|
168
|
+
const log = (() => {
|
|
169
|
+
try {
|
|
170
|
+
return JSON.parse(log_ as string)
|
|
171
|
+
} catch {
|
|
172
|
+
return log_
|
|
173
|
+
}
|
|
174
|
+
})()
|
|
175
|
+
const RUST_LOG = log && typeof log !== 'boolean' ? log : ''
|
|
176
|
+
|
|
177
|
+
// L1 ports inside the container (only reachable over the shared network).
|
|
178
|
+
const l1HttpPort = 8545
|
|
179
|
+
const l1WsPort = 8546
|
|
180
|
+
|
|
181
|
+
const name = 'tempo-zone'
|
|
182
|
+
let container: StartedTestContainer | undefined
|
|
183
|
+
let l1Container: StartedTestContainer | undefined
|
|
184
|
+
let network: StartedNetwork | undefined
|
|
185
|
+
|
|
186
|
+
async function teardown() {
|
|
187
|
+
if (container) await container.stop().catch(() => {})
|
|
188
|
+
if (l1Container) await l1Container.stop().catch(() => {})
|
|
189
|
+
if (network) await network.stop().catch(() => {})
|
|
190
|
+
container = undefined
|
|
191
|
+
l1Container = undefined
|
|
192
|
+
network = undefined
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
_internal: {
|
|
197
|
+
args,
|
|
198
|
+
get l1() {
|
|
199
|
+
if (!l1Container) return undefined
|
|
200
|
+
return {
|
|
201
|
+
host: l1Container.getHost(),
|
|
202
|
+
port: l1Container.getMappedPort(l1HttpPort),
|
|
203
|
+
wsPort: l1Container.getMappedPort(l1WsPort),
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
host: args.host ?? 'localhost',
|
|
208
|
+
name,
|
|
209
|
+
port: args.port ?? 9545,
|
|
210
|
+
async start({ port = args.port }, { emitter, setEndpoint }) {
|
|
211
|
+
const containerPort = port ?? 9545
|
|
212
|
+
const timeout = ContainerOptions.resolveStartupTimeout(startupTimeout)
|
|
213
|
+
|
|
214
|
+
const logConsumer =
|
|
215
|
+
(reject: (error: Error) => void) =>
|
|
216
|
+
(stream: NodeJS.ReadableStream) => {
|
|
217
|
+
stream.on('data', (data) => {
|
|
218
|
+
const message = data.toString()
|
|
219
|
+
emitter.emit('message', message)
|
|
220
|
+
emitter.emit('stdout', message)
|
|
221
|
+
if (log) console.log(message)
|
|
222
|
+
if (message.includes('shutting down'))
|
|
223
|
+
reject(new Error(`Failed to start: ${message}`))
|
|
224
|
+
})
|
|
225
|
+
stream.on('error', (error) => {
|
|
226
|
+
if (log) console.error(error.message)
|
|
227
|
+
emitter.emit('message', error.message)
|
|
228
|
+
emitter.emit('stderr', error.message)
|
|
229
|
+
reject(new Error(`Failed to start: ${error.message}`))
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
// Start a dev L1 on a shared network unless attaching to an existing one.
|
|
235
|
+
let l1RpcUrl = l1Parameters?.rpcUrl
|
|
236
|
+
if (!l1RpcUrl) {
|
|
237
|
+
network = await new Network().start()
|
|
238
|
+
l1Container = await new GenericContainer(
|
|
239
|
+
l1Parameters?.image ?? 'ghcr.io/tempoxyz/tempo:latest',
|
|
240
|
+
)
|
|
241
|
+
.withPullPolicy(PullPolicy.alwaysPull())
|
|
242
|
+
.withPlatform('linux/x86_64')
|
|
243
|
+
.withNetwork(network)
|
|
244
|
+
.withNetworkAliases('l1')
|
|
245
|
+
.withExposedPorts(l1HttpPort, l1WsPort)
|
|
246
|
+
.withName(`${containerName}.l1`)
|
|
247
|
+
.withEnvironment({ RUST_LOG })
|
|
248
|
+
.withCommand(
|
|
249
|
+
command({
|
|
250
|
+
port: l1HttpPort,
|
|
251
|
+
// The zone anchors to the L1 over WebSocket.
|
|
252
|
+
ws: [true, { addr: '0.0.0.0', api: 'all', port: l1WsPort }],
|
|
253
|
+
}),
|
|
254
|
+
)
|
|
255
|
+
.withWaitStrategy(
|
|
256
|
+
Wait.forLogMessage(
|
|
257
|
+
/Received (block|new payload) from consensus engine/,
|
|
258
|
+
),
|
|
259
|
+
)
|
|
260
|
+
.withLogConsumer(
|
|
261
|
+
logConsumer(() => {
|
|
262
|
+
// L1 shutdown surfaces via the zone container failing to start.
|
|
263
|
+
}),
|
|
264
|
+
)
|
|
265
|
+
.withStartupTimeout(timeout)
|
|
266
|
+
.start()
|
|
267
|
+
l1RpcUrl = `ws://l1:${l1WsPort}`
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const promise = Promise.withResolvers<void>()
|
|
271
|
+
|
|
272
|
+
let c = new GenericContainer(image)
|
|
273
|
+
.withPullPolicy(PullPolicy.alwaysPull())
|
|
274
|
+
.withPlatform('linux/x86_64')
|
|
275
|
+
.withExposedPorts(containerPort)
|
|
276
|
+
.withExtraHosts([
|
|
277
|
+
{ host: 'host.docker.internal', ipAddress: 'host-gateway' },
|
|
278
|
+
])
|
|
279
|
+
.withName(containerName)
|
|
280
|
+
.withEnvironment({ RUST_LOG })
|
|
281
|
+
.withCommand(
|
|
282
|
+
zoneCommand({
|
|
283
|
+
...args,
|
|
284
|
+
l1: {
|
|
285
|
+
...(l1Parameters?.factoryAddress
|
|
286
|
+
? { factoryAddress: l1Parameters.factoryAddress }
|
|
287
|
+
: {}),
|
|
288
|
+
rpcUrl: l1RpcUrl,
|
|
289
|
+
},
|
|
290
|
+
port: containerPort,
|
|
291
|
+
}),
|
|
292
|
+
)
|
|
293
|
+
.withWaitStrategy(Wait.forLogMessage('RPC HTTP server started'))
|
|
294
|
+
.withLogConsumer(logConsumer(promise.reject))
|
|
295
|
+
.withStartupTimeout(timeout)
|
|
296
|
+
if (network) c = c.withNetwork(network)
|
|
297
|
+
|
|
298
|
+
c.start()
|
|
299
|
+
.then((started) => {
|
|
300
|
+
container = started
|
|
301
|
+
setEndpoint?.({
|
|
302
|
+
host: started.getHost(),
|
|
303
|
+
port: started.getMappedPort(containerPort),
|
|
304
|
+
})
|
|
305
|
+
promise.resolve()
|
|
306
|
+
})
|
|
307
|
+
.catch(promise.reject)
|
|
308
|
+
|
|
309
|
+
await promise.promise
|
|
310
|
+
} catch (error) {
|
|
311
|
+
await teardown()
|
|
312
|
+
throw error
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
async stop() {
|
|
316
|
+
await teardown()
|
|
317
|
+
},
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
export declare namespace tempoZone {
|
|
323
|
+
export type Parameters = Omit<core_tempoZone.Parameters, 'binary' | 'l1'> &
|
|
324
|
+
ContainerOptions.Parameters & {
|
|
325
|
+
/**
|
|
326
|
+
* Name of the container.
|
|
327
|
+
*/
|
|
328
|
+
containerName?: string | undefined
|
|
329
|
+
/**
|
|
330
|
+
* Docker image to use for the zone node.
|
|
331
|
+
*/
|
|
332
|
+
image?: string | undefined
|
|
333
|
+
/**
|
|
334
|
+
* Host the server will listen on.
|
|
335
|
+
*/
|
|
336
|
+
host?: string | undefined
|
|
337
|
+
/**
|
|
338
|
+
* Tempo L1 options.
|
|
339
|
+
*/
|
|
340
|
+
l1?:
|
|
341
|
+
| (NonNullable<core_tempoZone.Parameters['l1']> & {
|
|
342
|
+
/**
|
|
343
|
+
* Docker image to use for the L1 node (when no `rpcUrl` is provided).
|
|
344
|
+
*/
|
|
345
|
+
image?: string | undefined
|
|
346
|
+
/**
|
|
347
|
+
* Existing Tempo L1 WebSocket RPC URL, reachable from inside the
|
|
348
|
+
* container (e.g. `ws://host.docker.internal:8546`). Starts a
|
|
349
|
+
* dev L1 container when omitted.
|
|
350
|
+
*/
|
|
351
|
+
rpcUrl?: string | undefined
|
|
352
|
+
})
|
|
353
|
+
| undefined
|
|
354
|
+
/**
|
|
355
|
+
* Port the server will listen on.
|
|
356
|
+
*/
|
|
357
|
+
port?: number | undefined
|
|
358
|
+
}
|
|
359
|
+
}
|