From 531ed106d90eeacc2914b17d797737df720656ea Mon Sep 17 00:00:00 2001 From: adnpark Date: Thu, 28 Nov 2024 22:09:01 +0900 Subject: [PATCH 1/4] feat: add sendUserOperations --- .../v0.7/multiChainECDSAValidator.test.ts | 162 ++++---- packages/test/v0.7/utils/common.ts | 2 +- .../actions/sendUserOperations.ts | 345 ++++++++++++++++ .../actions/sendUserOperations.ts | 376 ++++++++++++++++++ plugins/multi-chain-web-authn/index.ts | 5 + 5 files changed, 813 insertions(+), 77 deletions(-) create mode 100644 plugins/multi-chain-ecdsa/actions/sendUserOperations.ts create mode 100644 plugins/multi-chain-web-authn/actions/sendUserOperations.ts diff --git a/packages/test/v0.7/multiChainECDSAValidator.test.ts b/packages/test/v0.7/multiChainECDSAValidator.test.ts index bfd709b7..8b6e255a 100644 --- a/packages/test/v0.7/multiChainECDSAValidator.test.ts +++ b/packages/test/v0.7/multiChainECDSAValidator.test.ts @@ -6,6 +6,11 @@ import { signUserOperations } from "@zerodev/multi-chain-ecdsa-validator" import { toMultiChainECDSAValidator } from "@zerodev/multi-chain-ecdsa-validator" +import { + type ClientWithChainId, + type SendUserOperationsParameters, + sendUserOperations +} from "@zerodev/multi-chain-ecdsa-validator/actions/sendUserOperations.js" import { EIP1271Abi, type KernelAccountClient, @@ -94,7 +99,6 @@ const SEPOLIA_ZERODEV_RPC_URL = getBundlerRpc( const SEPOLIA_ZERODEV_PAYMASTER_RPC_URL = getPaymasterRpc( config["0.7"][sepolia.id].projectId ) - const OPTIMISM_SEPOLIA_ZERODEV_RPC_URL = getBundlerRpc( config["0.7"][optimismSepolia.id].projectId ) @@ -812,60 +816,60 @@ describe("MultiChainECDSAValidator", () => { paymaster: opSepoliaZeroDevPaymasterClient }) - const sepoliaUserOp = - await sepoliaZerodevKernelClient.prepareUserOperation({ - callData: - await sepoliaZerodevKernelClient.account.encodeCalls([ + const clientsWithChainId: ClientWithChainId< + Transport, + Chain, + SmartAccount + >[] = [ + { + ...sepoliaZerodevKernelClient, + chainId: sepolia.id + }, + { + ...optimismSepoliaZerodevKernelClient, + chainId: optimismSepolia.id + } + ] + + const userOps = await Promise.all( + clientsWithChainId.map(async (client) => { + return { + callData: await client.account.encodeCalls([ { to: zeroAddress, value: BigInt(0), data: "0x" } ]) + } }) + ) - const optimismSepoliaUserOp = - await optimismSepoliaZerodevKernelClient.prepareUserOperation({ - callData: - await optimismSepoliaZerodevKernelClient.account.encodeCalls( - [ - { - to: zeroAddress, - value: BigInt(0), - data: "0x" - } - ] - ) - }) - - const signedUserOps = await signUserOperations( - sepoliaZerodevKernelClient, + const userOpParams: SendUserOperationsParameters[] = [ { - userOperations: [ - { ...sepoliaUserOp, chainId: sepolia.id }, - { - ...optimismSepoliaUserOp, - chainId: optimismSepolia.id - } - ] + ...userOps[0], + chainId: sepolia.id + }, + { + ...userOps[1], + chainId: optimismSepolia.id } + ] + + const userOpHashes = await sendUserOperations( + clientsWithChainId, + userOpParams ) - const sepoliaUserOpHash = - await sepoliaZerodevKernelClient.sendUserOperation({ - ...signedUserOps[0] - }) + console.log("userOpHashes", userOpHashes) + const sepoliaUserOpHash = userOpHashes[0] + const optimismSepoliaUserOpHash = userOpHashes[1] console.log("sepoliaUserOpHash", sepoliaUserOpHash) await sepoliaZerodevKernelClient.waitForUserOperationReceipt({ hash: sepoliaUserOpHash }) - const optimismSepoliaUserOpHash = - await optimismSepoliaZerodevKernelClient.sendUserOperation({ - ...signedUserOps[1] - }) - console.log("optimismSepoliaUserOpHash", optimismSepoliaUserOpHash) await optimismSepoliaZerodevKernelClient.waitForUserOperationReceipt( { @@ -963,55 +967,61 @@ describe("MultiChainECDSAValidator", () => { paymaster: opSepoliaZeroDevPaymasterClient }) - const sepoliaUserOp = - await sepoliaZerodevKernelClient.prepareUserOperation({ - callData: await sepoliaKernelAccount.encodeCalls([ - { - to: zeroAddress, - value: BigInt(0), - data: "0x" - } - ]) - }) + const clientsWithChainId: ClientWithChainId< + Transport, + Chain, + SmartAccount + >[] = [ + { + ...sepoliaZerodevKernelClient, + chainId: sepolia.id + }, + { + ...optimismSepoliaZerodevKernelClient, + chainId: optimismSepolia.id + } + ] - const optimismSepoliaUserOp = - await optimismSepoliaZerodevKernelClient.prepareUserOperation({ - callData: await optimismSepoliaKernelAccount.encodeCalls([ - { - to: zeroAddress, - value: BigInt(0), - data: "0x" - } - ]) + const userOps = await Promise.all( + clientsWithChainId.map(async (client) => { + return { + callData: await client.account.encodeCalls([ + { + to: zeroAddress, + value: BigInt(0), + data: "0x" + } + ]) + } }) + ) - const signedEnableUserOps = await ecdsaSignUserOpsWithEnable({ - multiChainUserOpConfigsForEnable: [ - { - account: sepoliaKernelAccount, - userOp: sepoliaUserOp - }, - { - account: optimismSepoliaKernelAccount, - userOp: optimismSepoliaUserOp - } - ] - }) + const userOpParams: SendUserOperationsParameters[] = [ + { + ...userOps[0], + chainId: sepolia.id + }, + { + ...userOps[1], + chainId: optimismSepolia.id + } + ] - const sepoliaUserOpHash = - await sepoliaZerodevKernelClient.sendUserOperation({ - ...signedEnableUserOps[0] - }) + const userOpHashes = await sendUserOperations( + clientsWithChainId, + userOpParams + ) + + console.log("userOpHashes", userOpHashes) + + const sepoliaUserOpHash = userOpHashes[0] console.log("sepoliaUserOpHash", sepoliaUserOpHash) await sepoliaZerodevKernelClient.waitForUserOperationReceipt({ hash: sepoliaUserOpHash }) - const optimismSepoliaUserOpHash = - await optimismSepoliaZerodevKernelClient.sendUserOperation({ - ...signedEnableUserOps[1] - }) + const optimismSepoliaUserOpHash = userOpHashes[1] console.log("optimismSepoliaUserOpHash", optimismSepoliaUserOpHash) await optimismSepoliaZerodevKernelClient.waitForUserOperationReceipt( diff --git a/packages/test/v0.7/utils/common.ts b/packages/test/v0.7/utils/common.ts index 35fc0bca..4b12366b 100644 --- a/packages/test/v0.7/utils/common.ts +++ b/packages/test/v0.7/utils/common.ts @@ -27,7 +27,7 @@ export const Test_ERC20Address = "0x3870419Ba2BBf0127060bCB37f69A1b1C090992B" const testingChain = allChains.sepolia.id export const kernelVersion = "0.3.1" export const index = 11111111111111111n // 432334375434333332434365532464445487823332432423423n -const DEFAULT_PROVIDER = "ALCHEMY" +const DEFAULT_PROVIDER = "PIMLICO" const projectId = config["0.7"][testingChain].projectId export const getEntryPoint = (): { diff --git a/plugins/multi-chain-ecdsa/actions/sendUserOperations.ts b/plugins/multi-chain-ecdsa/actions/sendUserOperations.ts new file mode 100644 index 00000000..437abfb7 --- /dev/null +++ b/plugins/multi-chain-ecdsa/actions/sendUserOperations.ts @@ -0,0 +1,345 @@ +import { + AccountNotFoundError, + type Action, + type KernelSmartAccountImplementation, + KernelV3AccountAbi, + getEncodedPluginsData, + isPluginInitialized +} from "@zerodev/sdk" +import { MerkleTree } from "merkletreejs" +import { + type Chain, + type Client, + type Hash, + type Hex, + type Transport, + concatHex, + encodeAbiParameters, + getAbiItem, + hashTypedData, + keccak256, + toFunctionSelector, + zeroAddress +} from "viem" +import { + type PrepareUserOperationParameters, + type SendUserOperationParameters, + type SmartAccount, + type UserOperation, + getUserOperationHash, + prepareUserOperation, + sendUserOperation +} from "viem/account-abstraction" +import { parseAccount } from "viem/accounts" +import { getAction } from "viem/utils" + +export type ClientWithChainId< + TTransport extends Transport, + TChain extends Chain | undefined, + TAccount extends SmartAccount | undefined = undefined +> = Client & { + chainId: number +} + +export type SendUserOperationsParameters< + account extends SmartAccount | undefined = SmartAccount | undefined, + accountOverride extends SmartAccount | undefined = SmartAccount | undefined, + calls extends readonly unknown[] = readonly unknown[] +> = SendUserOperationParameters & { + chainId: number +} + +export async function sendUserOperations< + account extends SmartAccount | undefined, + chain extends Chain | undefined, + accountOverride extends SmartAccount | undefined = undefined, + calls extends readonly unknown[] = readonly unknown[] +>( + clients: ClientWithChainId[], + args_: SendUserOperationsParameters[] +): Promise { + if (clients.length < 2 && args_.length < 2) { + throw new Error("Should send more than 1 user operation") + } + if (clients.length !== args_.length) { + throw new Error("Number of clients and user operations do not match") + } + for (let i = 0; i < clients.length; i++) { + if (clients[i].chainId !== args_[i].chainId) { + throw new Error( + `Chain ID mismatch at index ${i}: client.chainId (${clients[i].chainId}) !== args_.chainId (${args_[i].chainId})` + ) + } + } + + const args = args_ as SendUserOperationsParameters[] + const accounts_ = args.map( + (arg, index) => arg.account ?? clients[index].account + ) + if ( + !accounts_.every( + (account): account is SmartAccount => account !== undefined + ) + ) { + throw new AccountNotFoundError() + } + + const accounts = accounts_.map( + (account) => + parseAccount( + account + ) as SmartAccount + ) + + const _userOperations = args.map(({ chainId, ...userOp }) => userOp) + + const action: Action = { + selector: toFunctionSelector( + getAbiItem({ abi: KernelV3AccountAbi, name: "execute" }) + ), + address: zeroAddress + } + + const account = accounts[0] + + // check if regular validator exists + if (account.kernelPluginManager.regularValidator) { + const isPluginEnabledPerChains = await Promise.all( + accounts.map( + async (account, index) => + (await account.kernelPluginManager.isEnabled( + account.address, + action.selector + )) || + (await isPluginInitialized( + clients[index], + account.address, + account.kernelPluginManager.address + )) + ) + ) + + const allEnabled = isPluginEnabledPerChains.every((enabled) => enabled) + const noneEnabled = isPluginEnabledPerChains.every( + (enabled) => !enabled + ) + + if (!allEnabled && !noneEnabled) { + throw new Error( + "Plugins must be either all enabled or all disabled across chains." + ) + } + // if regular validators are not enabled, encode with enable signatures + if (noneEnabled) { + const dummySignatures = await Promise.all( + accounts.map(async (account, index) => { + return account.kernelPluginManager.regularValidator?.getStubSignature( + _userOperations[index] as UserOperation + ) + }) + ) + + for (const signature of dummySignatures) { + if (signature === undefined) { + throw new Error("Dummy signatures are undefined") + } + } + + const pluginEnableTypedDatas = await Promise.all( + accounts.map(async (account) => { + return account.kernelPluginManager.getPluginsEnableTypedData( + account.address + ) + }) + ) + + const leaves = pluginEnableTypedDatas.map((typedData) => { + return hashTypedData(typedData) + }) + + const merkleTree = new MerkleTree(leaves, keccak256, { + sortPairs: true + }) + + const merkleRoot = merkleTree.getHexRoot() as Hex + + const ecdsaSig = + await account.kernelPluginManager.sudoValidator?.signMessage({ + message: { + raw: merkleRoot + } + }) + + if (!ecdsaSig) { + throw new Error( + "No ecdsaSig, check if the sudo validator is multi-chain-ecdsa-validator" + ) + } + + const enableSigs = accounts.map((_, index) => { + const merkleProof = merkleTree.getHexProof( + leaves[index] + ) as Hex[] + const encodedMerkleProof = encodeAbiParameters( + [{ name: "proof", type: "bytes32[]" }], + [merkleProof] + ) + return concatHex([ecdsaSig, merkleRoot, encodedMerkleProof]) + }) + + const encodedDummySignatures = await Promise.all( + accounts.map(async (account, index) => { + return getEncodedPluginsData({ + enableSignature: enableSigs[index], + userOpSignature: dummySignatures[index] as Hex, + action, + enableData: + await account.kernelPluginManager.getEnableData( + account.address + ) + }) + }) + ) + + for (const userOperation of _userOperations) { + userOperation.signature = encodedDummySignatures[index] + } + + const userOperations = await Promise.all( + _userOperations.map(async (_userOperation, index) => { + return await getAction( + clients[index], + prepareUserOperation, + "prepareUserOperation" + )(_userOperation as PrepareUserOperationParameters) + }) + ) + + const encodedSignatures = await Promise.all( + userOperations.map(async (userOperation, index) => { + return await getEncodedPluginsData({ + enableSignature: enableSigs[index], + userOpSignature: await accounts[ + index + ].kernelPluginManager.signUserOperationWithActiveValidator( + userOperation as UserOperation + ), + action, + enableData: await accounts[ + index + ].kernelPluginManager.getEnableData(account.address) + }) + }) + ) + + userOperations.forEach((userOperation, index) => { + userOperation.signature = encodedSignatures[index] + }) + + return await Promise.all( + userOperations.map(async (userOperation, index) => { + return await getAction( + clients[index], + sendUserOperation, + "sendUserOperation" + )(userOperation) + }) + ) + } + // if regular validators are enabled, use signUserOperationWithActiveValidator directly + if (allEnabled) { + const userOperations = await Promise.all( + _userOperations.map(async (_userOperation, index) => { + return await getAction( + clients[index], + prepareUserOperation, + "prepareUserOperation" + )(_userOperation as PrepareUserOperationParameters) + }) + ) + + const signatures = await Promise.all( + userOperations.map((userOperation, index) => + accounts[ + index + ].kernelPluginManager.signUserOperationWithActiveValidator( + userOperation as UserOperation + ) + ) + ) + + userOperations.forEach((userOperation, index) => { + userOperation.signature = signatures[index] + }) + + return await Promise.all( + userOperations.map(async (userOperation, index) => { + return await getAction( + clients[index], + sendUserOperation, + "sendUserOperation" + )(userOperation) + }) + ) + } + } + // If regular validators do not exist, sign with multi-chain-ecdsa-validator + const userOperations = await Promise.all( + _userOperations.map(async (_userOperation, index) => { + return await getAction( + clients[index], + prepareUserOperation, + "prepareUserOperation" + )(_userOperation as PrepareUserOperationParameters) + }) + ) + + const userOpHashes = userOperations.map((userOp, index) => { + return getUserOperationHash({ + userOperation: { + ...userOp, + signature: "0x" + } as UserOperation, + entryPointAddress: account.entryPoint.address, + entryPointVersion: account.entryPoint.version, + chainId: args_[index].chainId + }) + }) + + const merkleTree = new MerkleTree(userOpHashes, keccak256, { + sortPairs: true + }) + + const merkleRoot = merkleTree.getHexRoot() as Hex + const ecdsaSig = await account.kernelPluginManager.signMessage({ + message: { + raw: merkleRoot + } + }) + + const encodeMerkleDataWithSig = (userOpHash: Hex) => { + const merkleProof = merkleTree.getHexProof(userOpHash) as Hex[] + const encodedMerkleProof = encodeAbiParameters( + [{ name: "proof", type: "bytes32[]" }], + [merkleProof] + ) + return concatHex([ecdsaSig, merkleRoot, encodedMerkleProof]) + } + + const signedMultiUserOps = userOperations.map((userOp, index) => { + return { + ...userOp, + signature: encodeMerkleDataWithSig(userOpHashes[index]) + } + }) + + return await Promise.all( + signedMultiUserOps.map(async (userOp, index) => { + return await getAction( + clients[index], + sendUserOperation, + "sendUserOperation" + )({ ...userOp }) + }) + ) +} diff --git a/plugins/multi-chain-web-authn/actions/sendUserOperations.ts b/plugins/multi-chain-web-authn/actions/sendUserOperations.ts new file mode 100644 index 00000000..1917ebc0 --- /dev/null +++ b/plugins/multi-chain-web-authn/actions/sendUserOperations.ts @@ -0,0 +1,376 @@ +import { + AccountNotFoundError, + type Action, + type KernelSmartAccountImplementation, + KernelV3AccountAbi, + getEncodedPluginsData, + isPluginInitialized +} from "@zerodev/sdk" +import { MerkleTree } from "merkletreejs" +import { + type Chain, + type Client, + type Hash, + type Hex, + type Transport, + concatHex, + encodeAbiParameters, + getAbiItem, + hashMessage, + hashTypedData, + keccak256, + toFunctionSelector, + zeroAddress +} from "viem" +import { + type PrepareUserOperationParameters, + type SendUserOperationParameters, + type SmartAccount, + type UserOperation, + getUserOperationHash, + prepareUserOperation, + sendUserOperation +} from "viem/account-abstraction" +import { parseAccount } from "viem/accounts" +import { getAction } from "viem/utils" + +export type ClientWithChainId< + TTransport extends Transport, + TChain extends Chain | undefined, + TAccount extends SmartAccount | undefined = undefined +> = Client & { + chainId: number +} + +export type SendUserOperationsParameters< + account extends SmartAccount | undefined = SmartAccount | undefined, + accountOverride extends SmartAccount | undefined = SmartAccount | undefined, + calls extends readonly unknown[] = readonly unknown[] +> = SendUserOperationParameters & { + chainId: number +} + +export async function sendUserOperations< + account extends SmartAccount | undefined, + chain extends Chain | undefined, + accountOverride extends SmartAccount | undefined = undefined, + calls extends readonly unknown[] = readonly unknown[] +>( + clients: ClientWithChainId[], + args_: SendUserOperationsParameters[] +): Promise { + if (clients.length < 2 && args_.length < 2) { + throw new Error("Should send more than 1 user operation") + } + if (clients.length !== args_.length) { + throw new Error("Number of clients and user operations do not match") + } + for (let i = 0; i < clients.length; i++) { + if (clients[i].chainId !== args_[i].chainId) { + throw new Error( + `Chain ID mismatch at index ${i}: client.chainId (${clients[i].chainId}) !== args_.chainId (${args_[i].chainId})` + ) + } + } + + const args = args_ as SendUserOperationsParameters[] + const accounts_ = args.map( + (arg, index) => arg.account ?? clients[index].account + ) + if ( + !accounts_.every( + (account): account is SmartAccount => account !== undefined + ) + ) { + throw new AccountNotFoundError() + } + + const accounts = accounts_.map( + (account) => + parseAccount( + account + ) as SmartAccount + ) + + const _userOperations = args.map(({ chainId, ...userOp }) => userOp) + + const action: Action = { + selector: toFunctionSelector( + getAbiItem({ abi: KernelV3AccountAbi, name: "execute" }) + ), + address: zeroAddress + } + + const account = accounts[0] + + // check if regular validator exists + if (account.kernelPluginManager.regularValidator) { + const isPluginEnabledPerChains = await Promise.all( + accounts.map( + async (account, index) => + (await account.kernelPluginManager.isEnabled( + account.address, + action.selector + )) || + (await isPluginInitialized( + clients[index], + account.address, + account.kernelPluginManager.address + )) + ) + ) + + const allEnabled = isPluginEnabledPerChains.every((enabled) => enabled) + const noneEnabled = isPluginEnabledPerChains.every( + (enabled) => !enabled + ) + + if (!allEnabled && !noneEnabled) { + throw new Error( + "Plugins must be either all enabled or all disabled across chains." + ) + } + // if regular validators are not enabled, encode with enable signatures + if (noneEnabled) { + const dummySignatures = await Promise.all( + accounts.map(async (account, index) => { + return account.kernelPluginManager.regularValidator?.getStubSignature( + _userOperations[index] as UserOperation + ) + }) + ) + + for (const signature of dummySignatures) { + if (signature === undefined) { + throw new Error("Dummy signatures are undefined") + } + } + + const pluginEnableTypedDatas = await Promise.all( + accounts.map(async (account) => { + return account.kernelPluginManager.getPluginsEnableTypedData( + account.address + ) + }) + ) + + const leaves = pluginEnableTypedDatas.map((typedData) => { + return hashTypedData(typedData) + }) + + const merkleTree = new MerkleTree(leaves, keccak256, { + sortPairs: true + }) + + const merkleRoot = merkleTree.getHexRoot() as Hex + const toEthSignedMessageHash = hashMessage({ raw: merkleRoot }) + + const passkeySig = + await account.kernelPluginManager.sudoValidator?.signMessage({ + message: { + raw: toEthSignedMessageHash + } + }) + + if (!passkeySig) { + throw new Error( + "No passkeySig, check if the sudo validator is multi-chain-web-authn-validator" + ) + } + + const enableSigs = accounts.map((_, index) => { + const merkleProof = merkleTree.getHexProof( + leaves[index] + ) as Hex[] + const encodedMerkleProof = encodeAbiParameters( + [{ name: "proof", type: "bytes32[]" }], + [merkleProof] + ) + const merkleData = concatHex([merkleRoot, encodedMerkleProof]) + return encodeAbiParameters( + [ + { + name: "merkleData", + type: "bytes" + }, + { + name: "signature", + type: "bytes" + } + ], + [merkleData, passkeySig] + ) + }) + + const encodedDummySignatures = await Promise.all( + accounts.map(async (account, index) => { + return getEncodedPluginsData({ + enableSignature: enableSigs[index], + userOpSignature: dummySignatures[index] as Hex, + action, + enableData: + await account.kernelPluginManager.getEnableData( + account.address + ) + }) + }) + ) + + _userOperations.forEach((userOperation, index) => { + userOperation.signature = encodedDummySignatures[index] + }) + + const userOperations = await Promise.all( + _userOperations.map(async (_userOperation, index) => { + return await getAction( + clients[index], + prepareUserOperation, + "prepareUserOperation" + )(_userOperation as PrepareUserOperationParameters) + }) + ) + + const encodedSignatures = await Promise.all( + userOperations.map(async (userOperation, index) => { + return await getEncodedPluginsData({ + enableSignature: enableSigs[index], + userOpSignature: await accounts[ + index + ].kernelPluginManager.signUserOperationWithActiveValidator( + userOperation as UserOperation + ), + action, + enableData: await accounts[ + index + ].kernelPluginManager.getEnableData(account.address) + }) + }) + ) + + userOperations.forEach((userOperation, index) => { + userOperation.signature = encodedSignatures[index] + }) + + return await Promise.all( + userOperations.map(async (userOperation, index) => { + return await getAction( + clients[index], + sendUserOperation, + "sendUserOperation" + )(userOperation) + }) + ) + } + // if regular validators are enabled, use signUserOperationWithActiveValidator directly + if (allEnabled) { + const userOperations = await Promise.all( + _userOperations.map(async (_userOperation, index) => { + return await getAction( + clients[index], + prepareUserOperation, + "prepareUserOperation" + )(_userOperation as PrepareUserOperationParameters) + }) + ) + + const signatures = await Promise.all( + userOperations.map((userOperation, index) => + accounts[ + index + ].kernelPluginManager.signUserOperationWithActiveValidator( + userOperation as UserOperation + ) + ) + ) + + userOperations.forEach((userOperation, index) => { + userOperation.signature = signatures[index] + }) + + return await Promise.all( + userOperations.map(async (userOperation, index) => { + return await getAction( + clients[index], + sendUserOperation, + "sendUserOperation" + )(userOperation) + }) + ) + } + } + // If regular validators do not exist, sign with multi-chain-ecdsa-validator + const userOperations = await Promise.all( + _userOperations.map(async (_userOperation, index) => { + return await getAction( + clients[index], + prepareUserOperation, + "prepareUserOperation" + )(_userOperation as PrepareUserOperationParameters) + }) + ) + + const userOpHashes = userOperations.map((userOp, index) => { + return getUserOperationHash({ + userOperation: { + ...userOp, + signature: "0x" + } as UserOperation, + entryPointAddress: account.entryPoint.address, + entryPointVersion: account.entryPoint.version, + chainId: args_[index].chainId + }) + }) + + const merkleTree = new MerkleTree(userOpHashes, keccak256, { + sortPairs: true + }) + + const merkleRoot = merkleTree.getHexRoot() as Hex + const toEthSignedMessageHash = hashMessage({ raw: merkleRoot }) + + const passkeySig = await account.kernelPluginManager.signMessage({ + message: { + raw: toEthSignedMessageHash + } + }) + + const encodeMerkleDataWithSig = (userOpHash: Hex) => { + const merkleProof = merkleTree.getHexProof(userOpHash) as Hex[] + + const encodedMerkleProof = encodeAbiParameters( + [{ name: "proof", type: "bytes32[]" }], + [merkleProof] + ) + const merkleData = concatHex([merkleRoot, encodedMerkleProof]) + return encodeAbiParameters( + [ + { + name: "merkleData", + type: "bytes" + }, + { + name: "signature", + type: "bytes" + } + ], + [merkleData, passkeySig] + ) + } + + const signedMultiUserOps = userOperations.map((userOp, index) => { + return { + ...userOp, + signature: encodeMerkleDataWithSig(userOpHashes[index]) + } + }) + + return await Promise.all( + signedMultiUserOps.map(async (userOp, index) => { + return await getAction( + clients[index], + sendUserOperation, + "sendUserOperation" + )({ ...userOp }) + }) + ) +} diff --git a/plugins/multi-chain-web-authn/index.ts b/plugins/multi-chain-web-authn/index.ts index d3ddc8ba..44da5b1d 100644 --- a/plugins/multi-chain-web-authn/index.ts +++ b/plugins/multi-chain-web-authn/index.ts @@ -1,4 +1,9 @@ export { toMultiChainWebAuthnValidator } from "./toMultiChainWebAuthnValidator.js" +export { + type ClientWithChainId, + type SendUserOperationsParameters, + sendUserOperations +} from "./actions/sendUserOperations.js" export { type SignUserOperationsParameters, type SignUserOperationsRequest, From e4259dc73fac6a9df1506349987239a91fc7875c Mon Sep 17 00:00:00 2001 From: adnpark Date: Thu, 28 Nov 2024 23:53:00 +0900 Subject: [PATCH 2/4] chore: fix index error --- plugins/multi-chain-ecdsa/actions/sendUserOperations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/multi-chain-ecdsa/actions/sendUserOperations.ts b/plugins/multi-chain-ecdsa/actions/sendUserOperations.ts index 437abfb7..ef6e4bc7 100644 --- a/plugins/multi-chain-ecdsa/actions/sendUserOperations.ts +++ b/plugins/multi-chain-ecdsa/actions/sendUserOperations.ts @@ -201,7 +201,7 @@ export async function sendUserOperations< }) ) - for (const userOperation of _userOperations) { + for (const [index, userOperation] of _userOperations.entries()) { userOperation.signature = encodedDummySignatures[index] } From fdce3eb924ed7336dbd3dd425057af1ede512bbb Mon Sep 17 00:00:00 2001 From: adnpark Date: Fri, 29 Nov 2024 17:53:18 +0900 Subject: [PATCH 3/4] chore: remove clientWithChainId type --- .../v0.7/multiChainECDSAValidator.test.ts | 42 ++++++------------- .../actions/sendUserOperations.ts | 21 +++++----- plugins/multi-chain-ecdsa/index.ts | 5 +++ .../actions/sendUserOperations.ts | 21 +++++----- plugins/multi-chain-web-authn/index.ts | 1 - 5 files changed, 37 insertions(+), 53 deletions(-) diff --git a/packages/test/v0.7/multiChainECDSAValidator.test.ts b/packages/test/v0.7/multiChainECDSAValidator.test.ts index 8b6e255a..6dbff8f5 100644 --- a/packages/test/v0.7/multiChainECDSAValidator.test.ts +++ b/packages/test/v0.7/multiChainECDSAValidator.test.ts @@ -7,7 +7,6 @@ import { } from "@zerodev/multi-chain-ecdsa-validator" import { toMultiChainECDSAValidator } from "@zerodev/multi-chain-ecdsa-validator" import { - type ClientWithChainId, type SendUserOperationsParameters, sendUserOperations } from "@zerodev/multi-chain-ecdsa-validator/actions/sendUserOperations.js" @@ -42,7 +41,8 @@ import { hashMessage, hashTypedData, parseEther, - zeroAddress + zeroAddress, + Client } from "viem" import type { SmartAccount } from "viem/account-abstraction" import { generatePrivateKey, privateKeyToAccount } from "viem/accounts" @@ -816,23 +816,17 @@ describe("MultiChainECDSAValidator", () => { paymaster: opSepoliaZeroDevPaymasterClient }) - const clientsWithChainId: ClientWithChainId< - Transport, - Chain, - SmartAccount - >[] = [ + const clients: Client[] = [ { - ...sepoliaZerodevKernelClient, - chainId: sepolia.id + ...sepoliaZerodevKernelClient }, { - ...optimismSepoliaZerodevKernelClient, - chainId: optimismSepolia.id + ...optimismSepoliaZerodevKernelClient } ] const userOps = await Promise.all( - clientsWithChainId.map(async (client) => { + clients.map(async (client) => { return { callData: await client.account.encodeCalls([ { @@ -856,10 +850,7 @@ describe("MultiChainECDSAValidator", () => { } ] - const userOpHashes = await sendUserOperations( - clientsWithChainId, - userOpParams - ) + const userOpHashes = await sendUserOperations(clients, userOpParams) console.log("userOpHashes", userOpHashes) const sepoliaUserOpHash = userOpHashes[0] @@ -967,23 +958,17 @@ describe("MultiChainECDSAValidator", () => { paymaster: opSepoliaZeroDevPaymasterClient }) - const clientsWithChainId: ClientWithChainId< - Transport, - Chain, - SmartAccount - >[] = [ + const clients: Client[] = [ { - ...sepoliaZerodevKernelClient, - chainId: sepolia.id + ...sepoliaZerodevKernelClient }, { - ...optimismSepoliaZerodevKernelClient, - chainId: optimismSepolia.id + ...optimismSepoliaZerodevKernelClient } ] const userOps = await Promise.all( - clientsWithChainId.map(async (client) => { + clients.map(async (client) => { return { callData: await client.account.encodeCalls([ { @@ -1007,10 +992,7 @@ describe("MultiChainECDSAValidator", () => { } ] - const userOpHashes = await sendUserOperations( - clientsWithChainId, - userOpParams - ) + const userOpHashes = await sendUserOperations(clients, userOpParams) console.log("userOpHashes", userOpHashes) diff --git a/plugins/multi-chain-ecdsa/actions/sendUserOperations.ts b/plugins/multi-chain-ecdsa/actions/sendUserOperations.ts index ef6e4bc7..9c38363e 100644 --- a/plugins/multi-chain-ecdsa/actions/sendUserOperations.ts +++ b/plugins/multi-chain-ecdsa/actions/sendUserOperations.ts @@ -33,14 +33,6 @@ import { import { parseAccount } from "viem/accounts" import { getAction } from "viem/utils" -export type ClientWithChainId< - TTransport extends Transport, - TChain extends Chain | undefined, - TAccount extends SmartAccount | undefined = undefined -> = Client & { - chainId: number -} - export type SendUserOperationsParameters< account extends SmartAccount | undefined = SmartAccount | undefined, accountOverride extends SmartAccount | undefined = SmartAccount | undefined, @@ -55,7 +47,7 @@ export async function sendUserOperations< accountOverride extends SmartAccount | undefined = undefined, calls extends readonly unknown[] = readonly unknown[] >( - clients: ClientWithChainId[], + clients: Client[], args_: SendUserOperationsParameters[] ): Promise { if (clients.length < 2 && args_.length < 2) { @@ -65,9 +57,16 @@ export async function sendUserOperations< throw new Error("Number of clients and user operations do not match") } for (let i = 0; i < clients.length; i++) { - if (clients[i].chainId !== args_[i].chainId) { + const client = clients[i] + const arg = args_[i] + + if (client.chain === undefined) { + throw new Error("client.chain is undefined, please provide a chain") + } + + if (client.chain.id !== arg.chainId) { throw new Error( - `Chain ID mismatch at index ${i}: client.chainId (${clients[i].chainId}) !== args_.chainId (${args_[i].chainId})` + `Chain ID mismatch at index ${i}: client.chainId (${client.chain.id}) !== args_.chainId (${arg.chainId})` ) } } diff --git a/plugins/multi-chain-ecdsa/index.ts b/plugins/multi-chain-ecdsa/index.ts index f2eed9f4..c891e72c 100644 --- a/plugins/multi-chain-ecdsa/index.ts +++ b/plugins/multi-chain-ecdsa/index.ts @@ -6,6 +6,11 @@ export { signUserOperations } from "./actions/index.js" +export { + sendUserOperations, + type SendUserOperationsParameters +} from "./actions/sendUserOperations.js" + export { ecdsaGetMultiUserOpDummySignature } from "./utils/ecdsaGetMultiUserOpDummySignature.js" export { type MultiChainUserOpConfigForEnable, diff --git a/plugins/multi-chain-web-authn/actions/sendUserOperations.ts b/plugins/multi-chain-web-authn/actions/sendUserOperations.ts index 1917ebc0..98ba9103 100644 --- a/plugins/multi-chain-web-authn/actions/sendUserOperations.ts +++ b/plugins/multi-chain-web-authn/actions/sendUserOperations.ts @@ -34,14 +34,6 @@ import { import { parseAccount } from "viem/accounts" import { getAction } from "viem/utils" -export type ClientWithChainId< - TTransport extends Transport, - TChain extends Chain | undefined, - TAccount extends SmartAccount | undefined = undefined -> = Client & { - chainId: number -} - export type SendUserOperationsParameters< account extends SmartAccount | undefined = SmartAccount | undefined, accountOverride extends SmartAccount | undefined = SmartAccount | undefined, @@ -56,7 +48,7 @@ export async function sendUserOperations< accountOverride extends SmartAccount | undefined = undefined, calls extends readonly unknown[] = readonly unknown[] >( - clients: ClientWithChainId[], + clients: Client[], args_: SendUserOperationsParameters[] ): Promise { if (clients.length < 2 && args_.length < 2) { @@ -66,9 +58,16 @@ export async function sendUserOperations< throw new Error("Number of clients and user operations do not match") } for (let i = 0; i < clients.length; i++) { - if (clients[i].chainId !== args_[i].chainId) { + const client = clients[i] + const arg = args_[i] + + if (client.chain === undefined) { + throw new Error("client.chain is undefined, please provide a chain") + } + + if (client.chain.id !== arg.chainId) { throw new Error( - `Chain ID mismatch at index ${i}: client.chainId (${clients[i].chainId}) !== args_.chainId (${args_[i].chainId})` + `Chain ID mismatch at index ${i}: client.chainId (${client.chain.id}) !== args_.chainId (${arg.chainId})` ) } } diff --git a/plugins/multi-chain-web-authn/index.ts b/plugins/multi-chain-web-authn/index.ts index 44da5b1d..4fabe03d 100644 --- a/plugins/multi-chain-web-authn/index.ts +++ b/plugins/multi-chain-web-authn/index.ts @@ -1,6 +1,5 @@ export { toMultiChainWebAuthnValidator } from "./toMultiChainWebAuthnValidator.js" export { - type ClientWithChainId, type SendUserOperationsParameters, sendUserOperations } from "./actions/sendUserOperations.js" From 633987cb36a201d6b68cef5be8ea85abbaa5ff0f Mon Sep 17 00:00:00 2001 From: adnpark Date: Fri, 29 Nov 2024 17:53:50 +0900 Subject: [PATCH 4/4] chore: format --- packages/test/v0.7/multiChainECDSAValidator.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/test/v0.7/multiChainECDSAValidator.test.ts b/packages/test/v0.7/multiChainECDSAValidator.test.ts index 6dbff8f5..af48b1c1 100644 --- a/packages/test/v0.7/multiChainECDSAValidator.test.ts +++ b/packages/test/v0.7/multiChainECDSAValidator.test.ts @@ -28,6 +28,7 @@ import { http, type Address, type Chain, + type Client, type GetContractReturnType, type Hex, type PrivateKeyAccount, @@ -41,8 +42,7 @@ import { hashMessage, hashTypedData, parseEther, - zeroAddress, - Client + zeroAddress } from "viem" import type { SmartAccount } from "viem/account-abstraction" import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"