diff --git a/examples/send-tx.ts b/examples/send-tx.ts index 4dbf114..f0ff075 100644 --- a/examples/send-tx.ts +++ b/examples/send-tx.ts @@ -1,5 +1,6 @@ import dotenv from "dotenv"; import { ethers } from "ethers"; +import { isAddress } from "viem"; import { loadArgs, loadEnv } from "./cli"; import { TransactionManager } from "../src"; @@ -29,7 +30,11 @@ async function main(): Promise { }, ]; // Add Recovery if safe not deployed & recoveryAddress was provided. - if (!(await txManager.safeDeployed(chainId)) && recoveryAddress) { + if ( + !(await txManager.safeDeployed(chainId)) && + recoveryAddress && + isAddress(recoveryAddress) + ) { const recoveryTx = txManager.addOwnerTx(recoveryAddress); // This would happen (sequentially) after the userTx, but all executed in a single transactions.push(recoveryTx); diff --git a/src/lib/bundler.ts b/src/lib/bundler.ts index 9637e9c..65cd44b 100644 --- a/src/lib/bundler.ts +++ b/src/lib/bundler.ts @@ -25,7 +25,9 @@ export class Erc4337Bundler { this.entryPointAddress = entryPointAddress; this.apiKey = apiKey; this.chainId = chainId; - this.provider = new ethers.JsonRpcProvider(bundlerUrl(chainId, this.apiKey)); + this.provider = new ethers.JsonRpcProvider( + bundlerUrl(chainId, this.apiKey) + ); } async getPaymasterData( diff --git a/src/lib/safe.ts b/src/lib/safe.ts index 5821956..078b552 100644 --- a/src/lib/safe.ts +++ b/src/lib/safe.ts @@ -6,8 +6,20 @@ import { getSafe4337ModuleDeployment, getSafeModuleSetupDeployment, } from "@safe-global/safe-modules-deployments"; -import { ethers } from "ethers"; -import { Address, Hash, Hex } from "viem"; +import { + Address, + encodeFunctionData, + encodePacked, + getCreate2Address, + Hash, + Hex, + keccak256, + ParseAbi, + parseAbi, + PublicClient, + toHex, + zeroAddress, +} from "viem"; import { GasPrice, @@ -15,28 +27,39 @@ import { UnsignedUserOperation, UserOperation, } from "../types"; -import { PLACEHOLDER_SIG, packGas, packPaymasterData } from "../util"; +import { + PLACEHOLDER_SIG, + getClient, + packGas, + packPaymasterData, +} from "../util"; + +interface DeploymentData { + abi: unknown[] | ParseAbi; + address: `0x${string}`; +} /** * All contracts used in account creation & execution */ export class ContractSuite { - provider: ethers.JsonRpcProvider; - singleton: ethers.Contract; - proxyFactory: ethers.Contract; - m4337: ethers.Contract; - moduleSetup: ethers.Contract; - entryPoint: ethers.Contract; + // Used only for stateless contract reads. + dummyClient: PublicClient; + singleton: DeploymentData; + proxyFactory: DeploymentData; + m4337: DeploymentData; + moduleSetup: DeploymentData; + entryPoint: DeploymentData; constructor( - provider: ethers.JsonRpcProvider, - singleton: ethers.Contract, - proxyFactory: ethers.Contract, - m4337: ethers.Contract, - moduleSetup: ethers.Contract, - entryPoint: ethers.Contract + client: PublicClient, + singleton: DeploymentData, + proxyFactory: DeploymentData, + m4337: DeploymentData, + moduleSetup: DeploymentData, + entryPoint: DeploymentData ) { - this.provider = provider; + this.dummyClient = client; this.singleton = singleton; this.proxyFactory = proxyFactory; this.m4337 = m4337; @@ -46,89 +69,106 @@ export class ContractSuite { static async init(): Promise { // TODO - this is a cheeky hack. - const provider = new ethers.JsonRpcProvider("https://rpc2.sepolia.org"); - const safeDeployment = (fn: DeploymentFunction): Promise => - getDeployment(fn, { provider, version: "1.4.1" }); + const client = getClient(11155111); + const safeDeployment = (fn: DeploymentFunction): Promise => + getDeployment(fn, { version: "1.4.1" }); const m4337Deployment = async ( fn: DeploymentFunction - ): Promise => { - return getDeployment(fn, { provider, version: "0.3.0" }); + ): Promise => { + return getDeployment(fn, { version: "0.3.0" }); }; - // Need this first to get entryPoint address - const m4337 = await m4337Deployment(getSafe4337ModuleDeployment); - - const [singleton, proxyFactory, moduleSetup, supportedEntryPoint] = - await Promise.all([ - safeDeployment(getSafeL2SingletonDeployment), - safeDeployment(getProxyFactoryDeployment), - m4337Deployment(getSafeModuleSetupDeployment), - m4337.SUPPORTED_ENTRYPOINT(), - ]); - const entryPoint = new ethers.Contract( - supportedEntryPoint, - ["function getNonce(address, uint192 key) view returns (uint256 nonce)"], - provider - ); - console.log("Initialized ERC4337 & Safe Module Contracts:", { - singleton: await singleton.getAddress(), - proxyFactory: await proxyFactory.getAddress(), - m4337: await m4337.getAddress(), - moduleSetup: await moduleSetup.getAddress(), - entryPoint: await entryPoint.getAddress(), - }); + + const [singleton, proxyFactory, moduleSetup, m4337] = await Promise.all([ + safeDeployment(getSafeL2SingletonDeployment), + safeDeployment(getProxyFactoryDeployment), + m4337Deployment(getSafeModuleSetupDeployment), + m4337Deployment(getSafe4337ModuleDeployment), + ]); + + // console.log("Initialized ERC4337 & Safe Module Contracts:", { + // singleton: await singleton.getAddress(), + // proxyFactory: await proxyFactory.getAddress(), + // m4337: await m4337.getAddress(), + // moduleSetup: await moduleSetup.getAddress(), + // entryPoint: await entryPoint.getAddress(), + // }); return new ContractSuite( - provider, + client, singleton, proxyFactory, m4337, moduleSetup, - entryPoint + // EntryPoint: + { + address: (await client.readContract({ + address: m4337.address, + abi: m4337.abi, + functionName: "SUPPORTED_ENTRYPOINT", + })) as Address, + abi: parseAbi([ + "function getNonce(address, uint192 key) view returns (uint256 nonce)", + ]), + } ); } - async addressForSetup( - setup: ethers.BytesLike, - saltNonce?: string - ): Promise
{ + async addressForSetup(setup: Hex, saltNonce?: string): Promise
{ // bytes32 salt = keccak256(abi.encodePacked(keccak256(initializer), saltNonce)); // cf: https://github.com/safe-global/safe-smart-account/blob/499b17ad0191b575fcadc5cb5b8e3faeae5391ae/contracts/proxies/SafeProxyFactory.sol#L58 - const salt = ethers.keccak256( - ethers.solidityPacked( + const salt = keccak256( + encodePacked( ["bytes32", "uint256"], - [ethers.keccak256(setup), saltNonce || 0] + [keccak256(setup), BigInt(saltNonce || "0")] ) ); // abi.encodePacked(type(SafeProxy).creationCode, uint256(uint160(_singleton))); // cf: https://github.com/safe-global/safe-smart-account/blob/499b17ad0191b575fcadc5cb5b8e3faeae5391ae/contracts/proxies/SafeProxyFactory.sol#L29 - const initCode = ethers.solidityPacked( + const initCode = encodePacked( ["bytes", "uint256"], [ - await this.proxyFactory.proxyCreationCode(), - await this.singleton.getAddress(), + (await this.dummyClient.readContract({ + address: this.proxyFactory.address, + abi: this.proxyFactory.abi, + functionName: "proxyCreationCode", + })) as Hex, + BigInt(this.singleton.address), ] ); - return ethers.getCreate2Address( - await this.proxyFactory.getAddress(), + return getCreate2Address({ + from: this.proxyFactory.address, salt, - ethers.keccak256(initCode) - ) as Address; + bytecodeHash: keccak256(initCode), + }); } - async getSetup(owners: string[]): Promise { - const setup = await this.singleton.interface.encodeFunctionData("setup", [ - owners, - 1, // We use sign threshold of 1. - this.moduleSetup.target, - this.moduleSetup.interface.encodeFunctionData("enableModules", [ - [this.m4337.target], - ]), - this.m4337.target, - ethers.ZeroAddress, - 0, - ethers.ZeroAddress, - ]); - return setup as Hex; + getSetup(owners: string[]): Hex { + return encodeFunctionData({ + abi: this.singleton.abi, + functionName: "setup", + args: [ + owners, + 1, // We use sign threshold of 1. + this.moduleSetup.address, + encodeFunctionData({ + abi: this.moduleSetup.abi, + functionName: "enableModules", + args: [[this.m4337.address]], + }), + this.m4337.address, + zeroAddress, + 0, + zeroAddress, + ], + }); + } + + addOwnerData(newOwner: Address): Hex { + return encodeFunctionData({ + abi: this.singleton.abi, + functionName: "addOwnerWithThreshold", + args: [newOwner, 1], + }); } async getOpHash(unsignedUserOp: UserOperation): Promise { @@ -140,35 +180,45 @@ export class ContractSuite { maxPriorityFeePerGas, maxFeePerGas, } = unsignedUserOp; - return this.m4337.getOperationHash({ - ...unsignedUserOp, - initCode: factory - ? ethers.solidityPacked(["address", "bytes"], [factory, factoryData]) - : "0x", - accountGasLimits: packGas(verificationGasLimit, callGasLimit), - gasFees: packGas(maxPriorityFeePerGas, maxFeePerGas), - paymasterAndData: packPaymasterData(unsignedUserOp), - signature: PLACEHOLDER_SIG, + const opHash = await this.dummyClient.readContract({ + address: this.m4337.address, + abi: this.m4337.abi, + functionName: "getOperationHash", + args: [ + { + ...unsignedUserOp, + initCode: factory + ? encodePacked(["address", "bytes"], [factory, factoryData!]) + : "0x", + accountGasLimits: packGas(verificationGasLimit, callGasLimit), + gasFees: packGas(maxPriorityFeePerGas, maxFeePerGas), + paymasterAndData: packPaymasterData(unsignedUserOp), + signature: PLACEHOLDER_SIG, + }, + ], }); + return opHash as Hash; } - factoryDataForSetup( + private factoryDataForSetup( safeNotDeployed: boolean, setup: string, safeSaltNonce: string ): { factory?: Address; factoryData?: Hex } { return safeNotDeployed ? { - factory: this.proxyFactory.target as Address, - factoryData: this.proxyFactory.interface.encodeFunctionData( - "createProxyWithNonce", - [this.singleton.target, setup, safeSaltNonce] - ) as Hex, + factory: this.proxyFactory.address, + factoryData: encodeFunctionData({ + abi: this.proxyFactory.abi, + functionName: "createProxyWithNonce", + args: [this.singleton.address, setup, safeSaltNonce], + }), } : {}; } async buildUserOp( + nonce: bigint, txData: MetaTransaction, safeAddress: Address, feeData: GasPrice, @@ -176,20 +226,33 @@ export class ContractSuite { safeNotDeployed: boolean, safeSaltNonce: string ): Promise { - const rawUserOp = { + return { sender: safeAddress, - nonce: ethers.toBeHex(await this.entryPoint.getNonce(safeAddress, 0)), + nonce: toHex(nonce), ...this.factoryDataForSetup(safeNotDeployed, setup, safeSaltNonce), // - callData: this.m4337.interface.encodeFunctionData("executeUserOp", [ - txData.to, - BigInt(txData.value), - txData.data, - txData.operation || 0, - ]) as Hex, + callData: encodeFunctionData({ + abi: this.m4337.abi, + functionName: "executeUserOp", + args: [ + txData.to, + BigInt(txData.value), + txData.data, + txData.operation || 0, + ], + }), ...feeData, }; - return rawUserOp; + } + + async getNonce(address: Address, chainId: number): Promise { + const nonce = (await getClient(chainId).readContract({ + abi: this.entryPoint.abi, + address: this.entryPoint.address, + functionName: "getNonce", + args: [address, 0], + })) as bigint; + return nonce; } } @@ -198,31 +261,19 @@ type DeploymentFunction = (filter?: { }) => | { networkAddresses: { [chainId: string]: string }; abi: unknown[] } | undefined; -type DeploymentArgs = { provider: ethers.JsonRpcProvider; version: string }; +type DeploymentArgs = { version: string }; async function getDeployment( fn: DeploymentFunction, - { provider, version }: DeploymentArgs -): Promise { - const { chainId } = await provider.getNetwork(); + { version }: DeploymentArgs +): Promise { const deployment = fn({ version }); if (!deployment) { - throw new Error( - `Deployment not found for ${fn.name} version ${version} on chainId ${chainId}` - ); - } - let address = deployment.networkAddresses[`${chainId}`]; - if (!address) { - // console.warn( - // `Deployment asset ${fn.name} not listed on chainId ${chainId}, using likely fallback. For more info visit https://github.com/safe-global/safe-modules-deployments` - // ); - // TODO: This is a cheeky hack. Real solution proposed in - // https://github.com/Mintbase/near-safe/issues/42 - address = deployment.networkAddresses["11155111"]; + throw new Error(`Deployment not found for ${fn.name} version ${version}`); } - return new ethers.Contract( - address, - deployment.abi as ethers.Fragment[], - provider - ); + // TODO: maybe call parseAbi on deployment.abi here. + return { + address: deployment.networkAddresses["11155111"] as Address, + abi: deployment.abi, + }; } diff --git a/src/tx-manager.ts b/src/tx-manager.ts index 1685247..a28a439 100644 --- a/src/tx-manager.ts +++ b/src/tx-manager.ts @@ -3,7 +3,6 @@ import { NearEthAdapter, NearEthTxData, BaseTx, - Network, setupAdapter, signatureFromOutcome, } from "near-ca"; @@ -13,12 +12,11 @@ import { Erc4337Bundler } from "./lib/bundler"; import { encodeMulti } from "./lib/multisend"; import { ContractSuite } from "./lib/safe"; import { MetaTransaction, UserOperation, UserOperationReceipt } from "./types"; -import { isContract, packSignature } from "./util"; +import { getClient, isContract, packSignature } from "./util"; export class TransactionManager { readonly nearAdapter: NearEthAdapter; readonly address: Address; - readonly entryPointAddress: Address; private safePack: ContractSuite; private setup: string; @@ -32,13 +30,11 @@ export class TransactionManager { pimlicoKey: string, setup: string, safeAddress: Address, - entryPointAddress: Address, safeSaltNonce: string ) { this.nearAdapter = nearAdapter; this.safePack = safePack; this.pimlicoKey = pimlicoKey; - this.entryPointAddress = entryPointAddress; this.setup = setup; this.address = safeAddress; this.safeSaltNonce = safeSaltNonce; @@ -60,13 +56,11 @@ export class TransactionManager { console.log( `Near Adapter: ${nearAdapter.nearAccountId()} <> ${nearAdapter.address}` ); - const setup = await safePack.getSetup([nearAdapter.address]); + const setup = safePack.getSetup([nearAdapter.address]); const safeAddress = await safePack.addressForSetup( setup, config.safeSaltNonce ); - const entryPointAddress = - (await safePack.entryPoint.getAddress()) as Address; console.log(`Safe Address: ${safeAddress}`); return new TransactionManager( nearAdapter, @@ -74,7 +68,6 @@ export class TransactionManager { pimlicoKey, setup, safeAddress, - entryPointAddress, config.safeSaltNonce || "0" ); } @@ -84,12 +77,15 @@ export class TransactionManager { } async getBalance(chainId: number): Promise { - const provider = Network.fromChainId(chainId).client; - return await provider.getBalance({ address: this.address }); + return await getClient(chainId).getBalance({ address: this.address }); } bundlerForChainId(chainId: number): Erc4337Bundler { - return new Erc4337Bundler(this.entryPointAddress, this.pimlicoKey, chainId); + return new Erc4337Bundler( + this.safePack.entryPoint.address, + this.pimlicoKey, + chainId + ); } async buildTransaction(args: { @@ -98,28 +94,33 @@ export class TransactionManager { usePaymaster: boolean; }): Promise { const { transactions, usePaymaster, chainId } = args; - const bundler = this.bundlerForChainId(chainId); - const gasFees = (await bundler.getGasPrice()).fast; - // Build Singular MetaTransaction for Multisend from transaction list. if (transactions.length === 0) { throw new Error("Empty transaction set!"); } + const bundler = this.bundlerForChainId(chainId); + const [gasFees, nonce, safeDeployed] = await Promise.all([ + bundler.getGasPrice(), + this.safePack.getNonce(this.address, chainId), + this.safeDeployed(chainId), + ]); + // Build Singular MetaTransaction for Multisend from transaction list. const tx = transactions.length > 1 ? encodeMulti(transactions) : transactions[0]!; - const safeNotDeployed = !(await this.safeDeployed(chainId)); + const rawUserOp = await this.safePack.buildUserOp( + nonce, tx, this.address, - gasFees, + gasFees.fast, this.setup, - safeNotDeployed, + !safeDeployed, this.safeSaltNonce ); const paymasterData = await bundler.getPaymasterData( rawUserOp, usePaymaster, - safeNotDeployed + !safeDeployed ); const unsignedUserOp = { ...rawUserOp, ...paymasterData }; @@ -188,14 +189,11 @@ export class TransactionManager { return deployed; } - addOwnerTx(address: string): MetaTransaction { + addOwnerTx(address: Address): MetaTransaction { return { to: this.address, value: "0", - data: this.safePack.singleton.interface.encodeFunctionData( - "addOwnerWithThreshold", - [address, 1] - ), + data: this.safePack.addOwnerData(address), }; } diff --git a/src/util.ts b/src/util.ts index 62dfa9f..2a0055c 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,5 +1,12 @@ import { Network } from "near-ca"; -import { Address, Hex, concatHex, encodePacked, toHex } from "viem"; +import { + Address, + Hex, + concatHex, + encodePacked, + toHex, + PublicClient, +} from "viem"; import { PaymasterData, MetaTransaction } from "./types"; @@ -42,6 +49,9 @@ export async function isContract( address: Address, chainId: number ): Promise { - const client = Network.fromChainId(chainId).client; - return !!(await client.getCode({ address })); + return (await getClient(chainId).getCode({ address })) !== undefined; +} + +export function getClient(chainId: number): PublicClient { + return Network.fromChainId(chainId).client; } diff --git a/tests/ethers-safe.ts b/tests/ethers-safe.ts new file mode 100644 index 0000000..3e37b94 --- /dev/null +++ b/tests/ethers-safe.ts @@ -0,0 +1,234 @@ +import { + getProxyFactoryDeployment, + getSafeL2SingletonDeployment, +} from "@safe-global/safe-deployments"; +import { + getSafe4337ModuleDeployment, + getSafeModuleSetupDeployment, +} from "@safe-global/safe-modules-deployments"; +import { ethers } from "ethers"; +import { Address, Hash, Hex } from "viem"; + +import { + GasPrice, + MetaTransaction, + UnsignedUserOperation, + UserOperation, +} from "../src/types"; +import { PLACEHOLDER_SIG, packGas, packPaymasterData } from "../src/util"; + +/** + * All contracts used in account creation & execution + */ +export class ContractSuite { + provider: ethers.JsonRpcProvider; + singleton: ethers.Contract; + proxyFactory: ethers.Contract; + m4337: ethers.Contract; + moduleSetup: ethers.Contract; + entryPoint: ethers.Contract; + + constructor( + provider: ethers.JsonRpcProvider, + singleton: ethers.Contract, + proxyFactory: ethers.Contract, + m4337: ethers.Contract, + moduleSetup: ethers.Contract, + entryPoint: ethers.Contract + ) { + this.provider = provider; + this.singleton = singleton; + this.proxyFactory = proxyFactory; + this.m4337 = m4337; + this.moduleSetup = moduleSetup; + this.entryPoint = entryPoint; + } + + static async init(): Promise { + // TODO - this is a cheeky hack. + const provider = new ethers.JsonRpcProvider("https://rpc2.sepolia.org"); + const safeDeployment = (fn: DeploymentFunction): Promise => + getDeployment(fn, { provider, version: "1.4.1" }); + const m4337Deployment = async ( + fn: DeploymentFunction + ): Promise => { + return getDeployment(fn, { provider, version: "0.3.0" }); + }; + // Need this first to get entryPoint address + const m4337 = await m4337Deployment(getSafe4337ModuleDeployment); + + const [singleton, proxyFactory, moduleSetup, supportedEntryPoint] = + await Promise.all([ + safeDeployment(getSafeL2SingletonDeployment), + safeDeployment(getProxyFactoryDeployment), + m4337Deployment(getSafeModuleSetupDeployment), + m4337.SUPPORTED_ENTRYPOINT(), + ]); + const entryPoint = new ethers.Contract( + supportedEntryPoint, + ["function getNonce(address, uint192 key) view returns (uint256 nonce)"], + provider + ); + console.log("Initialized ERC4337 & Safe Module Contracts:", { + singleton: await singleton.getAddress(), + proxyFactory: await proxyFactory.getAddress(), + m4337: await m4337.getAddress(), + moduleSetup: await moduleSetup.getAddress(), + entryPoint: await entryPoint.getAddress(), + }); + return new ContractSuite( + provider, + singleton, + proxyFactory, + m4337, + moduleSetup, + entryPoint + ); + } + + async addressForSetup( + setup: ethers.BytesLike, + saltNonce?: string + ): Promise
{ + // bytes32 salt = keccak256(abi.encodePacked(keccak256(initializer), saltNonce)); + // cf: https://github.com/safe-global/safe-smart-account/blob/499b17ad0191b575fcadc5cb5b8e3faeae5391ae/contracts/proxies/SafeProxyFactory.sol#L58 + const salt = ethers.keccak256( + ethers.solidityPacked( + ["bytes32", "uint256"], + [ethers.keccak256(setup), saltNonce || 0] + ) + ); + + // abi.encodePacked(type(SafeProxy).creationCode, uint256(uint160(_singleton))); + // cf: https://github.com/safe-global/safe-smart-account/blob/499b17ad0191b575fcadc5cb5b8e3faeae5391ae/contracts/proxies/SafeProxyFactory.sol#L29 + const initCode = ethers.solidityPacked( + ["bytes", "uint256"], + [ + await this.proxyFactory.proxyCreationCode(), + await this.singleton.getAddress(), + ] + ); + return ethers.getCreate2Address( + await this.proxyFactory.getAddress(), + salt, + ethers.keccak256(initCode) + ) as Address; + } + + getSetup(owners: string[]): Hex { + return this.singleton.interface.encodeFunctionData("setup", [ + owners, + 1, // We use sign threshold of 1. + this.moduleSetup.target, + this.moduleSetup.interface.encodeFunctionData("enableModules", [ + [this.m4337.target], + ]), + this.m4337.target, + ethers.ZeroAddress, + 0, + ethers.ZeroAddress, + ]) as Hex; + } + + addOwnerData(newOwner: Address): Hex { + return this.singleton.interface.encodeFunctionData( + "addOwnerWithThreshold", + [newOwner, 1] + ) as Hex; + } + + async getOpHash(unsignedUserOp: UserOperation): Promise { + const { + factory, + factoryData, + verificationGasLimit, + callGasLimit, + maxPriorityFeePerGas, + maxFeePerGas, + } = unsignedUserOp; + return this.m4337.getOperationHash({ + ...unsignedUserOp, + initCode: factory + ? ethers.solidityPacked(["address", "bytes"], [factory, factoryData]) + : "0x", + accountGasLimits: packGas(verificationGasLimit, callGasLimit), + gasFees: packGas(maxPriorityFeePerGas, maxFeePerGas), + paymasterAndData: packPaymasterData(unsignedUserOp), + signature: PLACEHOLDER_SIG, + }); + } + + factoryDataForSetup( + safeNotDeployed: boolean, + setup: string, + safeSaltNonce: string + ): { factory?: Address; factoryData?: Hex } { + return safeNotDeployed + ? { + factory: this.proxyFactory.target as Address, + factoryData: this.proxyFactory.interface.encodeFunctionData( + "createProxyWithNonce", + [this.singleton.target, setup, safeSaltNonce] + ) as Hex, + } + : {}; + } + + async buildUserOp( + txData: MetaTransaction, + safeAddress: Address, + feeData: GasPrice, + setup: string, + safeNotDeployed: boolean, + safeSaltNonce: string + ): Promise { + const rawUserOp = { + sender: safeAddress, + nonce: ethers.toBeHex(await this.entryPoint.getNonce(safeAddress, 0)), + ...this.factoryDataForSetup(safeNotDeployed, setup, safeSaltNonce), + // + callData: this.m4337.interface.encodeFunctionData("executeUserOp", [ + txData.to, + BigInt(txData.value), + txData.data, + txData.operation || 0, + ]) as Hex, + ...feeData, + }; + return rawUserOp; + } +} + +type DeploymentFunction = (filter?: { + version: string; +}) => + | { networkAddresses: { [chainId: string]: string }; abi: unknown[] } + | undefined; +type DeploymentArgs = { provider: ethers.JsonRpcProvider; version: string }; + +async function getDeployment( + fn: DeploymentFunction, + { provider, version }: DeploymentArgs +): Promise { + const { chainId } = await provider.getNetwork(); + const deployment = fn({ version }); + if (!deployment) { + throw new Error( + `Deployment not found for ${fn.name} version ${version} on chainId ${chainId}` + ); + } + let address = deployment.networkAddresses[`${chainId}`]; + if (!address) { + // console.warn( + // `Deployment asset ${fn.name} not listed on chainId ${chainId}, using likely fallback. For more info visit https://github.com/safe-global/safe-modules-deployments` + // ); + // TODO: This is a cheeky hack. Real solution proposed in + // https://github.com/Mintbase/near-safe/issues/42 + address = deployment.networkAddresses["11155111"]; + } + return new ethers.Contract( + address, + deployment.abi as ethers.Fragment[], + provider + ); +} diff --git a/tests/lib.safe.spec.ts b/tests/lib.safe.spec.ts new file mode 100644 index 0000000..0650543 --- /dev/null +++ b/tests/lib.safe.spec.ts @@ -0,0 +1,68 @@ +import { zeroAddress } from "viem"; + +import { ContractSuite as EthPack } from "./ethers-safe"; +import { ContractSuite as ViemPack } from "../src/lib/safe"; + +describe("Safe Pack", () => { + let ethersPack: EthPack; + let viemPack: ViemPack; + beforeAll(async () => { + ethersPack = await EthPack.init(); + viemPack = await ViemPack.init(); + }); + + it("init", async () => { + expect(ethersPack.singleton.target).toEqual(viemPack.singleton.address); + expect(await ethersPack.singleton.getAddress()).toEqual( + viemPack.singleton.address + ); + + expect(ethersPack.m4337.target).toEqual(viemPack.m4337.address); + expect(await ethersPack.m4337.getAddress()).toEqual(viemPack.m4337.address); + + expect(ethersPack.moduleSetup.target).toEqual(viemPack.moduleSetup.address); + expect(await ethersPack.moduleSetup.getAddress()).toEqual( + viemPack.moduleSetup.address + ); + + expect(ethersPack.moduleSetup.target).toEqual(viemPack.moduleSetup.address); + expect(await ethersPack.moduleSetup.getAddress()).toEqual( + viemPack.moduleSetup.address + ); + + expect(ethersPack.entryPoint.target).toEqual(viemPack.entryPoint.address); + expect(await ethersPack.entryPoint.getAddress()).toEqual( + viemPack.entryPoint.address + ); + + expect(ethersPack.proxyFactory.target).toEqual( + viemPack.proxyFactory.address + ); + expect(await ethersPack.proxyFactory.getAddress()).toEqual( + viemPack.proxyFactory.address + ); + }); + + it("addOwnerData", () => { + expect(ethersPack.addOwnerData(zeroAddress)).toEqual( + viemPack.addOwnerData(zeroAddress) + ); + }); + it("getSetup", () => { + expect(ethersPack.getSetup([zeroAddress])).toEqual( + viemPack.getSetup([zeroAddress]) + ); + }); + + it("getSetup", async () => { + const setup = viemPack.getSetup([zeroAddress]); + const [eps0, vps0, eps1, vps1] = await Promise.all([ + ethersPack.addressForSetup(setup), + viemPack.addressForSetup(setup), + ethersPack.addressForSetup(setup, "1"), + viemPack.addressForSetup(setup, "1"), + ]); + expect(eps0).toEqual(vps0); + expect(eps1).toEqual(vps1); + }); +}); diff --git a/tests/utils.spec.ts b/tests/utils.spec.ts index 3a7c91d..11a089c 100644 --- a/tests/utils.spec.ts +++ b/tests/utils.spec.ts @@ -68,11 +68,11 @@ describe("Utility Functions (mostly byte packing)", () => { expect(containsValue([VALUE_TX, NO_VALUE_TX])).toBe(true); }); - it("isContract", () => { + it("isContract", async () => { const chainId = 11155111; - expect(isContract(zeroAddress, chainId)).toBe(false); + expect(await isContract(zeroAddress, chainId)).toBe(false); expect( - isContract("0x9008D19f58AAbD9eD0D60971565AA8510560ab41", chainId) + await isContract("0x9008D19f58AAbD9eD0D60971565AA8510560ab41", chainId) ).toBe(true); }); });