From 82e935ea0404d14b4fcccd5abd3a40dee7068904 Mon Sep 17 00:00:00 2001 From: Joe Pegler Date: Thu, 29 Aug 2024 12:52:51 +0100 Subject: [PATCH] chore: fix deployment state --- package.json | 4 +- src/account/BaseSmartContractAccount.ts | 21 +++- src/account/NexusSmartAccount.ts | 33 +++-- src/bundler/utils/HelperFunction.ts | 4 +- tests/account.read.test.ts | 4 +- tests/account.write.test.ts | 2 +- tests/modules.k1Validator.write.test.ts | 25 +--- tests/modules.ownableExecutor.read.test.ts | 2 +- tests/modules.ownableExecutor.write.test.ts | 2 +- ...les.ownableValidator.install.write.test.ts | 2 +- ...s.ownableValidator.uninstall.write.test.ts | 2 +- tests/playground.test.ts | 101 +++++++++------ tests/src/README.md | 27 +++- tests/src/globalSetup.ts | 4 +- tests/src/testSetup.ts | 46 +++++-- tests/src/testUtils.ts | 117 ++++++++++++++---- 16 files changed, 257 insertions(+), 139 deletions(-) diff --git a/package.json b/package.json index 2a71f27c..f737e324 100644 --- a/package.json +++ b/package.json @@ -60,8 +60,8 @@ "clean": "rimraf ./dist/_esm ./dist/_cjs ./dist/_types ./dist/tsconfig", "test": "vitest -c ./tests/vitest.config.ts", "test:watch": "bun run test dev", - "playground": "RUN_PLAYGROUND=true bun run test -t=Playground", - "playground:watch": "RUN_PLAYGROUND=true bun run test -t=Playground --watch", + "playground": "RUN_PLAYGROUND=true vitest -c ./tests/vitest.config.ts -t=playground", + "playground:watch": "RUN_PLAYGROUND=true bun run test -t=playground --watch", "size": "size-limit", "docs": "typedoc --tsconfig ./tsconfig/tsconfig.esm.json", "docs:deploy": "bun run docs && gh-pages -d docs", diff --git a/src/account/BaseSmartContractAccount.ts b/src/account/BaseSmartContractAccount.ts index ee116552..3faf3936 100644 --- a/src/account/BaseSmartContractAccount.ts +++ b/src/account/BaseSmartContractAccount.ts @@ -201,16 +201,21 @@ export abstract class BaseSmartContractAccount< validationMode?: typeof MODE_VALIDATION | typeof MODE_MODULE_ENABLE ): Promise + private async _isDeployed(): Promise { + const contractCode = await this.publicClient.getBytecode({ + address: await this.getAddress() + }) + return (contractCode?.length ?? 0) > 2 + } + async getInitCode(): Promise { if (this.deploymentState === DeploymentState.DEPLOYED) { return "0x" } - const contractCode = await this.publicClient.getBytecode({ - address: await this.getAddress() - }) + const isDeployed = await this._isDeployed() - if ((contractCode?.length ?? 0) > 2) { + if (isDeployed) { this.deploymentState = DeploymentState.DEPLOYED return "0x" } @@ -273,7 +278,7 @@ export abstract class BaseSmartContractAccount< return this.entryPointAddress } - async isAccountDeployed(): Promise { + async isAccountDeployed(forceFetch = false): Promise { return (await this.getDeploymentState()) === DeploymentState.DEPLOYED } @@ -284,9 +289,13 @@ export abstract class BaseSmartContractAccount< ? DeploymentState.DEPLOYED : DeploymentState.NOT_DEPLOYED } + if (this.deploymentState === DeploymentState.NOT_DEPLOYED) { + if (await this._isDeployed()) { + this.deploymentState = DeploymentState.DEPLOYED + } + } return this.deploymentState } - /** * https://eips.ethereum.org/EIPS/eip-4337#first-time-account-creation * The initCode field (if non-zero length) is parsed as a 20-byte address, diff --git a/src/account/NexusSmartAccount.ts b/src/account/NexusSmartAccount.ts index a258d528..26887186 100644 --- a/src/account/NexusSmartAccount.ts +++ b/src/account/NexusSmartAccount.ts @@ -723,6 +723,7 @@ export class NexusSmartAccount extends BaseSmartContractAccount { if (await this.isAccountDeployed()) return "0x" const factoryData = (await this.getFactoryData()) as Hex + return concatHex([this.factoryAddress, factoryData]) } @@ -1035,16 +1036,12 @@ export class NexusSmartAccount extends BaseSmartContractAccount { * const { success, receipt } = await wait(); * */ - async sendUserOp( - userOp: Partial - ): Promise { - // biome-ignore lint/performance/noDelete: - delete userOp.signature - const userOperation = await this.signUserOp(userOp) - - const bundlerResponse = await this.sendSignedUserOp(userOperation) - - return bundlerResponse + async sendUserOp({ + signature, + ...userOpWithoutSignature + }: Partial): Promise { + const userOperation = await this.signUserOp(userOpWithoutSignature) + return await this.sendSignedUserOp(userOperation) } /** @@ -1322,13 +1319,12 @@ export class NexusSmartAccount extends BaseSmartContractAccount { : [manyOrOneTransactions], buildUseropDto ) - const payload = await this.sendUserOp(userOp) - this.setDeploymentState(payload) // Don't wait - return payload + const response = await this.sendUserOp(userOp) + this.setDeploymentState(response) // don't wait for this to finish... + return response } - private async setDeploymentState({ wait }: UserOpResponse) { - if (this.deploymentState === DeploymentState.DEPLOYED) return + public async setDeploymentState({ wait }: UserOpResponse) { const { success } = await wait() if (success) { this.deploymentState = DeploymentState.DEPLOYED @@ -1394,7 +1390,8 @@ export class NexusSmartAccount extends BaseSmartContractAccount { const dummySignatureFetchPromise = this.getDummySignatures() const [nonceFromFetch, dummySignature] = await Promise.all([ this.getBuildUserOpNonce(buildUseropDto?.nonceOptions), - dummySignatureFetchPromise + dummySignatureFetchPromise, + this.getInitCode() // Not used, but necessary to determine if the account is deployed. Will return immediately if the account is already deployed ]) if (transactions.length === 0) { @@ -1724,13 +1721,13 @@ export class NexusSmartAccount extends BaseSmartContractAccount { async isModuleInstalled(module: Module) { if (await this.isAccountDeployed()) { const accountContract = await this._getAccountContract() - return await accountContract.read.isModuleInstalled([ + const result = await accountContract.read.isModuleInstalled([ BigInt(moduleTypeIds[module.type]), module.moduleAddress, module.data ?? "0x" ]) + return result } - Logger.warn("A module cannot be installed on an undeployed account") return false } diff --git a/src/bundler/utils/HelperFunction.ts b/src/bundler/utils/HelperFunction.ts index 9c57546a..cf96b536 100644 --- a/src/bundler/utils/HelperFunction.ts +++ b/src/bundler/utils/HelperFunction.ts @@ -58,7 +58,9 @@ function decodeErrorCode(errorCode: string) { "0x40d3d1a40000000000000000000000004d8249d21c9553b1bd23cabf611011376dd3416a": "LinkedList_EntryAlreadyInList", "0x40d3d1a40000000000000000000000004b8306128aed3d49a9d17b99bf8082d4e406fa1f": - "LinkedList_EntryAlreadyInList" + "LinkedList_EntryAlreadyInList", + "0x40d3d1a4000000000000000000000000d98238bbaea4f91683d250003799ead31d7f5c55": + "Error: Custom error message about the K1Validator contract" // Add more error codes and their corresponding human-readable messages here } const decodedError = errorMap[errorCode] || errorCode diff --git a/tests/account.read.test.ts b/tests/account.read.test.ts index 4b803499..a5f6685e 100644 --- a/tests/account.read.test.ts +++ b/tests/account.read.test.ts @@ -49,7 +49,7 @@ import { } from "./src/testUtils" import type { MasterClient, NetworkConfig } from "./src/testUtils" -const NETWORK_TYPE: TestFileNetworkType = "GLOBAL" +const NETWORK_TYPE: TestFileNetworkType = "COMMON_LOCALHOST" describe("account.read", () => { let network: NetworkConfig @@ -66,7 +66,7 @@ describe("account.read", () => { let smartAccountAddress: Hex beforeAll(async () => { - network = await toNetwork(NETWORK_TYPE) + network = (await toNetwork(NETWORK_TYPE)) as NetworkConfig chain = network.chain bundlerUrl = network.bundlerUrl diff --git a/tests/account.write.test.ts b/tests/account.write.test.ts index 537e2612..8e0c1c02 100644 --- a/tests/account.write.test.ts +++ b/tests/account.write.test.ts @@ -21,7 +21,7 @@ import { } from "./src/testUtils" import type { MasterClient, NetworkConfig } from "./src/testUtils" -const NETWORK_TYPE: TestFileNetworkType = "LOCAL" +const NETWORK_TYPE: TestFileNetworkType = "FILE_LOCALHOST" describe("account.write", () => { let network: NetworkConfig diff --git a/tests/modules.k1Validator.write.test.ts b/tests/modules.k1Validator.write.test.ts index 88365714..339abda8 100644 --- a/tests/modules.k1Validator.write.test.ts +++ b/tests/modules.k1Validator.write.test.ts @@ -9,7 +9,6 @@ import { encodePacked } from "viem" import { afterAll, beforeAll, describe, expect, test } from "vitest" -import { createK1ValidatorModule, getRandomSigner } from "../src" import addresses from "../src/__contracts/addresses" import { type NexusSmartAccount, @@ -28,7 +27,7 @@ import { } from "./src/testUtils" import type { MasterClient, NetworkConfig } from "./src/testUtils" -const NETWORK_TYPE: TestFileNetworkType = "LOCAL" +const NETWORK_TYPE: TestFileNetworkType = "FILE_LOCALHOST" describe("modules.k1Validator.write", () => { let network: NetworkConfig @@ -110,12 +109,13 @@ describe("modules.k1Validator.write", () => { }) test("should install k1 Validator with 1 owner", async () => { - // console.log(smartAccount.activeValidationModule, addresses.K1Validator) const isInstalledBefore = await smartAccount.isModuleInstalled({ type: "validator", moduleAddress: addresses.K1Validator }) + const modules = await smartAccount.getInstalledModules() + if (!isInstalledBefore) { const { wait } = await smartAccount.installModule({ moduleAddress: addresses.K1Validator, @@ -123,27 +123,8 @@ describe("modules.k1Validator.write", () => { data: encodePacked(["address"], [await smartAccount.getAddress()]) }) - console.log("waiting....") const { success: installSuccess } = await wait() - console.log({ installSuccess }) - expect(installSuccess).toBe(true) - - const k1ValidationModule = await createK1ValidatorModule( - smartAccount.getSigner() - ) - - const { wait: waitUninstall } = await smartAccount.uninstallModule({ - moduleAddress: addresses.K1Validator, - type: "validator", - data: encodePacked(["address"], [await smartAccount.getAddress()]) - }) - console.log("waiting....") - const { success: successUninstall } = await waitUninstall() - console.log({ successUninstall }) - - smartAccount.setActiveValidationModule(k1ValidationModule) - expect(successUninstall).toBe(true) } }, 60000) diff --git a/tests/modules.ownableExecutor.read.test.ts b/tests/modules.ownableExecutor.read.test.ts index cf98ccf9..1e9336d9 100644 --- a/tests/modules.ownableExecutor.read.test.ts +++ b/tests/modules.ownableExecutor.read.test.ts @@ -23,7 +23,7 @@ import { } from "./src/testUtils" import type { MasterClient, NetworkConfig } from "./src/testUtils" -const NETWORK_TYPE: TestFileNetworkType = "LOCAL" +const NETWORK_TYPE: TestFileNetworkType = "FILE_LOCALHOST" describe("modules.ownable.executor.read", () => { let network: NetworkConfig diff --git a/tests/modules.ownableExecutor.write.test.ts b/tests/modules.ownableExecutor.write.test.ts index 0543a92f..41522182 100644 --- a/tests/modules.ownableExecutor.write.test.ts +++ b/tests/modules.ownableExecutor.write.test.ts @@ -21,7 +21,7 @@ import { } from "./src/testUtils" import type { MasterClient, NetworkConfig } from "./src/testUtils" -const NETWORK_TYPE: TestFileNetworkType = "LOCAL" +const NETWORK_TYPE: TestFileNetworkType = "FILE_LOCALHOST" describe("modules.ownable.executor.write", () => { let network: NetworkConfig diff --git a/tests/modules.ownableValidator.install.write.test.ts b/tests/modules.ownableValidator.install.write.test.ts index c40a508d..680b4ca8 100644 --- a/tests/modules.ownableValidator.install.write.test.ts +++ b/tests/modules.ownableValidator.install.write.test.ts @@ -21,7 +21,7 @@ import { } from "./src/testUtils" import type { MasterClient, NetworkConfig } from "./src/testUtils" -const NETWORK_TYPE: TestFileNetworkType = "LOCAL" +const NETWORK_TYPE: TestFileNetworkType = "FILE_LOCALHOST" describe("modules.ownable.validator.install.write", () => { let network: NetworkConfig diff --git a/tests/modules.ownableValidator.uninstall.write.test.ts b/tests/modules.ownableValidator.uninstall.write.test.ts index 8b29b3b1..09fbd4a7 100644 --- a/tests/modules.ownableValidator.uninstall.write.test.ts +++ b/tests/modules.ownableValidator.uninstall.write.test.ts @@ -21,7 +21,7 @@ import { } from "./src/testUtils" import type { MasterClient, NetworkConfig } from "./src/testUtils" -const NETWORK_TYPE: TestFileNetworkType = "LOCAL" +const NETWORK_TYPE: TestFileNetworkType = "FILE_LOCALHOST" describe("modules.ownable.validator.uninstall.write", () => { let network: NetworkConfig diff --git a/tests/playground.test.ts b/tests/playground.test.ts index 86c74c34..bbfd7db5 100644 --- a/tests/playground.test.ts +++ b/tests/playground.test.ts @@ -1,9 +1,11 @@ import { config } from "dotenv" import { http, + Account, type Address, type Chain, type Hex, + type PrivateKeyAccount, type PublicClient, type WalletClient, createPublicClient, @@ -18,54 +20,67 @@ import { getCustomChain } from "../src/account" import { createK1ValidatorModule } from "../src/modules" -import { getBundlerUrl } from "./src/testUtils" -config() - -const privateKey = process.env.E2E_PRIVATE_KEY_ONE -const chainId = process.env.CHAIN_ID -const rpcUrl = process.env.RPC_URL //Optional, taken from chain (using chainId) if not provided -const _bundlerUrl = process.env.BUNDLER_URL // Optional, taken from chain (using chainId) if not provided -const conditionalDescribe = - process.env.RUN_PLAYGROUND === "true" ? describe : describe.skip +import { + type TestFileNetworkType, + describeWithPlaygroundGuard, + toNetwork +} from "./src/testSetup" +import { + type MasterClient, + type NetworkConfig, + getBundlerUrl, + getTestAccount, + toTestClient, + topUp +} from "./src/testUtils" -if (!privateKey) throw new Error("Missing env var E2E_PRIVATE_KEY_ONE") -if (!chainId) throw new Error("Missing env var CHAIN_ID") +const NETWORK_TYPE: TestFileNetworkType = "PUBLIC_TESTNET" // Remove the following lines to use the default factory and validator addresses // These are relevant only for now on base sopelia chain and are likely to change const k1ValidatorAddress = "0x663E709f60477f07885230E213b8149a7027239B" const factoryAddress = "0x887Ca6FaFD62737D0E79A2b8Da41f0B15A864778" -conditionalDescribe("playground", () => { - let ownerAddress: Address - let walletClient: WalletClient - let smartAccount: NexusSmartAccount - let smartAccountAddress: Address +describeWithPlaygroundGuard("playground", () => { + let network: NetworkConfig + // Nexus Config let chain: Chain let bundlerUrl: string - let publicClient: PublicClient + let walletClient: WalletClient + + // Test utils + let publicClient: PublicClient // testClient not available on public testnets + let account: PrivateKeyAccount + let smartAccount: NexusSmartAccount + let smartAccountAddress: Hex beforeAll(async () => { - try { - chain = getChain(+chainId) - } catch (e) { - if (!rpcUrl) throw new Error("Missing env var RPC_URL") - chain = getCustomChain("Custom Chain", +chainId, rpcUrl) - } + network = await toNetwork(NETWORK_TYPE) + + chain = network.chain + bundlerUrl = network.bundlerUrl + account = network.account as PrivateKeyAccount + walletClient = createWalletClient({ - account: privateKeyToAccount( - (privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`) as Hex - ), + account, chain, transport: http() }) - ownerAddress = walletClient?.account?.address as Hex + publicClient = createPublicClient({ chain, transport: http() }) - bundlerUrl = _bundlerUrl ?? getBundlerUrl(+chainId) + smartAccount = await createSmartAccountClient({ + signer: walletClient, + bundlerUrl, + chain, + k1ValidatorAddress, + factoryAddress + }) + + smartAccountAddress = await smartAccount.getAddress() }) test("should have factory and k1Validator deployed", async () => { @@ -95,39 +110,43 @@ conditionalDescribe("playground", () => { test("should log relevant addresses", async () => { smartAccountAddress = await smartAccount.getAddress() - console.log({ ownerAddress, smartAccountAddress }) + console.log({ smartAccountAddress }) }) - test("should check balances of relevant addresses", async () => { + test("should check balances and top up relevant addresses", async () => { const [ownerBalance, smartAccountBalance] = await Promise.all([ publicClient.getBalance({ - address: ownerAddress + address: account.address }), publicClient.getBalance({ address: smartAccountAddress }) ]) console.log({ ownerBalance, smartAccountBalance }) + const balancesAreOfCorrectType = [ownerBalance, smartAccountBalance].every( (balance) => typeof balance === "bigint" ) + if (smartAccountBalance === 0n) { + const hash = await walletClient.sendTransaction({ + chain, + account, + to: smartAccountAddress, + value: 1000000000000000000n + }) + const receipt = await publicClient.waitForTransactionReceipt({ hash }) + console.log({ receipt }) + } expect(balancesAreOfCorrectType).toBeTruthy() }) test("should send some native token", async () => { const balanceBefore = await publicClient.getBalance({ - address: ownerAddress + address: account.address }) - const k1ValidatorModule = await createK1ValidatorModule( - smartAccount.getSigner(), - k1ValidatorAddress - ) - - smartAccount.setActiveValidationModule(k1ValidatorModule) - const { wait } = await smartAccount.sendTransaction({ - to: ownerAddress, + to: account.address, data: "0x", value: 1n }) @@ -141,7 +160,7 @@ conditionalDescribe("playground", () => { console.log({ transactionHash }) const balanceAfter = await publicClient.getBalance({ - address: ownerAddress + address: account.address }) expect(balanceAfter - balanceBefore).toBe(1n) diff --git a/tests/src/README.md b/tests/src/README.md index d120de99..3d886816 100644 --- a/tests/src/README.md +++ b/tests/src/README.md @@ -26,20 +26,21 @@ The script performs the following: To prevent tests from conflicting with one another, networks can be scoped at three levels: ### Global Scope -- Use by setting `const NETWORK_TYPE: TestFileNetworkType = "LOCAL"` at the top of the test file. +- Use by setting `const NETWORK_TYPE: TestFileNetworkType = "FILE_LOCALHOST"` at the top of the test file. - Suitable when you're sure that tests in the file will **not** conflict with other tests using the global network. ### Local Scope -- Use by setting `const NETWORK_TYPE: TestFileNetworkType = "LOCAL"` for test files that may conflict with others. +- Use by setting `const NETWORK_TYPE: TestFileNetworkType = "FILE_LOCALHOST"` for test files that may conflict with others. - Networks scoped locally are isolated to the file in which they are used. - Tests within the same file using a local network may conflict with each other. If needed, split tests into separate files or use the Test Scope. ### Test Scope -- A network is spun up *only* for the individual test in which it is used. Access this via the `scopedTest` helper in the same file as `"GLOBAL"` or `"LOCAL"` network types. +- A network is spun up *only* for the individual test in which it is used. Access this via the `isolatedTest` helper in the same file as `"COMMON_LOCALHOST"` or `"FILE_LOCALHOST"` network types. Example usage: ```typescript -scopedTest("should be used in the following way", async({ config: { bundlerUrl, chain, fundedClients }}) => { +isolatedTest("should be used in the following way", async({ config: { bundlerUrl, chain, fundedClients }}) => { + // chain, bundlerUrl spun up just in time for this test only... expect(await fundedClients.smartAccount.getAccountAddress()).toBeTruthy(); }); ``` @@ -49,7 +50,23 @@ scopedTest("should be used in the following way", async({ config: { bundlerUrl, > Using *many* test files is preferable, as describe blocks run in parallel. ## Testing Custom/New Chains -- There is one area where SDK tests can be run against a remote testnet: the playground. +- There is currently one area where SDK tests can be run against a remote testnet: the playground. +- Additionally there are helpers for running tests on files on a public testnet: + - `const NETWORK_TYPE: TestFileNetworkType = "TESTNET"` will pick up relevant configuration from environment variables, and can be used at the top of a test file to have tests run against the specified testnet instead of the localhost + - If you want to run a single test on a public testnet *from inside a different describe block* you can use the: `testnetTest` helper: + +Example usage: +```typescript +testnetTest("should be used in the following way", async({ config: { bundlerUrl, chain, fundedClients }}) => { + // chain, bundlerUrl etc taken from environment variables... + expect(await fundedClients.smartAccount.getAccountAddress()).toBeTruthy(); +}); +``` + +> **Note:** +> As testnetTest runs against a public testnet the account related to the privatekey (in your env var) must be funded, and the testnet is not 'ephemeral' meaning state is persisted on the testnet after the test teardown. + + - The playground does not run in CI/CD but can be triggered manually from the GitHub Actions UI or locally via bun run playground. - The playground network is configured with environment variables: - E2E_PRIVATE_KEY_ONE diff --git a/tests/src/globalSetup.ts b/tests/src/globalSetup.ts index 80dcf92a..b3e12288 100644 --- a/tests/src/globalSetup.ts +++ b/tests/src/globalSetup.ts @@ -1,12 +1,12 @@ import { type NetworkConfig, type NetworkConfigWithBundler, - initNetwork + initLocalhostNetwork } from "./testUtils" let globalConfig: NetworkConfigWithBundler export const setup = async ({ provide }) => { - globalConfig = await initNetwork() + globalConfig = await initLocalhostNetwork() const { bundlerInstance, instance, ...serializeableConfig } = globalConfig provide("globalNetwork", serializeableConfig) } diff --git a/tests/src/testSetup.ts b/tests/src/testSetup.ts index 073311aa..b3d0b9ba 100644 --- a/tests/src/testSetup.ts +++ b/tests/src/testSetup.ts @@ -1,8 +1,10 @@ -import { inject, test } from "vitest" +import { describe, inject, test } from "vitest" import { type FundedTestClients, + type NetworkConfig, type NetworkConfigWithBundler, - initNetwork, + initLocalhostNetwork, + initTestnetNetwork, toFundedTestClients } from "./testUtils" @@ -10,13 +12,16 @@ export type NetworkConfigWithTestClients = NetworkConfigWithBundler & { fundedTestClients: FundedTestClients } -export const scopedTest = test.extend<{ +export const isolatedTest = test.extend<{ config: NetworkConfigWithTestClients }>({ // biome-ignore lint/correctness/noEmptyPattern: Needed in vitest :/ config: async ({}, use) => { - const testNetwork = await initNetwork() - const fundedTestClients = await toFundedTestClients(testNetwork) + const testNetwork = await initLocalhostNetwork() + const fundedTestClients = await toFundedTestClients({ + chain: testNetwork.chain, + bundlerUrl: testNetwork.bundlerUrl + }) await use({ ...testNetwork, fundedTestClients }) await Promise.all([ testNetwork.instance.stop(), @@ -25,7 +30,30 @@ export const scopedTest = test.extend<{ } }) -export type TestFileNetworkType = "LOCAL" | "GLOBAL" -export const toNetwork = async (networkType: TestFileNetworkType) => - // @ts-ignore - await (networkType === "GLOBAL" ? inject("globalNetwork") : initNetwork()) +export const testnetTest = test.extend<{ + config: NetworkConfig +}>({ + // biome-ignore lint/correctness/noEmptyPattern: Needed in vitest :/ + config: async ({}, use) => { + const testNetwork = await toNetwork("PUBLIC_TESTNET") + await use(testNetwork) + } +}) + +export type TestFileNetworkType = + | "FILE_LOCALHOST" + | "COMMON_LOCALHOST" + | "PUBLIC_TESTNET" + +export const toNetwork = async ( + networkType: TestFileNetworkType +): Promise => + await (networkType === "COMMON_LOCALHOST" + ? // @ts-ignore + inject("globalNetwork") + : networkType === "FILE_LOCALHOST" + ? initLocalhostNetwork() + : initTestnetNetwork()) + +export const describeWithPlaygroundGuard = + process.env.RUN_PLAYGROUND === "true" ? describe : describe.skip diff --git a/tests/src/testUtils.ts b/tests/src/testUtils.ts index 89d15bc6..04dba2dc 100644 --- a/tests/src/testUtils.ts +++ b/tests/src/testUtils.ts @@ -1,13 +1,16 @@ -import fs from "node:fs" +import { config } from "dotenv" import getPort from "get-port" import { alto, anvil } from "prool/instances" import { http, - type Abi, type Account, type Address, type Chain, type Hex, + type PrivateKeyAccount, + type PublicClient, + type WalletClient, + createPublicClient, createTestClient, createWalletClient, encodeAbiParameters, @@ -16,15 +19,16 @@ import { publicActions, walletActions } from "viem" -import { mnemonicToAccount } from "viem/accounts" +import { mnemonicToAccount, privateKeyToAccount } from "viem/accounts" import { type EIP712DomainReturn, type NexusSmartAccount, createSmartAccountClient } from "../../src" import contracts from "../../src/__contracts" -import { getCustomChain } from "../../src/account/utils" +import { getChain, getCustomChain } from "../../src/account/utils" import { Logger } from "../../src/account/utils/Logger" +import { createBundler } from "../../src/bundler" import { ENTRY_POINT_SIMULATIONS_CREATECALL, ENTRY_POINT_V07_CREATECALL, @@ -35,6 +39,8 @@ import { } from "./callDatas" import { deployProcess } from "./deployProcess" +config() + type AnvilInstance = ReturnType type BundlerInstance = ReturnType type BundlerDto = { @@ -52,7 +58,9 @@ export type NetworkConfigWithBundler = AnvilDto & BundlerDto export type NetworkConfig = Omit< NetworkConfigWithBundler, "instance" | "bundlerInstance" -> +> & { + account?: PrivateKeyAccount +} export const pKey = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" // This is a publicly available private key meant only for testing only @@ -83,19 +91,58 @@ export const killNetwork = (ids: number[]) => }) ) -export const initNetwork = async (): Promise => { - const configuredNetwork = await initAnvilPayload() - const bundlerConfig = await initBundlerInstance({ - rpcUrl: configuredNetwork.rpcUrl - }) - allInstances.set(configuredNetwork.instance.port, configuredNetwork.instance) - allInstances.set( - bundlerConfig.bundlerInstance.port, - bundlerConfig.bundlerInstance - ) - return { ...configuredNetwork, ...bundlerConfig } +export const initTestnetNetwork = async (): Promise => { + const privateKey = process.env.E2E_PRIVATE_KEY_ONE + const chainId = process.env.CHAIN_ID + const rpcUrl = process.env.RPC_URL //Optional, taken from chain (using chainId) if not provided + const _bundlerUrl = process.env.BUNDLER_URL // Optional, taken from chain (using chainId) if not provided + + let chain: Chain + + if (!privateKey) throw new Error("Missing env var E2E_PRIVATE_KEY_ONE") + if (!chainId) throw new Error("Missing env var CHAIN_ID") + + try { + chain = getChain(+chainId) + } catch (e) { + if (!rpcUrl) throw new Error("Missing env var RPC_URL") + chain = getCustomChain("Custom Chain", +chainId, rpcUrl) + } + const bundlerUrl = _bundlerUrl ?? getBundlerUrl(+chainId) + + return { + rpcUrl: chain.rpcUrls.default.http[0], + rpcPort: 0, + chain, + bundlerUrl, + bundlerPort: 0, + account: privateKeyToAccount( + privateKey?.startsWith("0x") ? (privateKey as Hex) : `0x${privateKey}` + ) + } } +export const initLocalhostNetwork = + async (): Promise => { + const configuredNetwork = await initAnvilPayload() + const bundlerConfig = await initBundlerInstance({ + rpcUrl: configuredNetwork.rpcUrl + }) + await ensureBundlerIsReady( + bundlerConfig.bundlerUrl, + getTestChainFromPort(configuredNetwork.rpcPort) + ) + allInstances.set( + configuredNetwork.instance.port, + configuredNetwork.instance + ) + allInstances.set( + bundlerConfig.bundlerInstance.port, + bundlerConfig.bundlerInstance + ) + return { ...configuredNetwork, ...bundlerConfig } + } + export type MasterClient = ReturnType export const toTestClient = (chain: Chain, account: Account) => createTestClient({ @@ -126,6 +173,25 @@ export const toBundlerInstance = async ({ return instance } +export const ensureBundlerIsReady = async ( + bundlerUrl: string, + chain: Chain +) => { + const bundler = await createBundler({ + chain, + bundlerUrl + }) + + while (true) { + try { + await bundler.getGasFeeValues() + return + } catch { + await new Promise((resolve) => setTimeout(resolve, 1000)) + } + } +} + export const toConfiguredAnvil = async ({ rpcPort }: { rpcPort: number }): Promise => { @@ -142,10 +208,10 @@ export const toConfiguredAnvil = async ({ return instance } +const ports: number[] = [] export const initAnvilPayload = async (): Promise => { - const rpcPort = await getPort({ - port: [...Array.from({ length: 10 }, (_, i) => 55000 + i)] - }) + const rpcPort = await getPort({ exclude: ports }) + ports.push(rpcPort) const rpcUrl = `http://localhost:${rpcPort}` const chain = getTestChainFromPort(rpcPort) const instance = await toConfiguredAnvil({ rpcPort }) @@ -155,7 +221,8 @@ export const initAnvilPayload = async (): Promise => { export const initBundlerInstance = async ({ rpcUrl }: { rpcUrl: string }): Promise => { - const bundlerPort = await getPort() + const bundlerPort = await getPort({ exclude: ports }) + ports.push(bundlerPort) const bundlerUrl = `http://localhost:${bundlerPort}` const bundlerInstance = await toBundlerInstance({ rpcUrl, bundlerPort }) return { bundlerInstance, bundlerUrl, bundlerPort } @@ -194,12 +261,10 @@ export const nonZeroBalance = async ( } export type FundedTestClients = Awaited> -export const toFundedTestClients = async ( - network: NetworkConfigWithBundler -) => { - const chain = network.chain - const bundlerUrl = network.bundlerUrl - +export const toFundedTestClients = async ({ + chain, + bundlerUrl +}: { chain: Chain; bundlerUrl: string }) => { const account = getTestAccount(2) const recipientAccount = getTestAccount(3)