# Local Testing With Docker

This guide introduces a ready-to-use mock test environment, that contains:

* A local Alto bundler
* A mock 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](https://github.com/pimlicolabs/mock-aa-environment).

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

::::steps
### Setup

To get started, create a **alto-config.json** file at the root of your test directory with the following contents.

```json [alto-config.json]
{
    "network-name": "local",
    "log-environment": "production",
    "entrypoints": "0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789,0x0000000071727De22E5E9d8BAf0edAc6f37da032,0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108",
    "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.

```yaml [docker-compose.yaml]
services:
  anvil:
    image: ghcr.io/foundry-rs/foundry
    ports: [ "8545:8545" ]
    entrypoint: [ "anvil", "--host", "0.0.0.0", "--block-time", "0.1", "--silent", , "--hardfork", "prague"]
    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

```sh
docker compose up
```

Once docker has started, the following services can be accessed locally through the following endpoints:

* `Anvil` at **localhost:8545**
* `Alto Bundler` at **localhost:4337**
* `Mock 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.

The Mock Paymaster forwards all bundler methods to the bundler, so you can use it as a direct replacement for the bundler URL (just like in production where https://api.pimlico.io/v2/${CHAIN_ID}/rpc?apikey=${API_KEY} is used as both the paymaster and bundler urls).

```ts [clients.ts] twoslash
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"), // [!code ++]
});

const bundlerClient = createBundlerClient({
  chain: foundry,
  transport: http("http://localhost:3000") // [!code ++]
});

const pimlicoClient = createPimlicoClient({
    transport: http("http://localhost:3000"),  // [!code ++]
    entryPoint: {
        address: entryPoint07Address,
        version: "0.7",
    }
})
```

:::info
Note: All smart account types supported by permissionless.js will work out of the box as all related factories and modules are deployed on the local anvil instance.
:::

### Vitest Integration

You can add scripts in your **package.json** to automatically set up and tear down your mock environment when running tests.

```json [package.json]
{
  "name": "aa-tests",
  "scripts": {
    "test": "bun run docker:up && vitest run && bun run docker:down",
    "docker:up": "docker-compose up -d", // [!code ++]
    "docker:down": "docker-compose down" // [!code ++]
  },
  "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.

:::code-group
```ts [basic.test.ts]
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 () => { // [!code ++]
    await ensureBundlerIsReady(); // [!code ++]
    await ensurePaymasterIsReady(); // [!code ++]
  }); // [!code ++]

  test("Can get chainId", async () => {
    const bundlerClient = createBundlerClient({
      chain: foundry,
      transport: http("http://localhost:3000"),
    });

    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:3000"),
    });

    const supportedEntryPoints = await bundlerClient.getSupportedEntryPoints();

    expect(supportedEntryPoints).toEqual([
      entryPoint06Address,
      entryPoint07Address,
    ]);
  });
});
```

```ts [healthCheck.ts] twoslash
import { createBundlerClient } from "viem/account-abstraction";
import { http } from "viem";
import { foundry } from "viem/chains";

export const ensureBundlerIsReady = async () => {
  const bundlerClient = createBundlerClient({
    chain: foundry,
    transport: http("http://localhost:3000"),
  });

  while (true) {
    try {
      await bundlerClient.getChainId();
      return;
    } catch {
      await new Promise((resolve) => setTimeout(resolve, 1000));
    }
  }
};

export const ensurePaymasterIsReady = async () => {
  while (true) {
    try {
      // mock paymaster will open up this endpoint when ready
      const res = await fetch(`http://localhost:3000/ping`);
      const data = await res.json();
      if (data.message !== "pong") {
        throw new Error("paymaster not ready yet");
      }

      return;
    } catch {
      await new Promise((resolve) => setTimeout(resolve, 1000));
    }
  }
};
```
:::

### 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.

```yaml [docker-compose.yaml]
services:
  anvil:
    image: ghcr.io/foundry-rs/foundry:nightly-c4a984fbf2c48b793c8cd53af84f56009dd1070c
    ports: [ "8545:8545" ]
    entrypoint: [ "anvil", "--fork-url", "https://sepolia.rpc.thirdweb.com", "--chain-id", "11155111", "--host", "0.0.0.0", "--block-time", "0.1", "--silent"] // [!code ++]
    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 // [!code ++]
    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
```

:::warning
Do not use compromised or delegated private keys. Publicly known private keys have been delegated via EIP-7702 to sweeper contracts on many live chains. The bundler only supports non-delegated EOAs, so using delegated keys will cause transactions to fail.

To simplify testing, when `SKIP_DEPLOYMENTS=true`, all 7702 delegations for all anvil accounts are removed and their balances are set to 10k ETH.
:::
::::
