From 5075cf11fdf8cb240a273a55f149e6df48d947dd Mon Sep 17 00:00:00 2001 From: Sarah Schwartz <58856580+sarahschwartz@users.noreply.github.com> Date: Mon, 16 Sep 2024 11:05:57 -0600 Subject: [PATCH] docs: add test for multisig --- .github/workflows/playwright.yaml | 1 + .../tutorials/native-aa-multisig/10.index.md | 118 +++++++++++-- tests/configs/config.ts | 4 + tests/configs/native-aa-multisig.ts | 165 ++++++++++++++++++ tests/native-aa-multisig.spec.ts | 6 + tests/utils/files.ts | 5 +- tests/utils/setup.ts | 4 +- 7 files changed, 287 insertions(+), 16 deletions(-) create mode 100644 tests/configs/native-aa-multisig.ts create mode 100644 tests/native-aa-multisig.spec.ts diff --git a/.github/workflows/playwright.yaml b/.github/workflows/playwright.yaml index b6fcc09e..bbc3c542 100644 --- a/.github/workflows/playwright.yaml +++ b/.github/workflows/playwright.yaml @@ -13,6 +13,7 @@ jobs: - "tests/erc20-paymaster.spec.ts" - "tests/how-to-test-contracts.spec.ts" - "tests/daily-spend-limit.spec.ts" + - "tests/native-aa-multisig.spec.ts" steps: - uses: actions/checkout@v4 diff --git a/content/tutorials/native-aa-multisig/10.index.md b/content/tutorials/native-aa-multisig/10.index.md index 62a5988c..75afe2b8 100644 --- a/content/tutorials/native-aa-multisig/10.index.md +++ b/content/tutorials/native-aa-multisig/10.index.md @@ -33,6 +33,8 @@ Atlas is a smart contract IDE that lets you write, deploy, and interact with con 1. Initiate a new project by running the command: + :test-action{actionId="initialize-project"} + ```sh npx zksync-cli create custom-aa-tutorial --template hardhat_solidity ``` @@ -41,6 +43,8 @@ Atlas is a smart contract IDE that lets you write, deploy, and interact with con 1. Navigate into the project directory: + :test-action{actionId="move-into-project"} + ```sh cd custom-aa-tutorial ``` @@ -48,6 +52,8 @@ Atlas is a smart contract IDE that lets you write, deploy, and interact with con 1. For the purposes of this tutorial, we don't need the example contracts related files. So, proceed by removing all the files inside the `/contracts` and `/deploy` folders manually or by running the following commands: + :test-action{actionId="delete-templates"} + ```sh rm -rf ./contracts/* rm -rf ./deploy/* @@ -55,17 +61,30 @@ Atlas is a smart contract IDE that lets you write, deploy, and interact with con 1. Add the ZKsync and OpenZeppelin contract libraries: - ```sh - yarn add -D @matterlabs/zksync-contracts @openzeppelin/contracts@4.9.5 + :test-action{actionId="install-deps"} + + ::code-group + + ```shell [npm] + npm install -D @matterlabs/zksync-contracts @openzeppelin/contracts@4.9.5 @matterlabs/hardhat-zksync-deploy@1.3.0 + ``` + + ```shell [yarn] + yarn add -D @matterlabs/zksync-contracts @openzeppelin/contracts@4.9.5 @matterlabs/hardhat-zksync-deploy@1.3.0 ``` -1. Include the `isSystem: true` setting in the `zksolc` section of the `hardhat.config.ts` configuration file to allow interaction with system contracts: + :: + +1. Include the `enableEraVMExtensions: true` setting in the `zksolc` section of the `hardhat.config.ts` configuration file +to allow interaction with system contracts: ::callout{icon="i-heroicons-light-bulb"} This project does not use the latest version available of `@openzeppelin/contracts`. Make sure you install the specific version mentioned above. :: + :test-action{actionId="hardhat-config"} + ```ts import { HardhatUserConfig } from "hardhat/config"; import "@matterlabs/hardhat-zksync-deploy"; @@ -77,7 +96,7 @@ Atlas is a smart contract IDE that lets you write, deploy, and interact with con zksolc: { version: "latest", // Uses latest available in https://github.com/matter-labs/zksolc-bin/ settings: { - isSystem: true, // ⚠️ Make sure to include this line + enableEraVMExtensions: true, // ⚠️ Make sure to include this line }, }, defaultNetwork: "zkSyncTestnet", @@ -97,6 +116,10 @@ Atlas is a smart contract IDE that lets you write, deploy, and interact with con export default config; ``` + :test-action{actionId="add-local-node"} + :test-action{actionId="use-local-node"} + :test-action{actionId="start-local-node"} + ## Account Abstraction Each account must implement the [IAccount](https://docs.zksync.io/build/developer-reference/account-abstraction/design#iaccount-interface) interface. @@ -538,6 +561,14 @@ Therefore, it is highly recommended to put `require(success)` for the transactio 1. Create a file `TwoUserMultisig.sol` in the `contracts` folder and copy/paste the code below into it. +:test-action{actionId="make-multisig-contract"} + +```shell +touch contracts/TwoUserMultisig.sol +``` + +:test-action{actionId="multisig-contract-code"} + ```solidity [TwoUserMultisig.sol] // SPDX-License-Identifier: MIT pragma solidity ^0.8.17; @@ -797,6 +828,12 @@ contract TwoUserMultisig is IAccount, IERC1271 { 1. Create a new Solidity file in the `contracts` folder called `AAFactory.sol`. + :test-action{actionId="make-factory-contract"} + + ```shell + touch contracts/AAFactory.sol + ``` + The contract is a factory that deploys the accounts. ::callout{icon="i-heroicons-exclamation-triangle"} @@ -809,6 +846,8 @@ contract TwoUserMultisig is IAccount, IERC1271 { 1. Copy/paste the following code into the file. + :test-action{actionId="factory-contract-code"} + ```solidity [AAFactory.sol] // SPDX-License-Identifier: MIT pragma solidity ^0.8.17; @@ -863,6 +902,14 @@ Make sure you deposit funds on ZKsync Era using [one of the available bridges](h 1. In the `deploy` folder, create the file `deploy-factory.ts` and copy/paste the following code, replacing `` with your private key. + :test-action{actionId="make-deploy-script"} + + ```shell + touch deploy/deploy-factory.ts + ``` + + :test-action{actionId="deploy-script-code"} + ```ts [deploy-factory.ts] import { utils, Wallet } from "zksync-ethers"; import * as ethers from "ethers"; @@ -891,13 +938,26 @@ Make sure you deposit funds on ZKsync Era using [one of the available bridges](h } ``` + :test-action{actionId="deploy-script-pk"} + 1. From the project root, compile and deploy the contracts. - ```sh + :test-action{actionId="compile-and-deploy-factory"} + + ::code-group + + ```shell [npx] + npx hardhat compile + npx hardhat deploy-zksync --script deploy-factory.ts + ``` + + ```sh [yarn] yarn hardhat compile yarn hardhat deploy-zksync --script deploy-factory.ts ``` + :: + The output should look like this: ```txt @@ -919,15 +979,23 @@ This section assumes you have an EOA account with sufficient funds on ZKsync Era In the `deploy` folder, create a file called `deploy-multisig.ts`. +:test-action{actionId="create-deploy-multisig"} + +```shell +touch deploy/deploy-multisig.ts +``` + The call to the `deployAccount` function deploys the AA. +:test-action{actionId="deploy-multisig-code"} + ```ts [deploy-multisig.ts] import { utils, Wallet, Provider, EIP712Signer, types } from "zksync-ethers"; import * as ethers from "ethers"; import { HardhatRuntimeEnvironment } from "hardhat/types"; // Put the address of your AA factory -const AA_FACTORY_ADDRESS = ""; +const AA_FACTORY_ADDRESS = ""; //sepolia export default async function (hre: HardhatRuntimeEnvironment) { const provider = new Provider("https://sepolia.era.zksync.dev"); @@ -952,6 +1020,7 @@ export default async function (hre: HardhatRuntimeEnvironment) { // Getting the address of the deployed contract account // Always use the JS utility methods const abiCoder = new ethers.AbiCoder(); + const multisigAddress = utils.create2Address( AA_FACTORY_ADDRESS, await aaFactory.aaBytecodeHash(), @@ -974,9 +1043,10 @@ Read the documentation for more information on [address derivation differences b Before the deployed account can submit transactions, we need to deposit some ETH to it for the transaction fees. +:test-action{actionId="deposit-funds"} + ```ts console.log("Sending funds to multisig account"); - // Send funds to the multisig account we just deployed await( await wallet.sendTransaction({ @@ -994,6 +1064,8 @@ console.log(`Multisig account balance is ${multisigBalance.toString()}`); Now we can try to deploy a new multisig; the initiator of the transaction will be our deployed account from the previous part. +:test-action{actionId="create-deploy-tx"} + ```ts // Transaction to deploy a new account using the multisig we just deployed let aaTx = await aaFactory.deployAccount.populateTransaction( @@ -1006,6 +1078,8 @@ let aaTx = await aaFactory.deployAccount.populateTransaction( Then, we need to fill all the transaction fields: +:test-action{actionId="modify-deploy-tx"} + ```ts const gasLimit = await provider.estimateGas({ ...aaTx, from: wallet.address }); const gasPrice = await provider.getGasPrice(); @@ -1029,13 +1103,15 @@ aaTx = { ::callout{icon="i-heroicons-light-bulb"} Currently, we expect the `l2gasLimit` to cover both the verification and the execution steps. The gas returned by `estimateGas` is `execution_gas + 20000`, where `20000` is roughly equal to the overhead -needed for the defaultAA to have both fee charged and the signature verified. +needed for the default AA to have both the fee charged and the signature verified.
In the case that your AA has an expensive verification step, you should add some constant to the `l2gasLimit`. :: Then, we need to sign the transaction and provide the `aaParamas` in the customData of the transaction. +:test-action{actionId="sign-deploy-tx"} + ```ts const signedTxHash = EIP712Signer.getSignedDigest(aaTx); @@ -1050,6 +1126,8 @@ aaTx.customData = { Finally, we are ready to send the transaction: +:test-action{actionId="send-deploy-tx"} + ```ts console.log(`The multisig's nonce before the first tx is ${await provider.getTransactionCount(multisigAddress)}`); @@ -1070,6 +1148,8 @@ console.log(`Multisig account balance is now ${multisigBalance.toString()}`); 1. Copy/paste the following code into the deployment file, replacing the `` and private key `` placeholders with the relevant data. + :test-action{actionId="final-deploy-script"} + ```ts import { utils, Wallet, Provider, EIP712Signer, types } from "zksync-ethers"; import * as ethers from "ethers"; @@ -1112,7 +1192,7 @@ and private key `` placeholders with the relevant data. console.log("Sending funds to multisig account"); // Send funds to the multisig account we just deployed - await ( + await( await wallet.sendTransaction({ to: multisigAddress, // You can increase the amount of ETH sent to the multisig @@ -1165,7 +1245,6 @@ and private key `` placeholders with the relevant data. const sentTx = await provider.broadcastTransaction(types.Transaction.from(aaTx).serialized); console.log(`Transaction sent from multisig with hash ${sentTx.hash}`); - await sentTx.wait(); // Checking that the nonce for the account has increased @@ -1177,12 +1256,29 @@ and private key `` placeholders with the relevant data. } ``` + :test-action{actionId="get-deployed-account-address"} + :test-action{actionId="deploy-multisig-account"} + :test-action{actionId="deploy-multisig-provider"} + :test-action{actionId="deploy-multisig-pk"} + :test-action{actionId="import-dotenv"} + 1. Run the script from the `deploy` folder. - ```sh + :test-action{actionId="run-deploy-multisig"} + + ::code-group + + ```shell [npx] + npx hardhat deploy-zksync --script deploy-multisig.ts + ``` + + ```sh [yarn] yarn hardhat deploy-zksync --script deploy-multisig.ts + ``` + :: + The output should look something like this: ```txt diff --git a/tests/configs/config.ts b/tests/configs/config.ts index 925f8f05..dd177b5a 100644 --- a/tests/configs/config.ts +++ b/tests/configs/config.ts @@ -1,6 +1,7 @@ import { steps as erc20PaymasterSteps } from './erc20-paymaster'; import { steps as howToTestContractsSteps } from './how-to-test-contracts'; import { steps as dailySpendLimitSteps } from './daily-spend-limit'; +import { steps as multisigSteps } from './native-aa-multisig'; export function getConfig(tutorialName: string) { let steps; @@ -14,6 +15,9 @@ export function getConfig(tutorialName: string) { case 'daily-spend-limit': steps = dailySpendLimitSteps; break; + case 'native-aa-multisig': + steps = multisigSteps; + break; default: break; } diff --git a/tests/configs/native-aa-multisig.ts b/tests/configs/native-aa-multisig.ts new file mode 100644 index 00000000..37928ba9 --- /dev/null +++ b/tests/configs/native-aa-multisig.ts @@ -0,0 +1,165 @@ +import type { IStepConfig } from '../utils/types'; + +export const steps: IStepConfig = { + 'initialize-project': { + action: 'runCommand', + prompts: 'Private key of the wallet: |❯ npm: ', + }, + 'wait-for-init': { + action: 'wait', + timeout: 15000, + }, + 'move-into-project': { + action: 'runCommand', + }, + 'delete-templates': { + action: 'runCommand', + commandFolder: 'tests-output/custom-aa-tutorial', + }, + 'install-deps': { + action: 'runCommand', + commandFolder: 'tests-output/custom-aa-tutorial', + }, + 'hardhat-config': { + action: 'writeToFile', + filepath: 'tests-output/custom-aa-tutorial/hardhat.config.ts', + }, + 'add-local-node': { + action: 'modifyFile', + filepath: 'tests-output/custom-aa-tutorial/hardhat.config.ts', + useSetData: "inMemoryNode: { url: 'http://127.0.0.1:8011', ethNetwork: 'localhost', zksync: true,},", + atLine: 22, + }, + 'use-local-node': { + action: 'modifyFile', + filepath: 'tests-output/custom-aa-tutorial/hardhat.config.ts', + useSetData: " defaultNetwork: 'inMemoryNode',", + atLine: 14, + removeLines: [14], + }, + 'start-local-node': { + action: 'runCommand', + commandFolder: 'tests-output/custom-aa-tutorial', + useSetCommand: "bun pm2 start 'era_test_node run' --name era-test-node", + }, + 'make-multisig-contract': { + action: 'runCommand', + commandFolder: 'tests-output/custom-aa-tutorial', + }, + 'multisig-contract-code': { + action: 'writeToFile', + filepath: 'tests-output/custom-aa-tutorial/contracts/TwoUserMultisig.sol', + }, + 'make-factory-contract': { + action: 'runCommand', + commandFolder: 'tests-output/custom-aa-tutorial', + }, + 'factory-contract-code': { + action: 'writeToFile', + filepath: 'tests-output/custom-aa-tutorial/contracts/AAFactory.sol', + }, + 'make-deploy-script': { + action: 'runCommand', + commandFolder: 'tests-output/custom-aa-tutorial', + }, + 'deploy-script-code': { + action: 'writeToFile', + filepath: 'tests-output/custom-aa-tutorial/deploy/deploy-factory.ts', + }, + 'deploy-script-pk': { + action: 'modifyFile', + filepath: 'tests-output/custom-aa-tutorial/deploy/deploy-factory.ts', + atLine: 8, + removeLines: [8], + useSetData: 'const wallet = new Wallet("0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110");', + }, + 'compile-and-deploy-factory': { + action: 'runCommand', + commandFolder: 'tests-output/custom-aa-tutorial', + checkForOutput: 'AA factory address:', + saveOutput: 'tests-output/custom-aa-tutorial/deployed-factory-address.txt', + }, + 'create-deploy-multisig': { + action: 'runCommand', + commandFolder: 'tests-output/custom-aa-tutorial', + }, + 'deploy-multisig-code': { + action: 'writeToFile', + filepath: 'tests-output/custom-aa-tutorial/deploy/deploy-multisig.ts', + }, + 'deposit-funds': { + action: 'modifyFile', + filepath: 'tests-output/custom-aa-tutorial/deploy/deploy-multisig.ts', + atLine: 39, + addSpacesBefore: 1, + }, + 'create-deploy-tx': { + action: 'modifyFile', + filepath: 'tests-output/custom-aa-tutorial/deploy/deploy-multisig.ts', + atLine: 54, + addSpacesBefore: 1, + }, + 'modify-deploy-tx': { + action: 'modifyFile', + filepath: 'tests-output/custom-aa-tutorial/deploy/deploy-multisig.ts', + atLine: 62, + addSpacesBefore: 1, + }, + 'sign-deploy-tx': { + action: 'modifyFile', + filepath: 'tests-output/custom-aa-tutorial/deploy/deploy-multisig.ts', + atLine: 80, + addSpacesBefore: 1, + }, + 'send-deploy-tx': { + action: 'modifyFile', + filepath: 'tests-output/custom-aa-tutorial/deploy/deploy-multisig.ts', + atLine: 90, + addSpacesBefore: 1, + }, + 'final-deploy-script': { + action: 'compareToFile', + filepath: 'tests-output/custom-aa-tutorial/deploy/deploy-multisig.ts', + }, + 'get-deployed-account-address': { + action: 'extractDataToEnv', + dataFilepath: 'tests-output/custom-aa-tutorial/deployed-factory-address.txt', + regex: /0x[a-fA-F0-9]{40}/, + variableName: 'AA_FACTORY_ADDRESS', + envFilepath: 'tests-output/custom-aa-tutorial/.env', + }, + 'deploy-multisig-account': { + action: 'modifyFile', + filepath: 'tests-output/custom-aa-tutorial/deploy/deploy-multisig.ts', + atLine: 6, + removeLines: [6], + useSetData: 'const AA_FACTORY_ADDRESS = process.env.AA_FACTORY_ADDRESS || "";', + }, + 'deploy-multisig-provider': { + action: 'modifyFile', + filepath: 'tests-output/custom-aa-tutorial/deploy/deploy-multisig.ts', + atLine: 9, + removeLines: [9], + useSetData: 'const provider = new Provider("http://localhost:8011");', + }, + 'deploy-multisig-pk': { + action: 'modifyFile', + filepath: 'tests-output/custom-aa-tutorial/deploy/deploy-multisig.ts', + atLine: 11, + removeLines: [11], + useSetData: + 'const wallet = new Wallet("0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110").connect(provider);', + }, + 'import-dotenv': { + action: 'modifyFile', + filepath: 'tests-output/custom-aa-tutorial/deploy/deploy-multisig.ts', + useSetData: `import dotenv from "dotenv"; + dotenv.config();`, + atLine: 4, + }, + 'run-deploy-multisig': { + action: 'runCommand', + commandFolder: 'tests-output/custom-aa-tutorial', + checkForOutput: 'Multisig account balance is now', + }, +}; diff --git a/tests/native-aa-multisig.spec.ts b/tests/native-aa-multisig.spec.ts new file mode 100644 index 00000000..84b67036 --- /dev/null +++ b/tests/native-aa-multisig.spec.ts @@ -0,0 +1,6 @@ +import { test } from '@playwright/test'; +import { setupAndRunTest } from './utils/runTest'; + +test('Native AA Multisig', async ({ page, context }) => { + await setupAndRunTest(page, context, 'custom-aa-tutorial', ['/native-aa-multisig'], 'native-aa-multisig'); +}); diff --git a/tests/utils/files.ts b/tests/utils/files.ts index 10e394c9..e0233588 100644 --- a/tests/utils/files.ts +++ b/tests/utils/files.ts @@ -47,10 +47,9 @@ export async function modifyFile( }); } if (atLine) { - lines.splice(atLine - 1, 0, contentText); + lines.splice(atLine - 1, 0, spacesBefore + contentText + spacesAfter); } - let finalContent = lines.filter((line: string) => line !== '~~~REMOVE~~~').join('\n'); - finalContent = spacesBefore + finalContent + spacesAfter; + const finalContent = lines.filter((line: string) => line !== '~~~REMOVE~~~').join('\n'); writeFileSync(filePath, finalContent, 'utf8'); } } diff --git a/tests/utils/setup.ts b/tests/utils/setup.ts index 3fc5d3ae..b92ed693 100644 --- a/tests/utils/setup.ts +++ b/tests/utils/setup.ts @@ -4,8 +4,8 @@ import type { Page } from '@playwright/test'; export async function startLocalServer(page: Page) { console.log('STARTING...'); - await page.waitForTimeout(15000); - console.log('WAITED 15 SECONDS FOR LOCAL SERVER TO START'); + await page.waitForTimeout(20000); + console.log('WAITED 20 SECONDS FOR LOCAL SERVER TO START'); } export function stopServers() {