Skip to content

Commit

Permalink
feat: Add Bundler Environment
Browse files Browse the repository at this point in the history
  • Loading branch information
ankurdubey521 committed Aug 14, 2023
1 parent 305af0d commit 1b9ff20
Show file tree
Hide file tree
Showing 13 changed files with 1,531 additions and 2 deletions.
2 changes: 1 addition & 1 deletion hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ networks: {
allowUnlimitedContractSize: true,
chainId: 31337,
},
ganache: {
local: {
chainId: 1337,
url: "http://localhost:8545",
accounts: {
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"build": "npm run clean && npx hardhat compile && npx hardhat typechain",
"build:foundry": "forge build --via-ir",
"test:foundry": "forge test --via-ir",
"test:bundler": "./scripts/bundler-tests.sh",
"lint": "prettier --write 'contracts/**/*.sol'",
"postinstall": "husky install",
"prepack": "pinst --disable",
Expand Down Expand Up @@ -65,6 +66,7 @@
"@uniswap/sdk-core": "^3.2.2",
"@uniswap/universal-router": "^1.4.1",
"@uniswap/v3-periphery": "^1.4.3",
"axios": "^1.4.0",
"chai-as-promised": "^7.1.1",
"chai-string": "^1.5.0",
"dotenv": "^16.0.3",
Expand Down
46 changes: 46 additions & 0 deletions scripts/bundler-tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/bin/bash

SCRIPT=$(realpath $0)
SCRIPT_PATH=$(dirname $SCRIPT)
ROOT_PATH=$SCRIPT_PATH/..
ENVIONRMENT_PATH=$SCRIPT_PATH/../test/bundler-integration/environment
COMPOSE_FILE_PATH=$ENVIONRMENT_PATH/docker-compose.yml
ENTRYPOINT_DEPLOY_SCRIPT_PATH=$ENVIONRMENT_PATH/deployEntrypoint.ts

docker compose -f $COMPOSE_FILE_PATH down

echo "⚙️ 1. Launching geth...."
docker compose -f $COMPOSE_FILE_PATH up geth-dev -d

echo "⚙️ 2. Deploying Entrypoint..."
npx hardhat run $ENTRYPOINT_DEPLOY_SCRIPT_PATH --network local

source $ENVIONRMENT_PATH/.env

echo "⚙️ 3. Launching Bundler..."
docker compose -f $COMPOSE_FILE_PATH up bundler -d

echo "⚙️ 4. Waiting for Bundler to start..."
URL="http://localhost:3000"
JSON_DATA='{
"jsonrpc": "2.0",
"method": "web3_clientVersion",
"params": []
}'
while true; do
RESPONSE_CODE=$(curl --write-out '%{http_code}' --silent --output /dev/null --header "Content-Type: application/json" --request POST --data "$JSON_DATA" "$URL")

if [ "$RESPONSE_CODE" -eq 200 ]; then
echo "Received 200 OK response!"
break
else
echo "Waiting for 200 OK response, got $RESPONSE_CODE. Retrying in 5 seconds..."
sleep 5
fi
done

echo "⚙️ 5. Running tests with params --network local $@"
npx hardhat test $(find $ROOT_PATH/test/bundler-integration -type f -name "*.ts") --network local "$@"

echo "⚙️ 6. Stopping geth and bundler...."
docker compose -f $COMPOSE_FILE_PATH down
199 changes: 199 additions & 0 deletions test/bundler-integration/environment/bundlerEnvironment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import { providers, BigNumberish, utils, BigNumber } from "ethers";
import axios, { AxiosInstance } from "axios";
import { ethers, config } from "hardhat";
import type { HttpNetworkConfig } from "hardhat/types";
import { UserOperation } from "../../../account-abstraction/test/UserOperation";
import { hexValue } from "ethers/lib/utils";

export type Snapshot = {
blockNumber: number;
};

export class UserOperationSubmissionError extends Error {
constructor(message: string) {
super(message);
this.name = "UserOperationSubmissionError";
}
}
export class BundlerResetError extends Error {
constructor(message: string) {
super(message);
this.name = "BundleResetError";
}
}

export const serializeUserOp = (op: UserOperation) => ({
sender: op.sender,
nonce: hexValue(op.nonce),
initCode: op.initCode,
callData: op.callData,
callGasLimit: hexValue(op.callGasLimit),
verificationGasLimit: hexValue(op.verificationGasLimit),
preVerificationGas: hexValue(op.preVerificationGas),
maxFeePerGas: hexValue(op.maxFeePerGas),
maxPriorityFeePerGas: hexValue(op.maxPriorityFeePerGas),
paymasterAndData: op.paymasterAndData,
signature: op.signature,
});

export class BundlerTestEnvironment {
public static BUNDLER_ENVIRONMENT_CHAIN_ID = 1337;
public static DEFAULT_FUNDING_AMOUNT = utils.parseEther("1000");

DOCKER_COMPOSE_DIR = __dirname;
DOCKER_COMPOSE_BUNDLER_SERVICE = "bundler";

private apiClient: AxiosInstance;
public defaultSnapshot: Snapshot | undefined;
private static instance: BundlerTestEnvironment;

constructor(
public readonly provider: providers.JsonRpcProvider,
public readonly bundlerUrl: string
) {
this.apiClient = axios.create({
baseURL: this.bundlerUrl,
});
}

static getDefaultInstance = async () => {
if (this.instance) {
return this.instance;
}

this.instance = new BundlerTestEnvironment(
new providers.JsonRpcProvider(
(config.networks.local as HttpNetworkConfig).url
),
"http://localhost:3000"
);

const defaultAddresses = (await ethers.getSigners()).map(
(signer) => signer.address
);
await this.instance.fundAccounts(
defaultAddresses,
defaultAddresses.map((_) => this.DEFAULT_FUNDING_AMOUNT)
);

this.instance.defaultSnapshot = await this.instance.snapshot();

return this.instance;
};

fundAccounts = async (
accountsToFund: string[],
fundingAmount: BigNumberish[]
) => {
const signer = this.provider.getSigner();
const nonce = await signer.getTransactionCount();
accountsToFund = (
await Promise.all(
accountsToFund.map(
async (account, i): Promise<[string, boolean]> => [
account,
(await this.provider.getBalance(account)).lt(fundingAmount[i]),
]
)
)
)
.filter(([, needsFunding]) => needsFunding)
.map(([account]) => account);
await Promise.all(
accountsToFund.map((account, i) =>
signer.sendTransaction({
to: account,
value: fundingAmount[i],
nonce: nonce + i,
})
)
);
};

snapshot = async (): Promise<Snapshot> => ({
blockNumber: await this.provider.getBlockNumber(),
});

sendUserOperation = async (
userOperation: UserOperation,
entrypointAddress: string
): Promise<string> => {
const result = await this.apiClient.post("/rpc", {
jsonrpc: "2.0",
method: "eth_sendUserOperation",
params: [serializeUserOp(userOperation), entrypointAddress],
});
if (result.status !== 200) {
throw new Error(
`Failed to send user operation: ${JSON.stringify(
result.data.error.message
)}`
);
}
if (result.data.error) {
throw new UserOperationSubmissionError(JSON.stringify(result.data.error));
}

return result.data;
};

resetBundler = async () => {
const result = await this.apiClient.post("/rpc", {
jsonrpc: "2.0",
method: "debug_bundler_clearState",
params: [],
});
if (result.status !== 200) {
throw new Error(
`Failed to send reset bundler: ${JSON.stringify(
result.data.error.message
)}`
);
}
if (result.data.error) {
throw new BundlerResetError(JSON.stringify(result.data.error));
}

if (result.data.result !== "ok") {
throw new BundlerResetError(
`Failed to reset bundler: ${JSON.stringify(result.data.result)}`
);
}
};

dumpMempool = async () => {
const result = await this.apiClient.post("/rpc", {
jsonrpc: "2.0",
method: "debug_bundler_dumpMempool",
params: [],
});
if (result.status !== 200) {
throw new Error(
`Failed to send reset bundler: ${JSON.stringify(
result.data.error.message
)}`
);
}
if (result.data.error) {
throw new BundlerResetError(JSON.stringify(result.data.error));
}

return result.data.result;
};

revert = async (snapshot: Snapshot) => {
await this.provider.send("debug_setHead", [
utils.hexValue(BigNumber.from(snapshot.blockNumber)),
]);

// getBlockNumber() caches the result, so we directly call the rpc method instead
const currentBlockNumber = BigNumber.from(
await this.provider.send("eth_blockNumber", [])
);
if (!BigNumber.from(snapshot.blockNumber).eq(currentBlockNumber)) {
throw new Error(
`Failed to revert to block ${snapshot.blockNumber}. Current block number is ${currentBlockNumber}`
);
}
};
}
17 changes: 17 additions & 0 deletions test/bundler-integration/environment/deployEntrypoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ethers } from "hardhat";
import { BundlerTestEnvironment } from "./bundlerEnvironment";
import { promises } from "fs";
import path from "path";
import { EntryPoint__factory } from "@account-abstraction/contracts";

const envPath = path.join(__dirname, ".env");

if (require.main === module) {
(async () => {
await BundlerTestEnvironment.getDefaultInstance();
const [deployer] = await ethers.getSigners();
const entrypoint = await new EntryPoint__factory(deployer).deploy();
console.log("Entrypoint deployed at", entrypoint.address);
await promises.writeFile(envPath, `ENTRYPOINT=${entrypoint.address}`);
})();
}
20 changes: 20 additions & 0 deletions test/bundler-integration/environment/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
version: "2"

services:
bundler:
ports: ["3000:3000"]
image: ankurdubeybiconomy/bundler:latest # Image based off accountabstraction/bundler:0.6.1 with fixes for debug_bundler_clearState
command: --network http://geth-dev:8545 --entryPoint ${ENTRYPOINT} --show-stack-traces
volumes:
- ./workdir:/app/workdir:ro

mem_limit: 1000M
logging:
driver: "json-file"
options:
max-size: 10m
max-file: "10"

geth-dev:
build: geth-dev
ports: ["8545:8545"]
13 changes: 13 additions & 0 deletions test/bundler-integration/environment/geth-dev/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FROM ethereum/client-go:release-1.12
ENTRYPOINT geth \
--http.vhosts '*,localhost,host.docker.internal' \
--http \
--http.api personal,eth,net,web3,debug \
--http.corsdomain '*' \
--http.addr "0.0.0.0" \
--nodiscover --maxpeers 0 --mine \
--networkid 1337 \
--dev \
--allow-insecure-unlock \
--rpc.allow-unprotected-txs \
--dev.gaslimit 200000000 \
12 changes: 12 additions & 0 deletions test/bundler-integration/environment/workdir/bundler.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"gasFactor": "1",
"port": "3000",
"beneficiary": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"minBalance": "1",
"mnemonic": "./workdir/mnemonic.txt",
"maxBundleGas": 5e6,
"minStake": "1",
"minUnstakeDelay": 0,
"autoBundleInterval": 0,
"autoBundleMempoolSize": 0
}
1 change: 1 addition & 0 deletions test/bundler-integration/environment/workdir/mnemonic.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test test test test test test test test test test test junk
Loading

0 comments on commit 1b9ff20

Please sign in to comment.