diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3ca575b..58839f0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -127,3 +127,91 @@ jobs: - name: Run integration tests run: pnpm test + + zksync-os: + runs-on: ubuntu-latest + steps: + - name: Checkout the repository + uses: actions/checkout@v5 + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1.5.0 + with: + # zksync-os-server's zkos-l1-state.json doesn't work on latest version of anvil + version: v1.3.4 + + - name: Install dependencies + run: forge soldeer install + + - name: Build contracts + run: forge build + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.20.0 + + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version: lts/Jod + cache: pnpm + + - name: Install dependencies + run: | + pnpm install -r --frozen-lockfile + cd ${{ github.workspace }}/.. + git clone https://github.com/eth-infinitism/account-abstraction + git clone https://github.com/matter-labs/zksync-os-server + + - name: Cache Rust build + uses: Swatinem/rust-cache@v2 + with: + workspaces: "../zksync-os-server -> target" + + - name: Run server + run: | + cd ${{ github.workspace }}/../zksync-os-server + cargo build --release --bin zksync-os-server + anvil --load-state zkos-l1-state.json --port 8545 &> anvil.log & + cargo run --release --bin zksync-os-server &> server.log & + + - name: Deploy entrypoint + run: | + cd ${{ github.workspace }}/../account-abstraction + git checkout v0.8.0 + sed -i "60a zksyncos: { url: 'http://localhost:3050', accounts: ['0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110'] }," hardhat.config.ts + npm install -g yarn + yarn install + yarn deploy --network zksyncos + + - name: Fund wallet + run: | + # Rich account + PRIVATE_KEY=0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110 + # send 10 ETH + TO=0xa0Ee7A142d267C1f36714E4a8F75612F20a79720 + cast send --private-key ${PRIVATE_KEY} --rpc-url http://localhost:3050 ${TO} --value 10000000000000000000 + + - name: Deploy contracts and a test account + run: pnpm deploy-test:zksync-os + + - name: Run bundler + run: | + pnpm bundler:zksync-os &> bundler.log & + sleep 5 + + - name: Run integration tests + run: pnpm test:zksync-os + + - name: Print anvil logs + if: always() + run: cat ${{ github.workspace }}/../zksync-os-server/anvil.log + + - name: Print server logs + if: always() + run: cat ${{ github.workspace }}/../zksync-os-server/server.log + + - name: Print bundler logs + if: always() + run: cat bundler.log diff --git a/alto-zksync-os.json b/alto-zksync-os.json new file mode 100644 index 0000000..cad0b41 --- /dev/null +++ b/alto-zksync-os.json @@ -0,0 +1,8 @@ +{ + "entrypoints": "0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108", + "executor-private-keys": "0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110", + "utility-private-key": "0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110", + "rpc-url": "http://localhost:3050", + "port": 4337, + "safe-mode": false +} diff --git a/package.json b/package.json index dcf9377..5d4997a 100644 --- a/package.json +++ b/package.json @@ -6,12 +6,15 @@ "scripts": { "bundler": "alto --config ./alto.json", "bundler:with-proxy": "node js/bundler-with-proxy.js", + "bundler:zksync-os": "alto --config ./alto-zksync-os.json", "bundler-proxy": "node js/bundler-proxy.js", "test": "node --test --import tsx --test-concurrency=1 test/integration/*.test.ts", + "test:zksync-os": "CHAIN_ID=6565 PORT=3050 node --test --import tsx --test-concurrency=1 test/integration/*.test.ts", "anvil": "anvil --fork-url https://reth-ethereum.ithaca.xyz/rpc --chain-id 1337", "coverage": "forge coverage --ir-minimum --no-match-coverage 'script|test'", "//": "Uses the last private key of anvil's rich accounts list", - "deploy-test": "forge script script/Deploy.s.sol --rpc-url http://127.0.0.1:8545 --broadcast --private-key 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6 --sig 'deployAll()'" + "deploy-test": "forge script script/Deploy.s.sol --rpc-url http://127.0.0.1:8545 --broadcast --private-key 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6 --sig 'deployAll()'", + "deploy-test:zksync-os": "forge script script/Deploy.s.sol --rpc-url http://127.0.0.1:3050 --broadcast --private-key 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6 --sig 'deployAll()'" }, "keywords": [ "zksync", diff --git a/test/integration/account.ts b/test/integration/account.ts index 3a95adb..965998c 100644 --- a/test/integration/account.ts +++ b/test/integration/account.ts @@ -16,6 +16,7 @@ import { } from "viem/account-abstraction"; import { hashTypedData, wrapTypedDataSignature } from "viem/experimental/erc7739"; +import { chainId } from "./utils"; const callAbi = [{ components: [ @@ -78,7 +79,7 @@ export class SsoAccount { userOperation: { ...userOperation, sender: address }, entryPointAddress: entryPoint08Address, entryPointVersion: '0.8', - chainId: 1337 + chainId: chainId(), }); return await sso.signer(userOpHash); }, @@ -96,7 +97,7 @@ export class SsoAccount { }, async signTypedData(typedData) { const verifierDomain = { - chainId: 1337, + chainId: chainId(), name: "zksync-sso-1271", version: "1.0.0", verifyingContract: address, diff --git a/test/integration/basic.test.ts b/test/integration/basic.test.ts index b4571a7..d37ccc7 100644 --- a/test/integration/basic.test.ts +++ b/test/integration/basic.test.ts @@ -4,15 +4,14 @@ import { parseAbi } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { SsoAccount } from "./account"; -import { contractAddresses, toEOASigner, createClients, randomAddress, deployContract } from "./utils"; +import { contractAddresses, toEOASigner, createClients, randomAddress, deployContract, chainId, rpcPort } from "./utils"; -const anvilPort = 8545; const altoPort = require("../../alto.json").port; const privateKey = "0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6"; test("executes a simple transfer signed using EOA", { timeout: 120_000 }, async () => { const { account } = contractAddresses(); - const { client, bundlerClient } = createClients(anvilPort, altoPort); + const { client, bundlerClient } = createClients(rpcPort(), altoPort); const sso = await SsoAccount.create(client, account, toEOASigner(privateKey)); const target = randomAddress(); @@ -24,7 +23,7 @@ test("executes a simple transfer signed using EOA", { timeout: 120_000 }, async }], }); - const receipt = await bundlerClient.waitForUserOperationReceipt({ hash, timeout: 0 }); + const receipt = await bundlerClient.waitForUserOperationReceipt({ hash, timeout: 2_000 }); assert.equal( receipt.receipt.status, "success", @@ -37,7 +36,7 @@ test("executes a simple transfer signed using EOA", { timeout: 120_000 }, async test("executes a transaction sponsored by a paymaster", { timeout: 120_000 }, async () => { const { account } = contractAddresses(); - const { client, bundlerClient } = createClients(anvilPort, altoPort); + const { client, bundlerClient } = createClients(rpcPort(), altoPort); const sso = await SsoAccount.create(client, account, toEOASigner(privateKey)); const deployer = privateKeyToAccount(privateKey); const paymaster = await deployContract(client, privateKey, "MockPaymaster"); @@ -47,9 +46,9 @@ test("executes a transaction sponsored by a paymaster", { timeout: 120_000 }, as abi: parseAbi(["function deposit() external payable"]), functionName: "deposit", args: [], - value: 10n ** 18n, // 1 ETH + value: 10n ** 17n, // 0.1 ETH }) - await client.waitForTransactionReceipt({ hash: depositHash, timeout: 0 }); + await client.waitForTransactionReceipt({ hash: depositHash, timeout: 2_000 }); const balanceBefore = await client.getBalance({ address: account }); const sponsored = await bundlerClient.sendUserOperation({ @@ -58,7 +57,7 @@ test("executes a transaction sponsored by a paymaster", { timeout: 120_000 }, as paymaster, }); - const receipt = await bundlerClient.waitForUserOperationReceipt({ hash: sponsored, timeout: 0 }); + const receipt = await bundlerClient.waitForUserOperationReceipt({ hash: sponsored, timeout: 2_000 }); const balanceAfter = await client.getBalance({ address: account }); assert.equal(receipt.receipt.status, "success", "sponsored user operation should execute successfully"); @@ -67,7 +66,7 @@ test("executes a transaction sponsored by a paymaster", { timeout: 120_000 }, as test("checks ERC7739 EOA signature using ERC1271", { timeout: 120_000 }, async () => { const { account } = contractAddresses(); - const { client } = createClients(anvilPort, altoPort); + const { client } = createClients(rpcPort(), altoPort); const sso = await SsoAccount.create(client, account, toEOASigner(privateKey)); const erc1271Caller = await deployContract(client, privateKey, "MockERC1271Caller"); @@ -84,7 +83,7 @@ test("checks ERC7739 EOA signature using ERC1271", { timeout: 120_000 }, async ( ] }, domain: { - chainId: 1337, + chainId: chainId(), name: "ERC1271Caller", version: "1.0.0", verifyingContract: erc1271Caller, diff --git a/test/integration/passkey.test.ts b/test/integration/passkey.test.ts index 25a5c45..16bf138 100644 --- a/test/integration/passkey.test.ts +++ b/test/integration/passkey.test.ts @@ -4,9 +4,8 @@ import crypto from "crypto"; import { encodeFunctionData, toHex, parseAbi } from "viem"; import { SsoAccount } from "./account"; -import { contractAddresses, toEOASigner, toPasskeySigner, createClients, randomAddress, deployContract } from "./utils"; +import { contractAddresses, toEOASigner, toPasskeySigner, createClients, randomAddress, deployContract, chainId, rpcPort } from "./utils"; -const anvilPort = 8545; const altoPort = require("../../alto.json").port; const privateKey = "0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6"; @@ -20,7 +19,7 @@ const publicKey = { test("adds a Passkey to the account", { timeout: 120_000 }, async () => { const { account, webauthnValidator } = contractAddresses(); - const { client, bundlerClient } = createClients(anvilPort, altoPort); + const { client, bundlerClient } = createClients(rpcPort(), altoPort); const sso = await SsoAccount.create(client, account, toEOASigner(privateKey)); // add validation key via the passkey validator contract @@ -36,7 +35,7 @@ test("adds a Passkey to the account", { timeout: 120_000 }, async () => { }], }); - const receipt = await bundlerClient.waitForUserOperationReceipt({ hash, timeout: 0 }); + const receipt = await bundlerClient.waitForUserOperationReceipt({ hash, timeout: 2_000 }); assert.equal( receipt.receipt.status, "success", @@ -46,7 +45,7 @@ test("adds a Passkey to the account", { timeout: 120_000 }, async () => { test("executes a simple transfer signed using Passkey", { timeout: 120_000 }, async () => { const { account } = contractAddresses(); - const { client, bundlerClient } = createClients(anvilPort, altoPort); + const { client, bundlerClient } = createClients(rpcPort(), altoPort); const sso = await SsoAccount.create(client, account, toPasskeySigner(keyPair.privateKey, credentialId)); // transfer to a random address using passkey signer @@ -59,7 +58,7 @@ test("executes a simple transfer signed using Passkey", { timeout: 120_000 }, as }], }); - const receipt = await bundlerClient.waitForUserOperationReceipt({ hash, timeout: 0 }); + const receipt = await bundlerClient.waitForUserOperationReceipt({ hash, timeout: 2_000 }); assert.equal( receipt.receipt.status, "success", @@ -72,7 +71,7 @@ test("executes a simple transfer signed using Passkey", { timeout: 120_000 }, as test("checks ERC7739 Passkey signature using ERC1271", { timeout: 120_000 }, async () => { const { account } = contractAddresses(); - const { client } = createClients(anvilPort, altoPort); + const { client } = createClients(rpcPort(), altoPort); const sso = await SsoAccount.create(client, account, toPasskeySigner(keyPair.privateKey, credentialId)); const erc1271Caller = await deployContract(client, privateKey, "MockERC1271Caller"); @@ -89,7 +88,7 @@ test("checks ERC7739 Passkey signature using ERC1271", { timeout: 120_000 }, asy ] }, domain: { - chainId: 1337, + chainId: chainId(), name: "ERC1271Caller", version: "1.0.0", verifyingContract: erc1271Caller, diff --git a/test/integration/utils.ts b/test/integration/utils.ts index fab3991..6d166f5 100644 --- a/test/integration/utils.ts +++ b/test/integration/utils.ts @@ -8,8 +8,6 @@ import { concat, type Hex, type Address, - type WalletClient, - type PublicClient, } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { createBundlerClient } from "viem/account-abstraction"; @@ -17,8 +15,16 @@ import { localhost } from "viem/chains"; import crypto from "crypto"; +export function chainId() { + return Number(process.env.CHAIN_ID || '1337'); +} + +export function rpcPort() { + return Number(process.env.PORT || '8545'); +} + export function contractAddresses() { - const txs = require('../../broadcast/Deploy.s.sol/1337/deployAll-latest.json').transactions; + const txs = require(`../../broadcast/Deploy.s.sol/${chainId()}/deployAll-latest.json`).transactions; return { eoaValidator: txs[1].contractAddress as Address, sessionValidator: txs[3].contractAddress as Address, @@ -33,6 +39,9 @@ export function createClients(anvilPort: number, bundlerPort: number) { // smaller polling interval to speed up the test const pollingInterval = 100; + // @ts-ignore + localhost.id = chainId(); + const client = createPublicClient({ chain: localhost, transport: http(`http://localhost:${anvilPort}`), @@ -59,7 +68,7 @@ export async function deployContract(client: any, privateKey: Hex, name: string) abi: [], bytecode: require(`../../out/${name}.sol/${name}.json`).bytecode.object }); - const deployment = await client.waitForTransactionReceipt({ hash: deploymentHash, timeout: 100 }); + const deployment = await client.waitForTransactionReceipt({ hash: deploymentHash, timeout: 2_000 }); return deployment.contractAddress!; }