Skip to content

wevm/prool

Repository files navigation


mipd logo

HTTP testing instances for Ethereum

Version MIT License Downloads per month

Introduction

Prool is a library that provides programmatic HTTP testing instances for Ethereum. It is designed to be used in testing environments (e.g. Vitest) where you need to interact with an Ethereum server instance (e.g. Execution Node, 4337 Bundler, Indexer, etc) over HTTP or WebSocket.

Prool contains a set of pre-configured instances that can be used to simulate Ethereum server environments, being:

You can also create your own custom instances by using the Instance.define function.

Table of Contents

Install

npm i prool
pnpm add prool
bun i prool

Getting Started

Anvil (Execution Node)

Requirements

curl -L https://foundry.paradigm.xyz | bash   # Install Foundry

Usage

import { Instance, Server } from 'prool'

const server = Server.create({
  instance: Instance.anvil(),
})

await server.start() 
// Instances accessible at:
// "http://localhost:8545/1"
// "http://localhost:8545/2"
// "http://localhost:8545/3"
// "http://localhost:8545/n"

Tempo (Execution Node)

Requirements

curl -L https://tempo.xyz/install | bash   # Install Tempo

Usage

import { Instance, Server } from 'prool'

const server = Server.create({
  instance: Instance.tempo(),
})

await server.start() 
// Instances accessible at:
// "http://localhost:8545/1"
// "http://localhost:8545/2"
// "http://localhost:8545/3"
// "http://localhost:8545/n"

Alto (Bundler Node)

Requirements

Usage

import { Instance, Server } from 'prool'

const executionServer = Server.create({
  instance: Instance.anvil(),
  port: 8545
})
await executionServer.start() 
// Instances accessible at:
// "http://localhost:8545/1"
// "http://localhost:8545/2"
// "http://localhost:8545/3"
// "http://localhost:8545/n"

const bundlerServer = Server.create({
  instance: (key) => Instance.alto({
    entrypoints: ['0x0000000071727De22E5E9d8BAf0edAc6f37da032'],
    rpcUrl: `http://localhost:8545/${key}`,
    executorPrivateKeys: ['0x...'],
  })
})
await bundlerServer.start()
// Instances accessible at:
// "http://localhost:3000/1" (→ http://localhost:8545/1)
// "http://localhost:3000/2" (→ http://localhost:8545/2)
// "http://localhost:3000/3" (→ http://localhost:8545/3)
// "http://localhost:3000/n" (→ http://localhost:8545/n)

Reference

Server.create

Creates a server for a keyed instance proxy or an exclusive lease pool.

Usage

import { Instance, Server } from 'prool'

const executionServer = Server.create({
  instance: Instance.anvil(),
})
await executionServer.start() 
// Instances accessible at:
// "http://localhost:8545/1"
// "http://localhost:8545/2"
// "http://localhost:8545/3"
// "http://localhost:8545/n"
// "http://localhost:8545/n/start"
// "http://localhost:8545/n/stop"
// "http://localhost:8545/n/restart"
// "http://localhost:8545/healthcheck"

Endpoints:

  • /:key: Proxy to instance at key.
  • /:key/start: Start instance at key.
  • /:key/stop: Stop instance at key.
  • /:key/restart: Restart instance at key.
  • /healthcheck: Healthcheck endpoint.

A lease pool exposes POST /acquire and POST /release/:token instead:

import { Instance, Pool, Server } from 'prool'

const pool = Pool.create({
  instance: Instance.anvil(),
  limit: 2,
  async reset(instance) {
    // Reset application state before reuse.
  },
})
const server = Server.create({ pool })
await server.start()

API

Name Description Type
instance Instance for the server. Instance | (key: number) => Instance
limit Number of instances that can be instantiated in the pool number
pool Exclusive lease pool. LeasePool
host Host for the server. string
port Port for the server. number
returns Server Server.Server

Instance.define

Creates an instance definition, that can be used with Server.create or Pool.define.

Usage

import { Instance } from 'prool'

const foo = Instance.define((parameters: FooParameters) => {
 return {
   name: 'foo',
   host: 'localhost',
   port: 3000,
   async start() {
     // ...
   },
   async stop() {
     // ...
   },
 }
})

API

Name Description Type
fn Instance definition. DefineInstanceFn
returns Instance. Instance

Pool.create

Creates a bounded pool of exclusively leased instances. Acquisitions wait in order when every instance is busy.

If reset fails, the instance is destroyed and the release rejects before the slot becomes available again.

Usage

import { Instance, Pool } from 'prool'

const pool = Pool.create({
  instance: Instance.anvil(),
})
const lease = await pool.acquire()
try {
  await fetch(lease.instance.url)
} finally {
  await lease.release()
}
await pool.close()

API

Name Description Type
instance Instance to lease. Instance
limit Maximum concurrent leases. Defaults to half the available logical CPUs. number
reset Resets an instance before its next lease. (instance: Instance) => Promise<void> | void
returns Exclusive lease pool. LeasePool

Pool.define

Defines a pool of instances. Instances can be started, cached, and stopped against an identifier.

Usage

import { Instance, Pool } from 'prool'

const pool = Pool.define({
 instance: Instance.anvil(),
})
const instance_1 = await pool.start(1)
const instance_2 = await pool.start(2)
const instance_3 = await pool.start(3)

API

Name Description Type
instance Instance for the pool. Instance
limit Number of instances that can be instantiated in the pool number
returns Pool. Pool

Vitest

Server.setup starts one lazy keyed proxy for a Vitest project. Each worker can use Server.get to address, reset, or restart its own instance. Pool.setup eagerly starts one direct instance per worker instead.

Config

import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    globalSetup: './test/setup.global.ts',
    setupFiles: './test/setup.ts',
  },
})

Lazy Server

import { Instance } from 'prool'
import { Server } from 'prool/vitest'
import type { TestProject } from 'vitest/node'

declare module 'vitest' {
  export interface ProvidedContext {
    anvil: Server.Context
  }
}

export default Server.setup({
  instance: Instance.anvil(),
  setup(server, project: TestProject) {
    project.provide('anvil', server)
  },
})
import { Server } from 'prool/vitest'
import { inject } from 'vitest'

const anvil = Server.get(inject('anvil'))
await anvil.reset({ signal: AbortSignal.timeout(30_000) })

export const rpcUrl = anvil.url

reset destroys only that worker's instance, and its next request starts a fresh one. restart retains the pooled instance and endpoint.

Eager Pool

import { Instance } from 'prool'
import { Pool } from 'prool/vitest'
import type { TestProject } from 'vitest/node'

declare module 'vitest' {
  export interface ProvidedContext {
    rpcUrls: readonly string[]
  }
}

export default Pool.setup({
  instance: Instance.anvil(),
  setup(instances, project: TestProject) {
    project.provide(
      'rpcUrls',
      instances.map((instance) => instance.url),
    )
  },
})
import { Pool } from 'prool/vitest'
import { inject } from 'vitest'

export const rpcUrl = Pool.get(inject('rpcUrls'))

Authors

License

MIT License

About

HTTP testing instances for Ethereum

Resources

License

Code of conduct

Stars

140 stars

Watchers

3 watching

Forks

Sponsor this project

  •  

Contributors