Skip to content
Open
Show file tree
Hide file tree
Changes from 13 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
184 changes: 184 additions & 0 deletions commands/foundry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
const { findNetwork } = require('../helpers');
const which = require('which');

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();

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
} else {
// todo: add support for:
// (single) network ID
}
}
},
async setupViem(anvilChainType) {
try {
const {
createTestClient,
createPublicClient,
createWalletClient,
http,
} = require('viem');

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

async function validateIfAnvilIsInstalledOrThrow() {
try {
await which('anvil');
} catch (e) {
throw new Error(
'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.

14 changes: 14 additions & 0 deletions support/commands.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
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('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
42 changes: 39 additions & 3 deletions support/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,41 @@
declare namespace Cypress {
interface Chainable<Subject> {
// foundry commands
/**
* Setup Foundry, run Anvil fork pool and expose Viem clients
* @example
* cy.forkChains({chainsToFork: {optimism: {forkUrl: undefined, forkBlockNumber: undefined, host: '0.0.0.0', nativeCurrency: {decimals: 18, name: 'Optimism Ether', symbol: 'oETH'} } } });
* cy.forkChains({chainsToFork: {optimism: {forkUrl: 'https://rpc.ankr.com/optimism', forkBlockNumber: 123123123, host: '0.0.0.0', nativeCurrency: {decimals: 18, name: 'Optimism Ether', symbol: 'oETH'} } } });
*/
forkChains(options: {
chainsToFork: {
[chain: string]: {
forkUrl?: string;
forkBlockNumber?: number;
host: string;
nativeCurrency: {
decimals: number;
name: string;
symbol: string;
};
};
};
}): Chainable<Subject>;
/**
* Stop Anvil instance
* @example
* cy.stopAnvil(anvilInstance)
*/
stopAnvil(anvilInstance): Chainable<Subject>;
/**
* Stop Anvil pool
* @example
* cy.stopAnvilPool()
* cy.stopAnvilPool(anvilPool)
*/
stopAnvilPool(anvilPool?): Chainable<Subject>;

// playwright commands
/**
* Connect playwright with Cypress instance
* @example
Expand Down Expand Up @@ -60,7 +96,7 @@ declare namespace Cypress {
* If preset for your custom chain is not available, you can add custom network by yourself.
* @example
* cy.addMetamaskNetwork('optimism') // works only if chain is available as preset
* cy.addMetamaskNetwork({name: 'optimism', rpcUrl: 'https://mainnet.optimism.io', chainId: 10, symbol: 'oETH', blockExplorer: 'https://https://optimistic.etherscan.io', isTestnet: false})
* cy.addMetamaskNetwork({name: 'optimism', rpcUrl: 'https://mainnet.optimism.io', chainId: 10, symbol: 'oETH', blockExplorer: 'https://optimistic.etherscan.io', isTestnet: false})
* cy.addMetamaskNetwork({id: 10, name: 'optimism', nativeCurrency: { symbol: 'OP' }, rpcUrls: { default: { http: ['https://mainnet.optimism.io'] } }, testnet: false })
*/
addMetamaskNetwork(
Expand Down Expand Up @@ -427,9 +463,9 @@ declare namespace Cypress {
* @example
* cy.setupMetamask() // will use defaults
* cy.setupMetamask('secret, words, ...', 'optimism', 'password for metamask') // works only if chain is available as preset
* cy.setupMetamask('secret, words, ...', {name: 'optimism', rpcUrl: 'https://mainnet.optimism.io', chainId: 10, symbol: 'oETH', blockExplorer: 'https://https://optimistic.etherscan.io', isTestnet: false}, 'password for metamask')
* cy.setupMetamask('secret, words, ...', {name: 'optimism', rpcUrl: 'https://mainnet.optimism.io', chainId: 10, symbol: 'oETH', blockExplorer: 'https://optimistic.etherscan.io', isTestnet: false}, 'password for metamask')
* cy.setupMetamask('private_key', 'goerli', 'password for metamask')
* cy.setupMetamask('private_key', {name: 'optimism', rpcUrl: 'https://mainnet.optimism.io', chainId: 10, symbol: 'oETH', blockExplorer: 'https://https://optimistic.etherscan.io', isTestnet: false}, 'password for metamask')
* cy.setupMetamask('private_key', {name: 'optimism', rpcUrl: 'https://mainnet.optimism.io', chainId: 10, symbol: 'oETH', blockExplorer: 'https://optimistic.etherscan.io', isTestnet: false}, 'password for metamask')
*/
setupMetamask(
secretWordsOrPrivateKey?: string,
Expand Down
Loading