Skip to content

Commit

Permalink
test(smart-contracts): migrate one test suite to use prool instance (#…
Browse files Browse the repository at this point in the history
…858)

* test: add prool instances setup

* chore: update test script to use vitest

* test(smart-contracts): migrate one test suite to use prool instance
  • Loading branch information
moldy530 committed Aug 28, 2024
1 parent def1b01 commit 0fe811b
Show file tree
Hide file tree
Showing 15 changed files with 495 additions and 43 deletions.
9 changes: 6 additions & 3 deletions .github/workflows/on-pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 18
node-version: 22
cache: "yarn"

- name: Install dependencies
Expand Down Expand Up @@ -41,7 +41,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 18
node-version: 22
cache: "yarn"

- name: Install dependencies
Expand Down Expand Up @@ -72,11 +72,14 @@ jobs:
with:
fetch-depth: "0"
submodules: "recursive"

- name: Set up foundry
uses: foundry-rs/foundry-toolchain@v1

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 18
node-version: 22
cache: "yarn"

- name: Install dependencies
Expand Down
7 changes: 5 additions & 2 deletions .github/workflows/publish-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,13 @@ jobs:
fetch-depth: "0"
submodules: "recursive"

- name: Set Node.js 18.x
- name: Set up foundry
uses: foundry-rs/foundry-toolchain@v1

- name: Set Node.js 22.x
uses: actions/setup-node@v3
with:
node-version: 18
node-version: 22
cache: "yarn"

- name: Set Github User Details
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,5 @@ site/**/*.js
**/userop.json

vocs.config.tsx.timestamp-*.mjs

bin
22 changes: 22 additions & 0 deletions .vitest/globalSetup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { rundlerBinaryPath } from "./src/constants";
import * as instances from "./src/instances";
import {
cleanupRundler,
downloadLatestRundlerRelease,
isRundlerInstalled,
} from "./src/rundler";

export default async function () {
if (!(await isRundlerInstalled(rundlerBinaryPath))) {
await downloadLatestRundlerRelease(rundlerBinaryPath);
}

const shutdown = await Promise.all(
Object.values(instances).map((instance) => instance.start())
);

return async () => {
await Promise.all(shutdown.map((stop) => stop()));
await cleanupRundler(rundlerBinaryPath);
};
}
10 changes: 10 additions & 0 deletions .vitest/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "local-vitest",
"version": "0.0.0",
"private": true,
"type": "module",
"devDependencies": {
"prool": "^0.0.15",
"typescript-template": "*"
}
}
14 changes: 13 additions & 1 deletion .vitest/setupTests.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import dotenv from "dotenv";
import fetch from "node-fetch";
import { setIntervalMining } from "viem/actions";
import { afterAll, beforeAll } from "vitest";
import * as instances from "./src/instances.js";

const client = instances.anvilArbSepolia.getClient();

dotenv.config();

// @ts-expect-error wut
// @ts-expect-error this does exist but ts is not liking it
global.fetch = fetch;

beforeAll(async () => {
await setIntervalMining(client, { interval: 0 });
}, 60_000);

afterAll(async () => {});
5 changes: 5 additions & 0 deletions .vitest/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { join } from "path";

export const rundlerBinaryPath = join(__dirname, "../bin/rundler");

export const poolId = Number(process.env.VITEST_POOL_ID ?? 1);
131 changes: 131 additions & 0 deletions .vitest/src/instances.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import getPort from "get-port";
import { createServer } from "prool";
import { anvil, rundler } from "prool/instances";
import { createClient, http, type Chain, type ClientConfig } from "viem";
import { arbitrumSepolia } from "viem/chains";
import { split } from "../../aa-sdk/core/src/transport/split";
import { poolId, rundlerBinaryPath } from "./constants";

export const anvilArbSepolia = defineInstance({
chain: arbitrumSepolia,
forkUrl: "https://arbitrum-sepolia-rpc.publicnode.com",
anvilPort: 8545,
bundlerPort: 8645,
});

type DefineInstanceParams = {
chain: Chain;
forkUrl: string;
anvilPort: number;
bundlerPort: number;
};

const bundlerMethods = [
"eth_sendUserOperation",
"eth_estimateUserOperationGas",
"eth_getUserOperationReceipt",
"eth_getUserOperationByHash",
"eth_supportedEntryPoints",
];

function defineInstance(params: DefineInstanceParams) {
const { anvilPort, bundlerPort, forkUrl, chain: chain_ } = params;
const rpcUrls = {
bundler: `http://127.0.0.1:${bundlerPort}`,
anvil: `http://127.0.0.1:${anvilPort}`,
};

const chain = {
...chain_,
name: `${chain_.name} (Local)`,
rpcUrls: {
default: {
http: [rpcUrls.anvil],
},
},
} as const satisfies Chain;

const clientConfig = {
chain,
transport(args) {
const {
config,
request: request_,
value,
} = split({
overrides: [
{
methods: bundlerMethods,
transport: http(rpcUrls.bundler + `/${poolId}`),
},
],
fallback: http(rpcUrls.anvil + `/${poolId}`),
})(args);

return {
config,
request(params) {
// Here we can add further custom handling for certain methods, or we can do it above in the split transport config
return request_(params);
},
value,
};
},
} as const satisfies ClientConfig;

const anvilServer = createServer({
instance: anvil({
forkUrl: forkUrl,
noMining: true,
}),
port: anvilPort,
});

const bundlerServer = createServer({
instance: (key) =>
rundler({
binary: rundlerBinaryPath,
nodeHttp: rpcUrls.anvil + `/${key}`,
}),
port: bundlerPort,
});

return {
chain,
clientConfig,
anvilServer,
bundlerServer,
getClient() {
return createClient({
...clientConfig,
chain,
transport: clientConfig.transport,
}).extend(() => ({ mode: "anvil" }));
},
async restart() {
await fetch(`${rpcUrls.anvil}/${poolId}/restart`);
await bundlerServer.stop();
await bundlerServer.start();
},
async start() {
// We do this because it's possible we're running all workspaces at the same time
// and we don't want to start the servers multiple times
// This still gives us isolation because each workspace should have its own pool id
if ((await getPort({ port: anvilPort })) === anvilPort) {
await anvilServer.start();
}
if ((await getPort({ port: bundlerPort })) === bundlerPort) {
await bundlerServer.start();
}

return async () => {
await bundlerServer.stop();
await anvilServer.stop();
};
},
async stop() {
await anvilServer.stop();
await bundlerServer.stop();
},
};
}
93 changes: 93 additions & 0 deletions .vitest/src/rundler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import * as fs from "node:fs";
import * as path from "node:path";
import { pipeline } from "node:stream";
import { promisify } from "node:util";
import * as zlib from "node:zlib";
import * as tar from "tar";

const streamPipeline = promisify(pipeline);

export async function isRundlerInstalled(rundlerPath: string) {
try {
await fs.promises.access(rundlerPath, fs.constants.F_OK);
return true;
} catch (e) {
return false;
}
}

export async function cleanupRundler(rundlerPath: string) {
await fs.promises.rm(rundlerPath, { force: true });
}

export async function downloadLatestRundlerRelease(
filePath: string,
version = "v0.2.2"
) {
const repoUrl =
"https://api.github.com/repos/alchemyplatform/rundler/releases";
const { arch, platform } = process;

try {
// Get the list of releases from GitHub API
const releasesResponse = await fetch(repoUrl);
if (!releasesResponse.ok) {
throw new Error(
`Failed to fetch releases: ${releasesResponse.statusText}`
);
}
const releases: any = await releasesResponse.json();

if (releases.length === 0) {
return;
}

// Get the latest release
const latestRelease = releases.find((x: any) => x.tag_name === version);
if (!latestRelease) {
throw new Error(`Failed to find release with tag ${version}`);
}

const asset = latestRelease.assets
.filter((x: any) => (x.name as string).endsWith(".gz"))
.find((x: any) => {
return (
x.name.includes(platform) &&
(x.name.includes(arch) ||
(arch === "arm64" &&
platform === "darwin" &&
x.name.includes("aarch64")))
);
});

if (!asset) {
return;
}

const assetUrl = asset.browser_download_url;

// Download the asset
const assetResponse = await fetch(assetUrl);
if (!assetResponse.ok) {
throw new Error(`Failed to download asset: ${assetResponse.statusText}`);
}

if (!assetResponse.body) {
throw new Error("Github request returned an empty body");
}

// Save the downloaded file
const extractPath = path.resolve(filePath, "..");
if (!(await isRundlerInstalled(extractPath))) {
await fs.promises.mkdir(extractPath, { recursive: true });
}

const gunzipStream = zlib.createGunzip();
const tarStream = tar.extract({
cwd: extractPath,
});
await streamPipeline(assetResponse.body, gunzipStream, tarStream);
} catch (error) {
throw new Error(`Failed to download the latest release. ${error}`);
}
}
3 changes: 3 additions & 0 deletions .vitest/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "typescript-template/base.json"
}
3 changes: 2 additions & 1 deletion .vitest/vitest.shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import { configDefaults, defineConfig } from "vitest/config";
export const sharedConfig = defineConfig({
test: {
alias: {
"~test": join(__dirname, "."),
"~test": join(__dirname, "./src"),
},
singleThread: true,
globals: true,
setupFiles: [join(__dirname, "setupTests.ts")],
globalSetup: join(__dirname, "globalSetup.ts"),
exclude: [
...configDefaults.exclude,
"**/e2e-tests/**/*.test.ts",
Expand Down
Loading

0 comments on commit 0fe811b

Please sign in to comment.