Local Testing
This guide introduces a ready-to-use mock test environment, that contains:
- A local Alto bundler
- A mock verifying paymaster
- An Anvil node
- ERC-4337 related contracts, including the EntryPoint and account factories for all major smart account implementations.
The test environment is orchestrated using docker compose. Where the docker containers are pulled from this repo.
The mock environment is designed to mimic mainnet as closely as possible by building with the latest Alto version and by deploying all contracts (entrypoints, paymasters, smart account factories, etc.)
This has advantages over testing against a production testnet as you have more control over the testing environment and can make use of features like anvil cheatcodes.
Steps
Setup
To get started, create a alto-config.json file at the root of your test directory with the following contents.
{
"network-name": "local",
"log-environment": "production",
"entrypoints": "0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789,0x0000000071727De22E5E9d8BAf0edAc6f37da032",
"balance-override-enabled": "true",
"api-version": "v1,v2",
"rpc-url": "http://anvil:8545",
"min-balance": "0",
"utility-private-key": "0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97",
"executor-private-keys": "0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6,0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356,0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e,0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba,0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a",
"max-block-range": 10000,
"safe-mode": false,
"port": 4337,
"log-level": "info",
"public-client-log-level": "error",
"wallet-client-log-level": "error",
"polling-interval": 100
}
Note: All private keys are test keys generated by anvil
Then, create a docker-compose.yaml file at the root of your test directory with the following contents.
services:
anvil:
image: ghcr.io/foundry-rs/foundry:nightly-c4a984fbf2c48b793c8cd53af84f56009dd1070c
ports: [ "8545:8545" ]
entrypoint: [ "anvil", "--host", "0.0.0.0", "--block-time", "0.1", "--silent"]
platform: linux/amd64/v8
healthcheck:
test: ["CMD-SHELL", "cast rpc web3_clientVersion | grep -c anvil > /dev/null "]
start_interval: 250ms
start_period: 10s
interval: 30s
timeout: 5s
retries: 50
contract-deployer:
image: ghcr.io/pimlicolabs/mock-contract-deployer:main
environment:
- ANVIL_RPC=http://anvil:8545
depends_on:
anvil:
condition: service_healthy
mock-paymaster:
image: ghcr.io/pimlicolabs/mock-verifying-paymaster:main
ports: [ "3000:3000" ]
environment:
- ALTO_RPC=http://alto:4337
- ANVIL_RPC=http://anvil:8545
depends_on:
anvil:
condition: service_healthy
contract-deployer:
condition: service_completed_successfully
alto:
image: ghcr.io/pimlicolabs/alto:latest
ports: [ "4337:4337" ]
entrypoint: ["node", "src/lib/cli/alto.js", "run", "--config", "/app/alto-config.json"]
depends_on:
anvil:
condition: service_healthy
contract-deployer:
condition: service_completed_successfully
volumes:
- ./alto-config.json:/app/alto-config.json
To start the test environment, run
docker compose up
Once docker has started, the following services can be accessed locally through the following endpoints:
Anvil
at localhost:8545Alto Bundler
at localhost:4337Mock Paymaster
at localhost:3000
You can now use permissionless like you normally would but instead of referencing the live endpoints, use the local endpoints mentioned above when creating the clients.
import { createPimlicoClient } from "permissionless/clients/pimlico";
import { http, createPublicClient } from "viem";
import { createBundlerClient, entryPoint07Address } from "viem/account-abstraction"
import { foundry } from "viem/chains";
const publicClient = createPublicClient({
chain: foundry,
transport: http("http://localhost:8545"),
});
const bundlerClient = createBundlerClient({
chain: foundry,
transport: http("http://localhost:4337")
});
const pimlicoClient = createPimlicoClient({
transport: http("http://localhost:3000"),
entryPoint: {
address: entryPoint07Address,
version: "0.7",
}
})
Vitest Integration
You can add scripts in your package.json to automatically set up and tear down your mock environment when running tests.
{
"name": "aa-tests",
"scripts": {
"test": "bun run docker:up && vitest run && bun run docker:down",
"docker:up": "docker-compose up -d",
"docker:down": "docker-compose down"
},
"dependencies": {
"viem": "^2.9.17",
"permissionless": "^0.1.35"
},
"devDependencies": {
"vitest": "^1.5.2"
}
}
When writing test cases, ensure that the bundler and paymaster are fully setup before sending any request to them. To do this, make a simple health check in the beforeAll
declaration.
import { beforeAll, describe, expect, test } from "vitest";
import { ensureBundlerIsReady, ensurePaymasterIsReady } from "./healthCheck";
import { foundry } from "viem/chains";
import { http } from "viem";
import {
createBundlerClient,
entryPoint06Address,
entryPoint07Address
} from "viem/account-abstraction";
describe("Test basic bundler functions", () => {
beforeAll(async () => {
await ensureBundlerIsReady();
await ensurePaymasterIsReady();
});
test("Can get chainId", async () => {
const bundlerClient = createBundlerClient({
chain: foundry,
transport: http("http://localhost:4337"),
});
const chainId = await bundlerClient.getChainId();
expect(chainId).toEqual(foundry.id);
});
test("Can get supported entryPoints", async () => {
const bundlerClient = createBundlerClient({
chain: foundry,
transport: http("http://localhost:4337"),
});
const supportedEntryPoints = await bundlerClient.getSupportedEntryPoints();
expect(supportedEntryPoints).toEqual([
entryPoint06Address,
entryPoint07Address,
]);
});
});
Extension: Testing against forked state
If you want your tests to run against a live blockchain, you can slightly edit the docker-compose.yaml file to fork from the latest block by adding the anvil flag --fork-url and the environment variable SKIP_DEPLOYMENTS to skip the local contract deployments.
services:
anvil:
image: ghcr.io/foundry-rs/foundry:nightly-c4a984fbf2c48b793c8cd53af84f56009dd1070c
ports: [ "8545:8545" ]
entrypoint: [ "anvil", "--fork-url", "https://rpc.ankr.com/eth_sepolia", "--chain-id", "11155111", "--host", "0.0.0.0", "--block-time", "0.1", "--silent"]
platform: linux/amd64/v8
healthcheck:
test: ["CMD-SHELL", "cast rpc web3_clientVersion | grep -c anvil > /dev/null "]
start_interval: 250ms
start_period: 10s
interval: 30s
timeout: 5s
retries: 50
contract-deployer:
image: ghcr.io/pimlicolabs/mock-contract-deployer:main
environment:
- ANVIL_RPC=http://anvil:8545
- SKIP_DEPLOYMENTS=true
depends_on:
anvil:
condition: service_healthy
mock-paymaster:
image: ghcr.io/pimlicolabs/mock-verifying-paymaster:main
ports: [ "3000:3000" ]
environment:
- ALTO_RPC=http://alto:4337
- ANVIL_RPC=http://anvil:8545
depends_on:
anvil:
condition: service_healthy
contract-deployer:
condition: service_completed_successfully
alto:
image: ghcr.io/pimlicolabs/alto:latest
ports: [ "4337:4337" ]
entrypoint: ["node", "src/lib/cli/alto.js", "run", "--config", "/app/alto-config.json"]
depends_on:
anvil:
condition: service_healthy
contract-deployer:
condition: service_completed_successfully
volumes:
- ./alto-config.json:/app/alto-config.json