prool 0.0.0 → 0.0.2

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 (46) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +21 -0
  3. package/_lib/exports/index.d.ts +4 -0
  4. package/_lib/exports/index.d.ts.map +1 -0
  5. package/_lib/exports/index.js +4 -0
  6. package/_lib/exports/index.js.map +1 -0
  7. package/_lib/exports/instances.d.ts +2 -0
  8. package/_lib/exports/instances.d.ts.map +1 -0
  9. package/_lib/exports/instances.js +2 -0
  10. package/_lib/exports/instances.js.map +1 -0
  11. package/_lib/instance.d.ts +111 -0
  12. package/_lib/instance.d.ts.map +1 -0
  13. package/_lib/instance.js +141 -0
  14. package/_lib/instance.js.map +1 -0
  15. package/_lib/instances/anvil.d.ts +459 -0
  16. package/_lib/instances/anvil.d.ts.map +1 -0
  17. package/_lib/instances/anvil.js +74 -0
  18. package/_lib/instances/anvil.js.map +1 -0
  19. package/_lib/pool.d.ts +38 -0
  20. package/_lib/pool.d.ts.map +1 -0
  21. package/_lib/pool.js +134 -0
  22. package/_lib/pool.js.map +1 -0
  23. package/_lib/server.d.ts +21 -0
  24. package/_lib/server.d.ts.map +1 -0
  25. package/_lib/server.js +93 -0
  26. package/_lib/server.js.map +1 -0
  27. package/_lib/utils.d.ts +35 -0
  28. package/_lib/utils.d.ts.map +1 -0
  29. package/_lib/utils.js +71 -0
  30. package/_lib/utils.js.map +1 -0
  31. package/exports/index.test.ts +12 -0
  32. package/exports/index.ts +20 -0
  33. package/exports/instances.test.ts +10 -0
  34. package/exports/instances.ts +1 -0
  35. package/instance.test.ts +311 -0
  36. package/instance.ts +277 -0
  37. package/instances/anvil.test.ts +127 -0
  38. package/instances/anvil.ts +322 -0
  39. package/package.json +39 -8
  40. package/pool.test.ts +209 -0
  41. package/pool.ts +183 -0
  42. package/server.test.ts +294 -0
  43. package/server.ts +135 -0
  44. package/tsconfig.build.tsbuildinfo +1 -0
  45. package/utils.test.ts +44 -0
  46. package/utils.ts +92 -0
@@ -0,0 +1,322 @@
1
+ import { type ResultPromise, execa } from 'execa'
2
+ import { defineInstance } from '../instance.js'
3
+ import { stripColors } from '../utils.js'
4
+ import { toArgs } from '../utils.js'
5
+
6
+ export type AnvilParameters = {
7
+ /**
8
+ * Number of dev accounts to generate and configure.
9
+ *
10
+ * @defaultValue 10
11
+ */
12
+ accounts?: number | undefined
13
+ /**
14
+ * Set the Access-Control-Allow-Origin response header (CORS).
15
+ *
16
+ * @defaultValue *
17
+ */
18
+ allowOrigin?: string | undefined
19
+ /**
20
+ * Enable autoImpersonate on startup
21
+ */
22
+ autoImpersonate?: boolean | undefined
23
+ /**
24
+ * The balance of every dev account in Ether.
25
+ *
26
+ * @defaultValue 10000
27
+ */
28
+ balance?: number | bigint | undefined
29
+ /**
30
+ * The base fee in a block.
31
+ */
32
+ blockBaseFeePerGas?: number | bigint | undefined
33
+ /**
34
+ * Block time in seconds for interval mining.
35
+ */
36
+ blockTime?: number | undefined
37
+ /**
38
+ * Path or alias to the Anvil binary.
39
+ */
40
+ binary?: string | undefined
41
+ /**
42
+ * The chain id.
43
+ */
44
+ chainId?: number | undefined
45
+ /**
46
+ * EIP-170: Contract code size limit in bytes. Useful to increase this because of tests.
47
+ *
48
+ * @defaultValue 0x6000 (~25kb)
49
+ */
50
+ codeSizeLimit?: number | undefined
51
+ /**
52
+ * Sets the number of assumed available compute units per second for this fork provider.
53
+ *
54
+ * @defaultValue 350
55
+ * @see https://github.com/alchemyplatform/alchemy-docs/blob/master/documentation/compute-units.md#rate-limits-cups
56
+ */
57
+ computeUnitsPerSecond?: number | undefined
58
+ /**
59
+ * Writes output of `anvil` as json to user-specified file.
60
+ */
61
+ configOut?: string | undefined
62
+ /**
63
+ * Sets the derivation path of the child key to be derived.
64
+ *
65
+ * @defaultValue m/44'/60'/0'/0/
66
+ */
67
+ derivationPath?: string | undefined
68
+ /**
69
+ * Disable the `call.gas_limit <= block.gas_limit` constraint.
70
+ */
71
+ disableBlockGasLimit?: boolean | undefined
72
+ /**
73
+ * Dump the state of chain on exit to the given file. If the value is a directory, the state will be
74
+ * written to `<VALUE>/state.json`.
75
+ */
76
+ dumpState?: string | undefined
77
+ /**
78
+ * Fetch state over a remote endpoint instead of starting from an empty state.
79
+ *
80
+ * If you want to fetch state from a specific block number, add a block number like `http://localhost:8545@1400000`
81
+ * or use the `forkBlockNumber` option.
82
+ */
83
+ forkUrl?: string | undefined
84
+ /**
85
+ * Fetch state from a specific block number over a remote endpoint.
86
+ *
87
+ * Requires `forkUrl` to be set.
88
+ */
89
+ forkBlockNumber?: number | bigint | undefined
90
+ /**
91
+ * Specify chain id to skip fetching it from remote endpoint. This enables offline-start mode.
92
+ *
93
+ * You still must pass both `forkUrl` and `forkBlockNumber`, and already have your required state cached
94
+ * on disk, anything missing locally would be fetched from the remote.
95
+ */
96
+ forkChainId?: number | undefined
97
+ /**
98
+ * Specify headers to send along with any request to the remote JSON-RPC server in forking mode.
99
+ *
100
+ * e.g. "User-Agent: test-agent"
101
+ *
102
+ * Requires `forkUrl` to be set.
103
+ */
104
+ forkHeader?: Record<string, string> | undefined
105
+ /**
106
+ * Initial retry backoff on encountering errors.
107
+ */
108
+ forkRetryBackoff?: number | undefined
109
+ /**
110
+ * The block gas limit.
111
+ */
112
+ gasLimit?: number | bigint | undefined
113
+ /**
114
+ * The gas price.
115
+ */
116
+ gasPrice?: number | bigint | undefined
117
+ /**
118
+ * The EVM hardfork to use.
119
+ */
120
+ hardfork?:
121
+ | 'Frontier'
122
+ | 'Homestead'
123
+ | 'Dao'
124
+ | 'Tangerine'
125
+ | 'SpuriousDragon'
126
+ | 'Byzantium'
127
+ | 'Constantinople'
128
+ | 'Petersburg'
129
+ | 'Istanbul'
130
+ | 'Muirglacier'
131
+ | 'Berlin'
132
+ | 'London'
133
+ | 'ArrowGlacier'
134
+ | 'GrayGlacier'
135
+ | 'Paris'
136
+ | 'Shanghai'
137
+ | 'Cancun'
138
+ | 'Latest'
139
+ | undefined
140
+ /**
141
+ * The host the server will listen on.
142
+ */
143
+ host?: string | undefined
144
+ /**
145
+ * Initialize the genesis block with the given `genesis.json` file.
146
+ */
147
+ init?: string | undefined
148
+ /**
149
+ * Launch an ipc server at the given path or default path = `/tmp/anvil.ipc`.
150
+ */
151
+ ipc?: string | undefined
152
+ /**
153
+ * Initialize the chain from a previously saved state snapshot.
154
+ */
155
+ loadState?: string | undefined
156
+ /**
157
+ * BIP39 mnemonic phrase used for generating accounts.
158
+ */
159
+ mnemonic?: string | undefined
160
+ /**
161
+ * Automatically generates a BIP39 mnemonic phrase, and derives accounts from it.
162
+ */
163
+ mnemonicRandom?: boolean | undefined
164
+ /**
165
+ * Disable CORS.
166
+ */
167
+ noCors?: boolean | undefined
168
+ /**
169
+ * Disable auto and interval mining, and mine on demand instead.
170
+ */
171
+ noMining?: boolean | undefined
172
+ /**
173
+ * Disables rate limiting for this node's provider.
174
+ *
175
+ * @defaultValue false
176
+ * @see https://github.com/alchemyplatform/alchemy-docs/blob/master/documentation/compute-units.md#rate-limits-cups
177
+ */
178
+ noRateLimit?: boolean | undefined
179
+ /**
180
+ * Explicitly disables the use of RPC caching.
181
+ *
182
+ * All storage slots are read entirely from the endpoint.
183
+ */
184
+ noStorageCaching?: boolean | undefined
185
+ /**
186
+ * How transactions are sorted in the mempool.
187
+ *
188
+ * @defaultValue fees
189
+ */
190
+ order?: string | undefined
191
+ /**
192
+ * Run an Optimism chain.
193
+ */
194
+ optimism?: boolean | undefined
195
+ /**
196
+ * Port number to listen on.
197
+ *
198
+ * @defaultValue 8545
199
+ */
200
+ port?: number | undefined
201
+ /**
202
+ * Don't keep full chain history. If a number argument is specified, at most this number of states is kept in memory.
203
+ */
204
+ pruneHistory?: number | undefined | boolean
205
+ /**
206
+ * Number of retry requests for spurious networks (timed out requests).
207
+ *
208
+ * @defaultValue 5
209
+ */
210
+ retries?: number | undefined
211
+ /**
212
+ * Don't print anything on startup and don't print logs.
213
+ */
214
+ silent?: boolean | undefined
215
+ /**
216
+ * Slots in an epoch.
217
+ */
218
+ slotsInAnEpoch?: number | undefined
219
+ /**
220
+ * Enable steps tracing used for debug calls returning geth-style traces.
221
+ */
222
+ stepsTracing?: boolean | undefined
223
+ /**
224
+ * Interval in seconds at which the status is to be dumped to disk.
225
+ */
226
+ stateInterval?: number | undefined
227
+ /**
228
+ * This is an alias for both `loadState` and `dumpState`. It initializes the chain with the state stored at the
229
+ * file, if it exists, and dumps the chain's state on exit
230
+ */
231
+ state?: string | undefined
232
+ /**
233
+ * Timeout in ms for requests sent to remote JSON-RPC server in forking mode.
234
+ *
235
+ * @defaultValue 45000
236
+ */
237
+ timeout?: number | undefined
238
+ /**
239
+ * The timestamp of the genesis block.
240
+ */
241
+ timestamp?: number | bigint | undefined
242
+ /**
243
+ * Number of blocks with transactions to keep in memory.
244
+ */
245
+ transactionBlockKeeper?: number | undefined
246
+ }
247
+
248
+ /**
249
+ * Defines an Anvil instance.
250
+ *
251
+ * @example
252
+ * ```ts
253
+ * const instance = anvil({ forkRpcUrl: 'https://cloudflare-eth.com', port: 8546 })
254
+ * await instance.start()
255
+ * // ...
256
+ * await instance.stop()
257
+ * ```
258
+ */
259
+ export const anvil = defineInstance((parameters?: AnvilParameters) => {
260
+ const { binary = 'anvil', ...args } = parameters || {}
261
+
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
+ }
269
+
270
+ return {
271
+ _internal: {
272
+ args,
273
+ get process() {
274
+ return process
275
+ },
276
+ },
277
+ host: args.host ?? 'localhost',
278
+ name: 'anvil',
279
+ 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
316
+ },
317
+ async stop() {
318
+ process.removeAllListeners()
319
+ await stop()
320
+ },
321
+ }
322
+ })
package/package.json CHANGED
@@ -1,12 +1,43 @@
1
1
  {
2
2
  "name": "prool",
3
- "version": "0.0.0",
4
3
  "description": "",
5
- "main": "index.js",
6
- "keywords": [],
7
- "author": "",
8
- "license": "ISC",
9
- "scripts": {
10
- "test": "echo \"Error: no test specified\" && exit 1"
11
- }
4
+ "version": "0.0.2",
5
+ "type": "module",
6
+ "module": "_lib/exports/index.mjs",
7
+ "types": "_lib/exports/index.d.ts",
8
+ "typings": "_lib/exports/index.d.ts",
9
+ "sideEffects": false,
10
+ "engines": {
11
+ "node": ">=22"
12
+ },
13
+ "exports": {
14
+ ".": {
15
+ "types": "./_lib/exports/index.d.ts",
16
+ "default": "./_lib/exports/index.js"
17
+ },
18
+ "./instances": {
19
+ "types": "./_lib/exports/instances.d.ts",
20
+ "default": "./_lib/exports/instances.js"
21
+ }
22
+ },
23
+ "dependencies": {
24
+ "eventemitter3": "^5.0.1",
25
+ "execa": "^9.1.0",
26
+ "get-port": "^7.1.0",
27
+ "http-proxy": "^1.18.1"
28
+ },
29
+ "license": "MIT",
30
+ "homepage": "https://wevm.dev",
31
+ "repository": "wevm/prool",
32
+ "authors": [
33
+ "awkweb",
34
+ "jxom"
35
+ ],
36
+ "funding": [
37
+ {
38
+ "type": "github",
39
+ "url": "https://github.com/sponsors/wevm"
40
+ }
41
+ ],
42
+ "keywords": []
12
43
  }
package/pool.test.ts ADDED
@@ -0,0 +1,209 @@
1
+ import getPort from 'get-port'
2
+ import { afterEach, describe, expect, test } from 'vitest'
3
+ import { anvil } from './instances/anvil.js'
4
+ import { definePool } from './pool.js'
5
+
6
+ let pool: ReturnType<typeof definePool>
7
+
8
+ afterEach(async () => {
9
+ try {
10
+ await pool.stopAll()
11
+ } catch (err) {
12
+ console.error(err)
13
+ }
14
+ })
15
+
16
+ describe.each([{ instance: anvil() }])(
17
+ 'instance: $instance.name',
18
+ ({ instance }) => {
19
+ test('default', async () => {
20
+ pool = definePool({
21
+ instance,
22
+ })
23
+
24
+ expect(pool).toBeDefined()
25
+ })
26
+
27
+ test('start', async () => {
28
+ pool = definePool({
29
+ instance,
30
+ })
31
+
32
+ expect(pool.size).toEqual(0)
33
+
34
+ const instance_1 = await pool.start(1)
35
+ expect(instance_1.status).toBe('started')
36
+ expect(pool.size).toEqual(1)
37
+
38
+ const instance_2 = await pool.start(2)
39
+ expect(instance_2.status).toBe('started')
40
+ expect(pool.size).toEqual(2)
41
+
42
+ const instance_3 = await pool.start(1337)
43
+ expect(instance_3.status).toBe('started')
44
+ expect(pool.size).toEqual(3)
45
+ })
46
+
47
+ test('stop / destroy', async () => {
48
+ pool = definePool({
49
+ instance,
50
+ })
51
+
52
+ const instance_1 = await pool.start(1)
53
+ const instance_2 = await pool.start(2)
54
+ const instance_3 = await pool.start(3)
55
+
56
+ expect(instance_1.status).toBe('started')
57
+ expect(instance_2.status).toBe('started')
58
+ expect(instance_3.status).toBe('started')
59
+ expect(pool.size).toEqual(3)
60
+
61
+ await pool.stop(1)
62
+ expect(instance_1.status).toBe('stopped')
63
+
64
+ await pool.stop(2)
65
+ expect(instance_2.status).toBe('stopped')
66
+
67
+ await pool.stop(3)
68
+ expect(instance_3.status).toBe('stopped')
69
+
70
+ await pool.stop(1)
71
+ await pool.stop(2)
72
+ await pool.stop(3)
73
+ await pool.stop(4)
74
+
75
+ expect(pool.size).toEqual(3)
76
+
77
+ await pool.destroy(1)
78
+ expect(pool.size).toEqual(2)
79
+ await pool.destroy(2)
80
+ expect(pool.size).toEqual(1)
81
+ await pool.destroy(3)
82
+ expect(pool.size).toEqual(0)
83
+ })
84
+
85
+ test('start > stop > start', async () => {
86
+ pool = definePool({
87
+ instance,
88
+ })
89
+
90
+ const instance_1 = await pool.start(1)
91
+ expect(instance_1.status).toBe('started')
92
+
93
+ await pool.stop(1)
94
+ expect(instance_1.status).toBe('stopped')
95
+
96
+ await pool.start(1)
97
+ expect(instance_1.status).toBe('started')
98
+ })
99
+
100
+ test('stopAll / destroyAll', async () => {
101
+ pool = definePool({
102
+ instance,
103
+ })
104
+
105
+ await pool.start(1)
106
+ await pool.start(2)
107
+ await pool.start(3)
108
+
109
+ expect(pool.size).toEqual(3)
110
+
111
+ await pool.stopAll()
112
+ expect(pool.size).toEqual(3)
113
+
114
+ await pool.destroyAll()
115
+ expect(pool.size).toEqual(0)
116
+ })
117
+
118
+ test('get', async () => {
119
+ pool = definePool({
120
+ instance,
121
+ })
122
+
123
+ const instance_1 = await pool.start(1)
124
+ const instance_2 = await pool.start(2)
125
+ const instance_3 = await pool.start(3)
126
+
127
+ expect(pool.get(1)).toStrictEqual(instance_1)
128
+ expect(pool.get(2)).toStrictEqual(instance_2)
129
+ expect(pool.get(3)).toStrictEqual(instance_3)
130
+ })
131
+
132
+ test('behavior: start more than once', async () => {
133
+ pool = definePool({
134
+ instance,
135
+ })
136
+
137
+ const promise_1 = pool.start(1)
138
+ const promise_2 = pool.start(1)
139
+ expect(promise_1).toStrictEqual(promise_2)
140
+
141
+ const instance_1 = await promise_1
142
+ const instance_2 = await promise_2
143
+ expect(instance_1).toStrictEqual(instance_2)
144
+ })
145
+
146
+ test('behavior: clear more than once', async () => {
147
+ pool = definePool({
148
+ instance,
149
+ })
150
+
151
+ await pool.start(1)
152
+ await pool.start(2)
153
+ await pool.start(3)
154
+
155
+ const promise_1 = pool.stopAll()
156
+ const promise_2 = pool.stopAll()
157
+ expect(promise_1).toStrictEqual(promise_2)
158
+
159
+ await promise_1
160
+ await promise_2
161
+ })
162
+
163
+ test('behavior: stop more than once', async () => {
164
+ pool = definePool({
165
+ instance,
166
+ })
167
+
168
+ await pool.start(1)
169
+
170
+ const promise_1 = pool.stop(1)
171
+ const promise_2 = pool.stop(1)
172
+ expect(promise_1).toStrictEqual(promise_2)
173
+
174
+ await promise_1
175
+ await promise_2
176
+ })
177
+
178
+ test('error: start more than once on same port', async () => {
179
+ const port = await getPort()
180
+
181
+ pool = definePool({
182
+ instance,
183
+ })
184
+
185
+ await pool.start(1, { port })
186
+
187
+ const promise_1 = pool.start(2, { port })
188
+ const promise_2 = pool.start(2, { port })
189
+ expect(promise_1).toStrictEqual(promise_2)
190
+
191
+ await expect(() => promise_1).rejects.toThrowError()
192
+ await expect(() => promise_2).rejects.toThrowError()
193
+ })
194
+
195
+ test('error: instance limit reached', async () => {
196
+ pool = definePool({
197
+ instance,
198
+ limit: 2,
199
+ })
200
+
201
+ await pool.start(1)
202
+ await pool.start(2)
203
+
204
+ await expect(() => pool.start(3)).rejects.toThrowError(
205
+ 'Instance limit of 2 reached.',
206
+ )
207
+ })
208
+ },
209
+ )