prool 0.0.3 → 0.0.5

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.
Files changed (52) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/_lib/exports/index.d.ts +1 -1
  3. package/_lib/exports/processes.d.ts +2 -0
  4. package/_lib/exports/processes.d.ts.map +1 -0
  5. package/_lib/exports/processes.js +2 -0
  6. package/_lib/exports/processes.js.map +1 -0
  7. package/_lib/instance.d.ts +9 -6
  8. package/_lib/instance.d.ts.map +1 -1
  9. package/_lib/instance.js +22 -3
  10. package/_lib/instance.js.map +1 -1
  11. package/_lib/instances/anvil.d.ts +1 -5
  12. package/_lib/instances/anvil.d.ts.map +1 -1
  13. package/_lib/instances/anvil.js +18 -44
  14. package/_lib/instances/anvil.js.map +1 -1
  15. package/_lib/instances/stackup.d.ts +86 -0
  16. package/_lib/instances/stackup.d.ts.map +1 -0
  17. package/_lib/instances/stackup.js +89 -0
  18. package/_lib/instances/stackup.js.map +1 -0
  19. package/_lib/pool.d.ts +7 -6
  20. package/_lib/pool.d.ts.map +1 -1
  21. package/_lib/pool.js +60 -53
  22. package/_lib/pool.js.map +1 -1
  23. package/_lib/processes/execa.d.ts +27 -0
  24. package/_lib/processes/execa.d.ts.map +1 -0
  25. package/_lib/processes/execa.js +63 -0
  26. package/_lib/processes/execa.js.map +1 -0
  27. package/_lib/server.d.ts +3 -3
  28. package/_lib/server.d.ts.map +1 -1
  29. package/_lib/server.js +42 -34
  30. package/_lib/server.js.map +1 -1
  31. package/_lib/utils.d.ts +4 -3
  32. package/_lib/utils.d.ts.map +1 -1
  33. package/_lib/utils.js +2 -2
  34. package/_lib/utils.js.map +1 -1
  35. package/exports/index.ts +2 -2
  36. package/exports/processes.test.ts +10 -0
  37. package/exports/processes.ts +8 -0
  38. package/instance.test.ts +34 -2
  39. package/instance.ts +46 -13
  40. package/instances/anvil.test.ts +5 -3
  41. package/instances/anvil.ts +21 -49
  42. package/instances/stackup.test.ts +106 -0
  43. package/instances/stackup.ts +162 -0
  44. package/package.json +6 -2
  45. package/pool.test.ts +203 -138
  46. package/pool.ts +83 -66
  47. package/processes/execa.test.ts +143 -0
  48. package/processes/execa.ts +100 -0
  49. package/server.test.ts +162 -85
  50. package/server.ts +46 -38
  51. package/tsconfig.build.tsbuildinfo +1 -1
  52. package/utils.ts +11 -9
package/instance.ts CHANGED
@@ -8,11 +8,11 @@ type EventTypes = {
8
8
  stdout: [message: string]
9
9
  }
10
10
 
11
- type InstanceStartOptions_internal = {
11
+ export type InstanceStartOptions_internal = {
12
12
  emitter: EventEmitter<EventTypes>
13
13
  status: Instance['status']
14
14
  }
15
- type InstanceStopOptions_internal = {
15
+ export type InstanceStopOptions_internal = {
16
16
  emitter: EventEmitter<EventTypes>
17
17
  status: Instance['status']
18
18
  }
@@ -30,9 +30,10 @@ export type DefineInstanceFn<
30
30
  > = (parameters: parameters) => Pick<Instance, 'host' | 'name' | 'port'> & {
31
31
  _internal?: _internal | undefined
32
32
  start(
33
- options: InstanceStartOptions & InstanceStartOptions_internal,
33
+ options: InstanceStartOptions,
34
+ options_internal: InstanceStartOptions_internal,
34
35
  ): Promise<void>
35
- stop(options: InstanceStopOptions_internal): Promise<void>
36
+ stop(options_internal: InstanceStopOptions_internal): Promise<void>
36
37
  }
37
38
 
38
39
  export type Instance<
@@ -75,16 +76,25 @@ export type Instance<
75
76
  * @example ["Listening on http://127.0.0.1", "Started successfully."]
76
77
  */
77
78
  messages: { clear(): void; get(): string[] }
79
+ /**
80
+ * Retarts the instance.
81
+ */
82
+ restart(): Promise<void>
78
83
  /**
79
84
  * Status of the instance.
80
85
  *
81
86
  * @default "idle"
82
87
  */
83
- status: 'idle' | 'stopped' | 'starting' | 'started' | 'stopping'
88
+ status:
89
+ | 'idle'
90
+ | 'restarting'
91
+ | 'stopped'
92
+ | 'starting'
93
+ | 'started'
94
+ | 'stopping'
84
95
  /**
85
96
  * Starts the instance.
86
97
  *
87
- * @param options - Options for starting the instance.
88
98
  * @returns A function to stop the instance.
89
99
  */
90
100
  start(): Promise<() => void>
@@ -147,8 +157,9 @@ export function defineInstance<
147
157
  ...fn(parameters),
148
158
  ...createParameters,
149
159
  }
150
- const { messageBuffer = 20, timeout = 10_000 } = options
160
+ const { messageBuffer = 20, timeout } = options
151
161
 
162
+ let restartResolver = Promise.withResolvers<void>()
152
163
  let startResolver = Promise.withResolvers<() => void>()
153
164
  let stopResolver = Promise.withResolvers<void>()
154
165
 
@@ -156,6 +167,7 @@ export function defineInstance<
156
167
 
157
168
  let messages: string[] = []
158
169
  let status: Instance['status'] = 'idle'
170
+ let restarting = false
159
171
 
160
172
  function onExit() {
161
173
  status = 'stopped'
@@ -182,6 +194,7 @@ export function defineInstance<
182
194
  name,
183
195
  port,
184
196
  get status() {
197
+ if (restarting) return 'restarting'
185
198
  return status
186
199
  },
187
200
  async start() {
@@ -205,16 +218,20 @@ export function defineInstance<
205
218
  emitter.on('exit', onExit)
206
219
 
207
220
  status = 'starting'
208
- start({
209
- emitter,
210
- port,
211
- status: this.status,
212
- })
221
+ start(
222
+ {
223
+ port,
224
+ },
225
+ {
226
+ emitter,
227
+ status: this.status,
228
+ },
229
+ )
213
230
  .then(() => {
214
231
  status = 'started'
215
232
 
216
233
  stopResolver = Promise.withResolvers<void>()
217
- startResolver.resolve(this.stop)
234
+ startResolver.resolve(this.stop.bind(this))
218
235
  })
219
236
  .catch((error) => {
220
237
  status = 'idle'
@@ -262,6 +279,22 @@ export function defineInstance<
262
279
 
263
280
  return stopResolver.promise
264
281
  },
282
+ async restart() {
283
+ if (restarting) return restartResolver.promise
284
+
285
+ restarting = true
286
+
287
+ this.stop()
288
+ .then(() => this.start.bind(this)())
289
+ .then(() => restartResolver.resolve())
290
+ .catch(restartResolver.reject)
291
+ .finally(() => {
292
+ restartResolver = Promise.withResolvers<void>()
293
+ restarting = false
294
+ })
295
+
296
+ return restartResolver.promise
297
+ },
265
298
 
266
299
  addListener: emitter.addListener.bind(emitter),
267
300
  off: emitter.off.bind(emitter),
@@ -47,7 +47,7 @@ test('behavior: instance errored (duplicate ports)', async () => {
47
47
 
48
48
  await instance_1.start()
49
49
  await expect(() => instance_2.start()).rejects.toThrowError(
50
- 'Failed to start anvil',
50
+ 'Failed to start process "anvil"',
51
51
  )
52
52
  })
53
53
 
@@ -81,7 +81,9 @@ test('behavior: can subscribe to stderr', async () => {
81
81
 
82
82
  await instance_1.start()
83
83
  instance_2.on('stderr', (message) => messages.push(message))
84
- await expect(instance_2.start()).rejects.toThrow('Failed to start anvil')
84
+ await expect(instance_2.start()).rejects.toThrow(
85
+ 'Failed to start process "anvil"',
86
+ )
85
87
  })
86
88
 
87
89
  test('behavior: starts anvil with custom options', async () => {
@@ -122,6 +124,6 @@ test('behavior: exit when status is starting', async () => {
122
124
  instance._internal.process.kill()
123
125
 
124
126
  await expect(promise).rejects.toThrowErrorMatchingInlineSnapshot(
125
- '[Error: Failed to start anvil: exited.]',
127
+ '[Error: Failed to start process "anvil": exited.]',
126
128
  )
127
129
  })
@@ -1,6 +1,5 @@
1
- import { type ResultPromise, execa } from 'execa'
2
1
  import { defineInstance } from '../instance.js'
3
- import { stripColors } from '../utils.js'
2
+ import { execa } from '../processes/execa.js'
4
3
  import { toArgs } from '../utils.js'
5
4
 
6
5
  export type AnvilParameters = {
@@ -259,64 +258,37 @@ export type AnvilParameters = {
259
258
  export const anvil = defineInstance((parameters?: AnvilParameters) => {
260
259
  const { binary = 'anvil', ...args } = parameters || {}
261
260
 
262
- let process: ResultPromise<{ cleanup: true; reject: false }>
263
-
264
- async function stop() {
265
- const killed = process.kill()
266
- if (!killed) throw new Error('Failed to stop anvil')
267
- return new Promise((resolve) => process.on('close', resolve))
268
- }
261
+ const name = 'anvil'
262
+ const process = execa({ name })
269
263
 
270
264
  return {
271
265
  _internal: {
272
266
  args,
273
267
  get process() {
274
- return process
268
+ return process._internal.process
275
269
  },
276
270
  },
277
271
  host: args.host ?? 'localhost',
278
- name: 'anvil',
272
+ name,
279
273
  port: args.port ?? 8545,
280
- async start({ emitter, port = args.port, status }) {
281
- const { promise, resolve, reject } = Promise.withResolvers<void>()
282
-
283
- process = execa(binary, toArgs({ ...args, port }), {
284
- cleanup: true,
285
- reject: false,
286
- })
287
-
288
- process.stdout.on('data', (data) => {
289
- const message = stripColors(data.toString())
290
- emitter.emit('message', message)
291
- emitter.emit('stdout', message)
292
- if (message.includes('Listening on')) {
293
- emitter.emit('listening')
294
- resolve()
295
- }
296
- })
297
- process.stderr.on('data', async (data) => {
298
- const message = stripColors(data.toString())
299
- emitter.emit('message', message)
300
- emitter.emit('stderr', message)
301
- await stop()
302
- reject(new Error(`Failed to start anvil: ${data.toString()}`))
303
- })
304
- process.on('close', () => process.removeAllListeners())
305
- process.on('exit', (code, signal) => {
306
- emitter.emit('exit', code, signal)
307
-
308
- if (!code) {
309
- process.removeAllListeners()
310
- if (status === 'starting')
311
- reject(new Error('Failed to start anvil: exited.'))
312
- }
313
- })
314
-
315
- return promise
274
+ async start({ port = args.port }, options) {
275
+ return await process.start(
276
+ ($) => $`${binary} ${toArgs({ ...args, port })}`,
277
+ {
278
+ ...options,
279
+ // Resolve when the process is listening via a "Listening on" message.
280
+ resolver({ process, reject, resolve }) {
281
+ process.stdout.on('data', (data) => {
282
+ const message = data.toString()
283
+ if (message.includes('Listening on')) resolve()
284
+ })
285
+ process.stderr.on('data', reject)
286
+ },
287
+ },
288
+ )
316
289
  },
317
290
  async stop() {
318
- process.removeAllListeners()
319
- await stop()
291
+ await process.stop()
320
292
  },
321
293
  }
322
294
  })
@@ -0,0 +1,106 @@
1
+ import getPort from 'get-port'
2
+ import { afterEach, beforeAll, expect, test } from 'vitest'
3
+
4
+ import { stackupOptions } from '../../test/utils.js'
5
+ import type { Instance } from '../instance.js'
6
+ import { anvil } from './anvil.js'
7
+ import { type StackupParameters, stackup } from './stackup.js'
8
+
9
+ const instances: Instance[] = []
10
+
11
+ const port = await getPort()
12
+
13
+ const defineInstance = (parameters?: Partial<StackupParameters>) => {
14
+ const instance = stackup({
15
+ ...stackupOptions({ port }),
16
+ ...parameters,
17
+ })
18
+ instances.push(instance)
19
+ return instance
20
+ }
21
+
22
+ beforeAll(() => anvil({ port }).start())
23
+
24
+ afterEach(async () => {
25
+ for (const instance of instances) await instance.stop().catch(() => {})
26
+ })
27
+
28
+ test('default', async () => {
29
+ const messages: string[] = []
30
+ const stdouts: string[] = []
31
+
32
+ const instance = defineInstance()
33
+
34
+ instance.on('message', (m) => messages.push(m))
35
+ instance.on('stdout', (m) => stdouts.push(m))
36
+
37
+ expect(instance.messages.get()).toMatchInlineSnapshot('[]')
38
+
39
+ await instance.start()
40
+ expect(instance.status).toEqual('started')
41
+
42
+ expect(messages.join('')).toBeDefined()
43
+ expect(stdouts.join('')).toBeDefined()
44
+ expect(instance.messages.get().join('')).toBeDefined()
45
+
46
+ await instance.stop()
47
+ expect(instance.status).toEqual('stopped')
48
+
49
+ expect(messages.join('')).toBeDefined()
50
+ expect(stdouts.join('')).toBeDefined()
51
+ expect(instance.messages.get()).toMatchInlineSnapshot('[]')
52
+ })
53
+
54
+ test('behavior: instance errored (duplicate ports)', async () => {
55
+ const instance_1 = defineInstance({
56
+ port: 1337,
57
+ })
58
+ const instance_2 = defineInstance({
59
+ port: 1337,
60
+ })
61
+
62
+ await instance_1.start()
63
+ await expect(() => instance_2.start()).rejects.toThrowError()
64
+ })
65
+
66
+ test('behavior: start and stop multiple times', async () => {
67
+ const instance = defineInstance()
68
+
69
+ await instance.start()
70
+ await instance.stop()
71
+ await instance.start()
72
+ await instance.stop()
73
+ await instance.start()
74
+ await instance.stop()
75
+ await instance.start()
76
+ await instance.stop()
77
+ })
78
+
79
+ test('behavior: can subscribe to stderr', async () => {
80
+ const messages: string[] = []
81
+
82
+ const instance_1 = defineInstance({ port: 1337 })
83
+ const instance_2 = defineInstance({ port: 1337 })
84
+
85
+ await instance_1.start()
86
+ instance_2.on('stderr', (message) => messages.push(message))
87
+ await expect(instance_2.start()).rejects.toThrowError()
88
+ })
89
+
90
+ test('behavior: exit', async () => {
91
+ const instance = defineInstance()
92
+
93
+ let exitCode: number | null | undefined = undefined
94
+ instance.on('exit', (code) => {
95
+ exitCode = code
96
+ })
97
+
98
+ await instance.start()
99
+ expect(instance.status).toEqual('started')
100
+
101
+ instance._internal.process.kill()
102
+
103
+ await new Promise<void>((res) => setTimeout(res, 100))
104
+ expect(instance.status).toEqual('stopped')
105
+ expect(typeof exitCode !== 'undefined').toBeTruthy()
106
+ })
@@ -0,0 +1,162 @@
1
+ import { defineInstance } from '../instance.js'
2
+ import { execa } from '../processes/execa.js'
3
+
4
+ export type StackupParameters = {
5
+ /**
6
+ * The name of the native tracer to use during validation. This will be "bundlerCollectorTracer" if using builds from ERC-4337 execution client repo.
7
+ * Defaults to address of `privateKey`.
8
+ */
9
+ beneficiary?: string | undefined
10
+ /**
11
+ * Directory to store the embedded database.
12
+ * @default /tmp/stackup_bundler
13
+ */
14
+ dataDirectory?: string | undefined
15
+ /**
16
+ * RPC url to the execution client.
17
+ */
18
+ ethClientUrl: string
19
+ /**
20
+ * A boolean value for bundlers on an Arbitrum stack network to properly account for the L1 callData cost.
21
+ * @default false
22
+ */
23
+ isArbStackNetwork?: boolean | undefined
24
+ /**
25
+ * A boolean value for bundlers on an OP stack network to properly account for the L1 callData cost.
26
+ * @default false
27
+ */
28
+ isOpStackNetwork?: boolean | undefined
29
+ /**
30
+ * A boolean value for bundlers on a network that supports RIP-7212 precompile for secp256r1 signature verification.
31
+ * @default false
32
+ */
33
+ isRip7212Supported?: boolean | undefined
34
+ /**
35
+ * The maximum gas limit that can be submitted per UserOperation batch.
36
+ * @default 18_000_000
37
+ */
38
+ maxBatchGasLimit?: number | undefined
39
+ /**
40
+ * The maximum duration that a userOp can stay in the mempool before getting dropped.
41
+ * @default 180
42
+ */
43
+ maxOpTtlSeconds?: number | undefined
44
+ /**
45
+ * The maximum verificationGasLimit on a received UserOperation.
46
+ * @default 6_000_000
47
+ */
48
+ maxVerificationGas?: number | undefined
49
+ /**
50
+ * The maximum block range when looking up a User Operation with eth_getUserOperationReceipt or eth_getUserOperationByHash.
51
+ *
52
+ * Higher limits allow for fetching older User Operations but will result in higher request latency due to additional compute on the underlying node.
53
+ *
54
+ * @default 2_000
55
+ */
56
+ opLookupLimit?: number | undefined
57
+ /**
58
+ * Port to start the instance on.
59
+ */
60
+ port?: number | undefined
61
+ /**
62
+ * The private key for the EOA used to relay User Operation bundles to the EntryPoint.
63
+ */
64
+ privateKey: string
65
+ /**
66
+ * EntryPoint addresses to support. The first address is the preferred EntryPoint.
67
+ */
68
+ supportedEntryPoints?: string[]
69
+ }
70
+
71
+ /**
72
+ * Defines an Anvil instance.
73
+ *
74
+ * @example
75
+ * ```ts
76
+ * const instance = stackup({
77
+ * ethClientUrl: 'http://localhost:8545',
78
+ * port: 4337,
79
+ * privateKey: '0x...'
80
+ * })
81
+ * await instance.start()
82
+ * // ...
83
+ * await instance.stop()
84
+ * ```
85
+ */
86
+ export const stackup = defineInstance((parameters?: StackupParameters) => {
87
+ const args = (parameters || {}) as StackupParameters
88
+
89
+ const host = 'localhost'
90
+ const name = 'stackup'
91
+ const process = execa({ name })
92
+
93
+ return {
94
+ _internal: {
95
+ args,
96
+ get process() {
97
+ return process._internal.process
98
+ },
99
+ },
100
+ host,
101
+ name,
102
+ port: args.port ?? 4337,
103
+ async start({ port = args.port }, options) {
104
+ const args_ = [
105
+ '--net',
106
+ 'host',
107
+ '--add-host',
108
+ 'host.docker.internal:host-gateway',
109
+ '--add-host',
110
+ 'localhost:host-gateway',
111
+ '-p',
112
+ `${port}:${port}`,
113
+ '-e',
114
+ `ERC4337_BUNDLER_PORT=${port}`,
115
+ ...Object.entries(args).flatMap(([key, value]) => {
116
+ if (key === 'port') return []
117
+ if (value === undefined) return []
118
+
119
+ if (key === 'ethClientUrl')
120
+ value = (value as string).replaceAll(
121
+ /127\.0\.0\.1|0\.0\.0\.0/g,
122
+ 'host.docker.internal',
123
+ )
124
+ if (key === 'privateKey') value = (value as string).replace('0x', '')
125
+
126
+ return [
127
+ '-e',
128
+ `ERC4337_BUNDLER_${key
129
+ .replace(/([a-z])([A-Z])/g, '$1_$2')
130
+ .toUpperCase()}=${value}`,
131
+ ]
132
+ }),
133
+ 'stackupwallet/stackup-bundler:latest',
134
+ '/app/stackup-bundler',
135
+ 'start',
136
+ '--mode',
137
+ 'private',
138
+ ]
139
+ return await process.start(($) => $`docker run ${args_}`, {
140
+ ...options,
141
+ resolver({ process, resolve, reject }) {
142
+ process.stderr.on('data', async (data) => {
143
+ const message = data.toString()
144
+ // For some reason, `stackup-bundler` logs to stderr. So we have to try
145
+ // and dissect what is an error, and what is not. 😅
146
+ if (message.includes('Set nextTxnTs to'))
147
+ setTimeout(() => resolve(), 100)
148
+ else if (
149
+ message
150
+ .toLowerCase()
151
+ .match(/panic|error|connection refused|address already in use/)
152
+ )
153
+ reject(data)
154
+ })
155
+ },
156
+ })
157
+ },
158
+ async stop() {
159
+ await process.stop()
160
+ },
161
+ }
162
+ })
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "prool",
3
- "description": "",
4
- "version": "0.0.3",
3
+ "description": "HTTP testing instances for Ethereum",
4
+ "version": "0.0.5",
5
5
  "type": "module",
6
6
  "module": "_lib/exports/index.mjs",
7
7
  "types": "_lib/exports/index.d.ts",
@@ -15,6 +15,10 @@
15
15
  "types": "./_lib/exports/index.d.ts",
16
16
  "default": "./_lib/exports/index.js"
17
17
  },
18
+ "./processes": {
19
+ "types": "./_lib/exports/processes.d.ts",
20
+ "default": "./_lib/exports/processes.js"
21
+ },
18
22
  "./instances": {
19
23
  "types": "./_lib/exports/instances.d.ts",
20
24
  "default": "./_lib/exports/instances.js"