Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add foundry integration #845

Open
wants to merge 21 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/e2e_cypress-action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ jobs:
${{ runner.os }}-pnpm-v1-
continue-on-error: true

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1

- name: Install dependencies
run: pnpm install --frozen-lockfile --prefer-offline

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/e2e_debug.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ jobs:
${{ runner.os }}-pnpm-v1-
continue-on-error: true

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1

- name: Install dependencies
run: pnpm install --frozen-lockfile --prefer-offline

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/e2e_headful.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile --prefer-offline

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1

- name: Install linux deps
run: |
sudo apt-get install --no-install-recommends -y \
Expand Down
12 changes: 10 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
# syntax=docker/dockerfile:1
FROM --platform=linux/amd64 synthetixio/docker-e2e:18.16-ubuntu as base

RUN apt update && apt install -y nginx

ENV PATH "$PATH:/root/.foundry/bin"

RUN curl -L https://foundry.paradigm.xyz | bash && \
foundryup && \
forge --version && \
anvil --version && \
cast --version

RUN mkdir /app
WORKDIR /app

RUN apt update && apt install -y nginx

COPY nginx.conf /etc/nginx/sites-available/default

COPY package.json ./
Expand Down
193 changes: 193 additions & 0 deletions commands/foundry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
const { findNetwork } = require('../helpers');
const which = require('which');
duckception marked this conversation as resolved.
Show resolved Hide resolved

const log = require('debug')('synpress:foundry');

let activeChains;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doing module level state like this is common in synpress and not a good practice in general imo. Instead state should be instanciated by user e.g. synpressFoundry(options) where on the implementation you can set state in the factory function or the class in case of doing new SynpressFoundry(options). It's way more robust to later having more than one for example and leads to less problems long term. Easier to read the code and debug too imo


module.exports = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should write new code in a .mjs file imo so converting it to TS later is easier

async resetState() {
log('Resetting state of foundry');
activeChains = undefined;
},
async getActiveChains() {
return activeChains;
},
async forkChains(options) {
await validateIfAnvilIsInstalledOrThrow();
duckception marked this conversation as resolved.
Show resolved Hide resolved

if (typeof options === 'object') {
const chains = await module.exports.runAnvilWithViem(
options.chainsToFork,
);

return { chains };
} else if (typeof options === 'string') {
if (isNaN(options)) {
// todo: add support for:
// (multiple) network IDs
// (single) network name
// (multiple) network names
duckception marked this conversation as resolved.
Show resolved Hide resolved
} else {
// todo: add support for:
// (single) network ID
}

throw new Error('Not implemented');
}
},
async setupViem(anvilChainType) {
try {
const {
createTestClient,
createPublicClient,
createWalletClient,
http,
} = require('viem');
duckception marked this conversation as resolved.
Show resolved Hide resolved

const testClient = createTestClient({
chain: anvilChainType,
mode: 'anvil',
transport: http(),
});

const publicClient = createPublicClient({
chain: anvilChainType,
transport: http(),
});

const walletClient = createWalletClient({
chain: anvilChainType,
transport: http(),
});
Comment on lines +48 to +62
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you did the new AnvilSynpressUtil() pattern here the utility could give these to user so user could use them in their tests too


return { testClient, publicClient, walletClient };
} catch (error) {
throw new Error('There was an error while trying to setup Viem.', error);
}
},
async runAnvilWithViem(chains) {
try {
const { ethers } = require('ethers');
const anvilClient = await import('@viem/anvil');

const pool = anvilClient.createPool();

for (const [index, [chain, options]] of Object.entries(
Object.entries(chains),
)) {
// use fork url if provided, if not then find it in presets
const forkUrl =
options.forkUrl || (await findNetwork(chain)).rpcUrls.public.http[0];

const poolOptions = {
...options,
forkUrl,
};

// remove nativeCurrency because its not supported by anvil
if (poolOptions.nativeCurrency) {
delete poolOptions.nativeCurrency;
}

const anvilInstance = await pool.start(index, poolOptions);

const anvilUrl = `${anvilInstance.host}:${anvilInstance.port}`;
const provider = new ethers.JsonRpcProvider(`http://${anvilUrl}`);
const { chainId, name } = await provider.getNetwork();
chains[chain].anvilClientDetails = {
anvilPool: pool,
anvilPoolId: Number(index),
provider,
anvilInstance,
anvilUrl: `http://${anvilUrl}`,
anvilChainId: Number(chainId),
anvilChainName: name,
anvilChainType: {
id: Number(chainId),
name: name,
network: name,
nativeCurrency: options.nativeCurrency
? options.nativeCurrency
: {
decimals: 18,
name: 'Anvil',
symbol: 'ANV',
},
rpcUrls: {
default: {
http: [`http://${anvilUrl}`],
webSocket: [`ws://${anvilUrl}`],
},
public: {
http: [`http://${anvilUrl}`],
webSocket: [`ws://${anvilUrl}`],
},
},
},
};

chains[chain].viemClients = await module.exports.setupViem(
chains[chain].anvilClientDetails.anvilChainType,
);
}

activeChains = chains;
return chains;
} catch (error) {
throw new Error('There was an error while trying to run anvil.', error);
}
},
async stopAnvil(anvilInstance) {
try {
await anvilInstance.stop();
return true;
} catch (error) {
throw new Error('There was an error while trying to stop anvil.', error);
}
},
async stopAnvilPoolId(anvilPool, anvilPoolId) {
try {
await anvilPool.stop(anvilPoolId);
} catch (error) {
throw new Error(
`There was an error while trying to stop anvil pool with id ${anvilPoolId}`,
error,
);
}
},
async stopAnvilPool(anvilPool) {
try {
if (Object.values(activeChains)[0]) {
await Object.values(
activeChains,
)[0].anvilClientDetails.anvilPool.empty();
} else {
await anvilPool.empty();
}
return true;
} catch (error) {
throw new Error(
`There was an error while trying to stop anvil pool`,
error,
);
}
},
};

class AnvilNotInstalledError extends Error {
constructor(message) {
super(message);
this.name = 'AnvilNotInstalledError';
}
}

async function validateIfAnvilIsInstalledOrThrow() {
try {
await which('anvil');
} catch (e) {
throw new AnvilNotInstalledError(
'Anvil not detected! Forking is possible thanks to Anvil, a local testnet node shipped with Foundry. To install the Foundry toolchain please refer here: https://book.getfoundry.sh/getting-started/installation',
);
}
}
3 changes: 3 additions & 0 deletions commands/synpress.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ const log = require('debug')('synpress:synpress');
const playwright = require('./playwright');
const metamask = require('./metamask');
const helpers = require('../helpers');
const foundry = require('./foundry');

module.exports = {
async resetState() {
log('Resetting state of synpress');
await playwright.resetState();
await metamask.resetState();
await helpers.resetState();
await foundry.resetState();
return true;
},
};
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@
"node-fetch": "^2.6.1",
"underscore": "^1.13.6",
"viem": "^1.6.0",
"wait-on": "^7.0.1"
"wait-on": "^7.0.1",
"which": "^4.0.0"
},
"devDependencies": {
"@metamask/test-dapp": "^7.0.1",
Expand Down
5 changes: 5 additions & 0 deletions plugins/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const helpers = require('../helpers');
const playwright = require('../commands/playwright');
const metamask = require('../commands/metamask');
const etherscan = require('../commands/etherscan');
const foundry = require('../commands/foundry');

/**
* @type {Cypress.PluginConfig}
Expand Down Expand Up @@ -50,6 +51,10 @@ module.exports = (on, config) => {
console.warn('\u001B[33m', 'WARNING:', message, '\u001B[0m');
return true;
},
// foundry commands
forkChains: foundry.forkChains,
stopAnvil: foundry.stopAnvil,
stopAnvilPool: foundry.stopAnvilPool,
// playwright commands
initPlaywright: playwright.init,
clearPlaywright: playwright.clear,
Expand Down
16 changes: 16 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions support/commands.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
import '@testing-library/cypress/add-commands';
import 'cypress-wait-until';

// foundry commands

Cypress.Commands.add('forkChains', options => {
return cy.task('forkChains', options);
});

Cypress.Commands.add('getActiveChains', options => {
return cy.task('getActiveChains', options);
});

Cypress.Commands.add('stopAnvil', anvilInstance => {
return cy.task('stopAnvil', anvilInstance);
});

Cypress.Commands.add('stopAnvilPool', anvilPool => {
return cy.task('stopAnvilPool', anvilPool);
});

// playwright commands

Cypress.Commands.add('initPlaywright', () => {
Expand Down
Loading