diff --git a/.changeset/mighty-carrots-swim.md b/.changeset/mighty-carrots-swim.md new file mode 100644 index 00000000..bff423d8 --- /dev/null +++ b/.changeset/mighty-carrots-swim.md @@ -0,0 +1,5 @@ +--- +"permissionless": patch +--- + +Added decode calls function to all smart accounts diff --git a/packages/permissionless-test/src/utils.ts b/packages/permissionless-test/src/utils.ts index aaae353d..401845c3 100644 --- a/packages/permissionless-test/src/utils.ts +++ b/packages/permissionless-test/src/utils.ts @@ -326,7 +326,7 @@ export const getKernelEcdsaClient = async < (version === "0.3.0-beta" || version === "0.3.1") && entryPoint.version === "0.6" ) { - throw new Error("ERC7579 is not supported for V06") + throw new Error("Kernel ERC7579 is not supported for V06") } return toKernelSmartAccount({ diff --git a/packages/permissionless/accounts/biconomy/toBiconomySmartAccount.ts b/packages/permissionless/accounts/biconomy/toBiconomySmartAccount.ts index 63dd7b7d..b4ab8b2c 100644 --- a/packages/permissionless/accounts/biconomy/toBiconomySmartAccount.ts +++ b/packages/permissionless/accounts/biconomy/toBiconomySmartAccount.ts @@ -12,6 +12,7 @@ import { type Client, type Hex, type LocalAccount, + decodeFunctionData, encodeAbiParameters, encodeFunctionData, encodePacked, @@ -266,6 +267,35 @@ export async function toBiconomySmartAccount( args: [call.to, call.value ?? 0n, call.data ?? "0x"] }) }, + decodeCalls: async (data) => { + const decoded = decodeFunctionData({ + abi: BiconomyAbi, + data + }) + if (decoded.functionName === "execute_ncC") { + return [ + { + to: decoded.args[0], + value: decoded.args[1], + data: decoded.args[2] + } + ] + } + if (decoded.functionName === "executeBatch_y6U") { + const calls: { to: Address; value: bigint; data: Hex }[] = [] + + for (let i = 0; i < decoded.args[0].length; i++) { + calls.push({ + to: decoded.args[0][i], + value: decoded.args[1][i], + data: decoded.args[2][i] + }) + } + + return calls + } + throw new Error("Invalid function name") + }, // Get simple dummy signature for ECDSA module authorization async getStubSignature() { const dynamicPart = ecdsaModuleAddress.substring(2).padEnd(40, "0") diff --git a/packages/permissionless/accounts/decodeCalls.test.ts b/packages/permissionless/accounts/decodeCalls.test.ts new file mode 100644 index 00000000..ea5a6beb --- /dev/null +++ b/packages/permissionless/accounts/decodeCalls.test.ts @@ -0,0 +1,340 @@ +import { + decodeFunctionData, + encodeFunctionData, + erc20Abi, + zeroAddress +} from "viem" +import { describe, expect } from "vitest" +import { testWithRpc } from "../../permissionless-test/src/testWithRpc" +import { getCoreSmartAccounts } from "../../permissionless-test/src/utils" + +describe.each(getCoreSmartAccounts())( + "decodeCalls $name", + ({ getSmartAccountClient, name }) => { + testWithRpc( + "decodeCalls v0.6 single call with no data", + async ({ rpc }) => { + try { + const smartClient = await getSmartAccountClient({ + entryPoint: { + version: "0.6" + }, + ...rpc + }) + + const callData = await smartClient.account.encodeCalls([ + { + to: zeroAddress, + data: "0x", + value: 0n + } + ]) + + if (!smartClient.account.decodeCalls) { + throw new Error("decodeCalls is not supported") + } + + const decoded = + await smartClient.account.decodeCalls(callData) + + expect(decoded).toEqual([ + { to: zeroAddress, data: "0x", value: 0n } + ]) + } catch (e) { + if ( + e instanceof Error && + e.message === "Kernel ERC7579 is not supported for V06" + ) { + return // Expected error for ERC7579 accounts with v0.6 + } + throw e + } + } + ) + + testWithRpc( + "decodeCalls v0.6 single call with data", + async ({ rpc }) => { + try { + const smartClient = await getSmartAccountClient({ + entryPoint: { + version: "0.6" + }, + ...rpc + }) + + const erc20TransactionData = encodeFunctionData({ + abi: erc20Abi, + functionName: "transfer", + args: [zeroAddress, 1000000000000000000n] + }) + + const callData = await smartClient.account.encodeCalls([ + { + to: zeroAddress, + data: erc20TransactionData, + value: 0n + } + ]) + + if (!smartClient.account.decodeCalls) { + throw new Error("decodeCalls is not supported") + } + + const decoded = + await smartClient.account.decodeCalls(callData) + + expect(decoded).toEqual([ + { + to: zeroAddress, + data: erc20TransactionData, + value: 0n + } + ]) + + const decodeErc20TransactionData = decodeFunctionData({ + abi: erc20Abi, + data: erc20TransactionData + }) + + expect(decodeErc20TransactionData.args).toEqual([ + zeroAddress, + 1000000000000000000n + ]) + } catch (e) { + if ( + e instanceof Error && + e.message === "Kernel ERC7579 is not supported for V06" + ) { + return // Expected error for ERC7579 accounts with v0.6 + } + throw e + } + } + ) + + testWithRpc("decodeCalls v0.6 multiple calls", async ({ rpc }) => { + try { + const smartClient = await getSmartAccountClient({ + entryPoint: { + version: "0.6" + }, + ...rpc + }) + + const erc20TransactionData = encodeFunctionData({ + abi: erc20Abi, + functionName: "transfer", + args: [zeroAddress, 1000000000000000000n] + }) + + const callData = await smartClient.account.encodeCalls([ + { + to: zeroAddress, + data: "0x", + value: 0n + }, + { + to: zeroAddress, + data: erc20TransactionData, + value: 10n + } + ]) + + if (!smartClient.account.decodeCalls) { + throw new Error("decodeCalls is not supported") + } + + const decoded = await smartClient.account.decodeCalls(callData) + + expect(decoded).toEqual([ + { to: zeroAddress, data: "0x", value: 0n }, + { + to: zeroAddress, + data: erc20TransactionData, + value: name === "Simple" ? 0n : 10n + } + ]) + + const decodeErc20TransactionData = decodeFunctionData({ + abi: erc20Abi, + data: erc20TransactionData + }) + + expect(decodeErc20TransactionData.args).toEqual([ + zeroAddress, + 1000000000000000000n + ]) + } catch (e) { + if ( + e instanceof Error && + e.message === "Kernel ERC7579 is not supported for V06" + ) { + return // Expected error for ERC7579 accounts with v0.6 + } + throw e + } + }) + + testWithRpc( + "decodeCalls v0.7 single call with no data", + async ({ rpc }) => { + try { + const smartClient = await getSmartAccountClient({ + entryPoint: { + version: "0.7" + }, + ...rpc + }) + + const callData = await smartClient.account.encodeCalls([ + { + to: zeroAddress, + data: "0x", + value: 0n + } + ]) + + if (!smartClient.account.decodeCalls) { + throw new Error("decodeCalls is not supported") + } + + const decoded = + await smartClient.account.decodeCalls(callData) + + expect(decoded).toEqual([ + { to: zeroAddress, data: "0x", value: 0n } + ]) + } catch (e) { + if ( + e instanceof Error && + e.message === "Kernel ERC7579 is not supported for V06" + ) { + return // Expected error for ERC7579 accounts with v0.6 + } + throw e + } + } + ) + + testWithRpc( + "decodeCalls v0.7 single call with data", + async ({ rpc }) => { + try { + const smartClient = await getSmartAccountClient({ + entryPoint: { + version: "0.7" + }, + ...rpc + }) + + const erc20TransactionData = encodeFunctionData({ + abi: erc20Abi, + functionName: "transfer", + args: [zeroAddress, 1000000000000000000n] + }) + + const callData = await smartClient.account.encodeCalls([ + { + to: zeroAddress, + data: erc20TransactionData, + value: 0n + } + ]) + + if (!smartClient.account.decodeCalls) { + throw new Error("decodeCalls is not supported") + } + + const decoded = + await smartClient.account.decodeCalls(callData) + + expect(decoded).toEqual([ + { + to: zeroAddress, + data: erc20TransactionData, + value: 0n + } + ]) + + const decodeErc20TransactionData = decodeFunctionData({ + abi: erc20Abi, + data: erc20TransactionData + }) + + expect(decodeErc20TransactionData.args).toEqual([ + zeroAddress, + 1000000000000000000n + ]) + } catch (e) { + if ( + e instanceof Error && + e.message === "Kernel ERC7579 is not supported for V06" + ) { + return // Expected error for ERC7579 accounts with v0.6 + } + throw e + } + } + ) + + testWithRpc("decodeCalls v0.7 multiple calls", async ({ rpc }) => { + try { + const smartClient = await getSmartAccountClient({ + entryPoint: { + version: "0.7" + }, + ...rpc + }) + + const erc20TransactionData = encodeFunctionData({ + abi: erc20Abi, + functionName: "transfer", + args: [zeroAddress, 1000000000000000000n] + }) + + const callData = await smartClient.account.encodeCalls([ + { + to: zeroAddress, + data: "0x", + value: 0n + }, + { + to: zeroAddress, + data: erc20TransactionData, + value: 10n + } + ]) + + if (!smartClient.account.decodeCalls) { + throw new Error("decodeCalls is not supported") + } + + const decoded = await smartClient.account.decodeCalls(callData) + + expect(decoded).toEqual([ + { to: zeroAddress, data: "0x", value: 0n }, + { to: zeroAddress, data: erc20TransactionData, value: 10n } + ]) + + const decodeErc20TransactionData = decodeFunctionData({ + abi: erc20Abi, + data: erc20TransactionData + }) + + expect(decodeErc20TransactionData.args).toEqual([ + zeroAddress, + 1000000000000000000n + ]) + } catch (e) { + if ( + e instanceof Error && + e.message === "Kernel ERC7579 is not supported for V06" + ) { + return // Expected error for ERC7579 accounts with v0.6 + } + throw e + } + }) + } +) diff --git a/packages/permissionless/accounts/etherspot/toEtherspotSmartAccount.ts b/packages/permissionless/accounts/etherspot/toEtherspotSmartAccount.ts index 8f7e1cff..e66e601f 100644 --- a/packages/permissionless/accounts/etherspot/toEtherspotSmartAccount.ts +++ b/packages/permissionless/accounts/etherspot/toEtherspotSmartAccount.ts @@ -29,6 +29,7 @@ import { getChainId } from "viem/actions" import { getAction } from "viem/utils" import { getAccountNonce } from "../../actions/public/getAccountNonce.js" import { getSenderAddress } from "../../actions/public/getSenderAddress.js" +import { decode7579Calls } from "../../utils/decode7579Calls.js" import { encode7579Calls } from "../../utils/encode7579Calls.js" import { toOwner } from "../../utils/index.js" import { @@ -283,6 +284,9 @@ export async function toEtherspotSmartAccount( callData: calls }) }, + async decodeCalls(callData) { + return decode7579Calls(callData).callData + }, async getNonce(_args) { return getAccountNonce(client, { address: await this.getAddress(), diff --git a/packages/permissionless/accounts/kernel/toKernelSmartAccount.ts b/packages/permissionless/accounts/kernel/toKernelSmartAccount.ts index 5e3d86ec..a76fb998 100644 --- a/packages/permissionless/accounts/kernel/toKernelSmartAccount.ts +++ b/packages/permissionless/accounts/kernel/toKernelSmartAccount.ts @@ -48,6 +48,7 @@ import { ROOT_MODE_KERNEL_V2, VALIDATOR_TYPE } from "./constants.js" +import { decodeCallData } from "./utils/decodeCallData.js" import { encodeCallData } from "./utils/encodeCallData.js" import { getNonceKeyWithEncoding } from "./utils/getNonceKey.js" import { isKernelV2 } from "./utils/isKernelV2.js" @@ -544,6 +545,9 @@ export async function toKernelSmartAccount< async encodeCalls(calls) { return encodeCallData({ calls, kernelVersion }) }, + async decodeCalls(callData) { + return decodeCallData({ callData, kernelVersion }) + }, async getNonce(_args) { return getAccountNonce(client, { address: await this.getAddress(), diff --git a/packages/permissionless/accounts/kernel/utils/decodeCallData.ts b/packages/permissionless/accounts/kernel/utils/decodeCallData.ts new file mode 100644 index 00000000..917f11fc --- /dev/null +++ b/packages/permissionless/accounts/kernel/utils/decodeCallData.ts @@ -0,0 +1,43 @@ +import { type Hex, decodeFunctionData } from "viem" +import { decode7579Calls } from "../../../utils/decode7579Calls.js" +import { KernelExecuteAbi } from "../abi/KernelAccountAbi.js" +import type { KernelVersion } from "../toKernelSmartAccount.js" +import { isKernelV2 } from "./isKernelV2.js" + +export const decodeCallData = ({ + kernelVersion, + callData +}: { + callData: Hex + kernelVersion: KernelVersion<"0.6" | "0.7"> +}) => { + if (isKernelV2(kernelVersion)) { + const decoded = decodeFunctionData({ + abi: KernelExecuteAbi, + data: callData + }) + + if (decoded.functionName === "executeBatch") { + return decoded.args[0].map((tx) => ({ + to: tx.to, + value: tx.value, + data: tx.data + })) + } + + if (decoded.functionName === "execute") { + const [to, value, data] = decoded.args + return [ + { + to, + value, + data + } + ] + } + + throw new Error("Invalid function name") + } + + return decode7579Calls(callData).callData +} diff --git a/packages/permissionless/accounts/light/toLightSmartAccount.ts b/packages/permissionless/accounts/light/toLightSmartAccount.ts index 68afef32..41bad270 100644 --- a/packages/permissionless/accounts/light/toLightSmartAccount.ts +++ b/packages/permissionless/accounts/light/toLightSmartAccount.ts @@ -10,6 +10,7 @@ import { type Transport, type WalletClient, concat, + decodeFunctionData, encodeFunctionData, hashMessage, hashTypedData @@ -310,6 +311,95 @@ export async function toLightSmartAccount< args: [call.to, call.value ?? 0n, call.data ?? "0x"] }) }, + async decodeCalls(callData) { + try { + const decoded = decodeFunctionData({ + abi: [ + { + inputs: [ + { + internalType: "address[]", + name: "dest", + type: "address[]" + }, + { + internalType: "uint256[]", + name: "value", + type: "uint256[]" + }, + { + internalType: "bytes[]", + name: "func", + type: "bytes[]" + } + ], + name: "executeBatch", + outputs: [], + stateMutability: "nonpayable", + type: "function" + } + ], + data: callData + }) + + if (decoded.functionName === "executeBatch") { + const calls: { + to: Address + value: bigint + data: Hex + }[] = [] + + for (let i = 0; i < decoded.args[0].length; i++) { + calls.push({ + to: decoded.args[0][i], + value: decoded.args[1][i], + data: decoded.args[2][i] + }) + } + + return calls + } + + throw new Error("Invalid function name") + } catch (_) { + const decoded = decodeFunctionData({ + abi: [ + { + inputs: [ + { + internalType: "address", + name: "dest", + type: "address" + }, + { + internalType: "uint256", + name: "value", + type: "uint256" + }, + { + internalType: "bytes", + name: "func", + type: "bytes" + } + ], + name: "execute", + outputs: [], + stateMutability: "nonpayable", + type: "function" + } + ], + data: callData + }) + + return [ + { + to: decoded.args[0], + value: decoded.args[1], + data: decoded.args[2] + } + ] + } + }, async getNonce(args) { return getAccountNonce(client, { address: await this.getAddress(), diff --git a/packages/permissionless/accounts/nexus/toNexusSmartAccount.ts b/packages/permissionless/accounts/nexus/toNexusSmartAccount.ts index 1d40c64c..2991b588 100644 --- a/packages/permissionless/accounts/nexus/toNexusSmartAccount.ts +++ b/packages/permissionless/accounts/nexus/toNexusSmartAccount.ts @@ -38,6 +38,7 @@ import { import { getChainId, readContract } from "viem/actions" import { getAction } from "viem/utils" import { getAccountNonce } from "../../actions/public/getAccountNonce.js" +import { decode7579Calls } from "../../utils/decode7579Calls.js" import { encode7579Calls } from "../../utils/encode7579Calls.js" import { type EthereumProvider, toOwner } from "../../utils/toOwner.js" @@ -235,6 +236,9 @@ export async function toNexusSmartAccount( callData: calls }) }, + async decodeCalls(callData) { + return decode7579Calls(callData).callData + }, async getStubSignature() { const dynamicPart = validatorAddress.substring(2).padEnd(40, "0") return `0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000${dynamicPart}000000000000000000000000000000000000000000000000000000000000004181d4b4981670cb18f99f0b4a66446df1bf5b204d24cfcb659bf38ba27a4359b5711649ec2423c5e1247245eba2964679b6a1dbb85c992ae40b9b00c6935b02ff1b00000000000000000000000000000000000000000000000000000000000000` as Hex diff --git a/packages/permissionless/accounts/safe/toSafeSmartAccount.ts b/packages/permissionless/accounts/safe/toSafeSmartAccount.ts index c76b468d..565e904d 100644 --- a/packages/permissionless/accounts/safe/toSafeSmartAccount.ts +++ b/packages/permissionless/accounts/safe/toSafeSmartAccount.ts @@ -13,15 +13,19 @@ import { type TypedDataDefinition, type WalletClient, concat, + decodeFunctionData, encodeAbiParameters, encodeFunctionData, encodePacked, + getAddress, getContractAddress, hashMessage, hashTypedData, hexToBigInt, keccak256, pad, + size, + slice, toBytes, toHex, zeroAddress @@ -38,6 +42,7 @@ import { import { getChainId, readContract } from "viem/actions" import { getAction } from "viem/utils" import { getAccountNonce } from "../../actions/public/getAccountNonce.js" +import { decode7579Calls } from "../../utils/decode7579Calls.js" import { encode7579Calls } from "../../utils/encode7579Calls.js" import { isSmartAccountDeployed } from "../../utils/isSmartAccountDeployed.js" import { type EthereumProvider, toOwner } from "../../utils/toOwner.js" @@ -490,6 +495,11 @@ const encodeInternalTransaction = (tx: { operation: 0 | 1 }): string => { const encoded = encodePacked( + // uint8 = 1 byte for operation + // address = 20 bytes for to address + // uint256 = 32 bytes for value + // uint256 = 32 bytes for data length + // bytes = dynamic length for data ["uint8", "address", "uint256", "uint256", "bytes"], [ tx.operation, @@ -1401,6 +1411,81 @@ export async function toSafeSmartAccount< args: [to, value, data, operationType] }) }, + async decodeCalls(callData) { + try { + const decoded = decodeFunctionData({ + abi: setupSafeAbi, + data: callData + }) + + return decode7579Calls(decoded.args[0].callData).callData + } catch (_) {} + + try { + return decode7579Calls(callData).callData + } catch (_) {} + + const decoded = decodeFunctionData({ + abi: executeUserOpWithErrorStringAbi, + data: callData + }) + + const to = decoded.args[0] + const value = decoded.args[1] + const data = decoded.args[2] + + if (to === multiSendCallOnlyAddress) { + const decodedMultiSend = decodeFunctionData({ + abi: multiSendAbi, + data: data + }) + + const dataToDecode = decodedMultiSend.args[0] + const transactions: { + to: Address + value: bigint + data: Hex + }[] = [] + + let position = 0 + const dataLength = size(dataToDecode) + + while (position < dataLength) { + // skip the operation type + position += 1 + + const to = getAddress( + slice(dataToDecode, position, position + 20) + ) + position += 20 + + const value = BigInt( + slice(dataToDecode, position, position + 32) + ) + position += 32 + + const dataLength = Number( + BigInt(slice(dataToDecode, position, position + 32)) * + BigInt(2) + ) + + position += 32 + + const data = slice( + dataToDecode, + position, + position + dataLength + ) + position += dataLength + + transactions.push({ to, value, data }) + } + + return transactions + } + + return [{ to, value, data }] + }, async getNonce(args) { return getAccountNonce(client, { address: await this.getAddress(), diff --git a/packages/permissionless/accounts/simple/toSimpleSmartAccount.ts b/packages/permissionless/accounts/simple/toSimpleSmartAccount.ts index e691d16d..20836a64 100644 --- a/packages/permissionless/accounts/simple/toSimpleSmartAccount.ts +++ b/packages/permissionless/accounts/simple/toSimpleSmartAccount.ts @@ -9,6 +9,7 @@ import { type OneOf, type Transport, type WalletClient, + decodeFunctionData, encodeFunctionData } from "viem" import { @@ -291,6 +292,132 @@ export async function toSimpleSmartAccount< args: [call.to, call.value ?? 0n, call.data ?? "0x"] }) }, + decodeCalls: async (callData) => { + try { + const decodedV6 = decodeFunctionData({ + abi: [ + { + inputs: [ + { + internalType: "address[]", + name: "dest", + type: "address[]" + }, + { + internalType: "bytes[]", + name: "func", + type: "bytes[]" + } + ], + name: "executeBatch", + outputs: [], + stateMutability: "nonpayable", + type: "function" + } + ], + data: callData + }) + + const calls: { + to: Address + data: Hex + value?: bigint + }[] = [] + + for (let i = 0; i < decodedV6.args.length; i++) { + calls.push({ + to: decodedV6.args[0][i], + data: decodedV6.args[1][i], + value: 0n + }) + } + + return calls + } catch (_) { + try { + const decodedV7 = decodeFunctionData({ + abi: [ + { + inputs: [ + { + internalType: "address[]", + name: "dest", + type: "address[]" + }, + { + internalType: "uint256[]", + name: "value", + type: "uint256[]" + }, + { + internalType: "bytes[]", + name: "func", + type: "bytes[]" + } + ], + name: "executeBatch", + outputs: [], + stateMutability: "nonpayable", + type: "function" + } + ], + data: callData + }) + + const calls: { + to: Address + data: Hex + value?: bigint + }[] = [] + + for (let i = 0; i < decodedV7.args[0].length; i++) { + calls.push({ + to: decodedV7.args[0][i], + value: decodedV7.args[1][i], + data: decodedV7.args[2][i] + }) + } + + return calls + } catch (_) { + const decodedSingle = decodeFunctionData({ + abi: [ + { + inputs: [ + { + internalType: "address", + name: "dest", + type: "address" + }, + { + internalType: "uint256", + name: "value", + type: "uint256" + }, + { + internalType: "bytes", + name: "func", + type: "bytes" + } + ], + name: "execute", + outputs: [], + stateMutability: "nonpayable", + type: "function" + } + ], + data: callData + }) + return [ + { + to: decodedSingle.args[0], + value: decodedSingle.args[1], + data: decodedSingle.args[2] + } + ] + } + } + }, async getNonce(args) { return getAccountNonce(client, { address: await this.getAddress(), diff --git a/packages/permissionless/accounts/thirdweb/toThirdwebSmartAccount.ts b/packages/permissionless/accounts/thirdweb/toThirdwebSmartAccount.ts index 2510f2c2..9217d046 100644 --- a/packages/permissionless/accounts/thirdweb/toThirdwebSmartAccount.ts +++ b/packages/permissionless/accounts/thirdweb/toThirdwebSmartAccount.ts @@ -25,6 +25,7 @@ import { } from "viem/account-abstraction" import { getAction, toHex } from "viem/utils" import { type EthereumProvider, toOwner } from "../../utils/toOwner.js" +import { decodeCallData } from "./utils/decodeCallData.js" import { encodeCallData } from "./utils/encodeCallData.js" import { getAccountAddress } from "./utils/getAccountAddress.js" import { getFactoryData } from "./utils/getFactoryData.js" @@ -158,6 +159,9 @@ export async function toThirdwebSmartAccount< async encodeCalls(calls) { return encodeCallData(calls) }, + async decodeCalls(callData) { + return decodeCallData(callData) + }, async getNonce(args) { return getAccountNonce(client, { address: await this.getAddress(), diff --git a/packages/permissionless/accounts/thirdweb/utils/decodeCallData.ts b/packages/permissionless/accounts/thirdweb/utils/decodeCallData.ts new file mode 100644 index 00000000..8324739c --- /dev/null +++ b/packages/permissionless/accounts/thirdweb/utils/decodeCallData.ts @@ -0,0 +1,87 @@ +import { type Address, type Hex, decodeFunctionData } from "viem" + +export const decodeCallData = async (callData: `0x${string}`) => { + try { + const decodedBatch = decodeFunctionData({ + abi: [ + { + inputs: [ + { + internalType: "address[]", + name: "dest", + type: "address[]" + }, + { + internalType: "uint256[]", + name: "value", + type: "uint256[]" + }, + { + internalType: "bytes[]", + name: "func", + type: "bytes[]" + } + ], + name: "executeBatch", + outputs: [], + stateMutability: "nonpayable", + type: "function" + } + ], + data: callData + }) + + const calls: { + to: Address + data: Hex + value?: bigint + }[] = [] + + for (let i = 0; i < decodedBatch.args[0].length; i++) { + calls.push({ + to: decodedBatch.args[0][i], + value: decodedBatch.args[1][i], + data: decodedBatch.args[2][i] + }) + } + + return calls + } catch (_) {} + + const decodedSingle = decodeFunctionData({ + abi: [ + { + inputs: [ + { + internalType: "address", + name: "dest", + type: "address" + }, + { + internalType: "uint256", + name: "value", + type: "uint256" + }, + { + internalType: "bytes", + name: "func", + type: "bytes" + } + ], + name: "execute", + outputs: [], + stateMutability: "nonpayable", + type: "function" + } + ], + data: callData + }) + + return [ + { + to: decodedSingle.args[0], + value: decodedSingle.args[1], + data: decodedSingle.args[2] + } + ] +} diff --git a/packages/permissionless/accounts/trust/toTrustSmartAccount.ts b/packages/permissionless/accounts/trust/toTrustSmartAccount.ts index bf39bfea..b129f857 100644 --- a/packages/permissionless/accounts/trust/toTrustSmartAccount.ts +++ b/packages/permissionless/accounts/trust/toTrustSmartAccount.ts @@ -27,6 +27,7 @@ import { import { getAction } from "viem/utils" import { getSenderAddress } from "../../actions/public/getSenderAddress.js" import { type EthereumProvider, toOwner } from "../../utils/toOwner.js" +import { decodeCallData } from "./utils/decodeCallData.js" import { encodeCallData } from "./utils/encodeCallData.js" import { getFactoryData } from "./utils/getFactoryData.js" @@ -168,6 +169,9 @@ export async function toTrustSmartAccount( async encodeCalls(calls) { return encodeCallData(calls) }, + async decodeCalls(callData) { + return decodeCallData(callData) + }, async getNonce(args) { return getAccountNonce(client, { address: await this.getAddress(), diff --git a/packages/permissionless/accounts/trust/utils/decodeCallData.ts b/packages/permissionless/accounts/trust/utils/decodeCallData.ts new file mode 100644 index 00000000..8324739c --- /dev/null +++ b/packages/permissionless/accounts/trust/utils/decodeCallData.ts @@ -0,0 +1,87 @@ +import { type Address, type Hex, decodeFunctionData } from "viem" + +export const decodeCallData = async (callData: `0x${string}`) => { + try { + const decodedBatch = decodeFunctionData({ + abi: [ + { + inputs: [ + { + internalType: "address[]", + name: "dest", + type: "address[]" + }, + { + internalType: "uint256[]", + name: "value", + type: "uint256[]" + }, + { + internalType: "bytes[]", + name: "func", + type: "bytes[]" + } + ], + name: "executeBatch", + outputs: [], + stateMutability: "nonpayable", + type: "function" + } + ], + data: callData + }) + + const calls: { + to: Address + data: Hex + value?: bigint + }[] = [] + + for (let i = 0; i < decodedBatch.args[0].length; i++) { + calls.push({ + to: decodedBatch.args[0][i], + value: decodedBatch.args[1][i], + data: decodedBatch.args[2][i] + }) + } + + return calls + } catch (_) {} + + const decodedSingle = decodeFunctionData({ + abi: [ + { + inputs: [ + { + internalType: "address", + name: "dest", + type: "address" + }, + { + internalType: "uint256", + name: "value", + type: "uint256" + }, + { + internalType: "bytes", + name: "func", + type: "bytes" + } + ], + name: "execute", + outputs: [], + stateMutability: "nonpayable", + type: "function" + } + ], + data: callData + }) + + return [ + { + to: decodedSingle.args[0], + value: decodedSingle.args[1], + data: decodedSingle.args[2] + } + ] +} diff --git a/packages/permissionless/utils/decode7579Calls.ts b/packages/permissionless/utils/decode7579Calls.ts new file mode 100644 index 00000000..3b5ed6d3 --- /dev/null +++ b/packages/permissionless/utils/decode7579Calls.ts @@ -0,0 +1,133 @@ +import { + type Address, + type Hex, + decodeAbiParameters, + decodeFunctionData, + getAddress, + size, + slice +} from "viem" +import type { + CallType, + ExecutionMode +} from "../actions/erc7579/supportsExecutionMode.js" + +export type DecodeCallDataReturnType = { + mode: ExecutionMode + callData: readonly { + to: Address + value?: bigint | undefined + data?: Hex | undefined + }[] +} + +export function decode7579Calls(callData: Hex): DecodeCallDataReturnType { + const executeAbi = [ + { + type: "function", + name: "execute", + inputs: [ + { + name: "execMode", + type: "bytes32", + internalType: "ExecMode" + }, + { + name: "executionCalldata", + type: "bytes", + internalType: "bytes" + } + ], + outputs: [], + stateMutability: "payable" + } + ] as const + + const decoded = decodeFunctionData({ + abi: executeAbi, + data: callData + }) + + const mode = decoded.args[0] + const executionCalldata = decoded.args[1] + + const callType = slice(mode, 0, 1) // First byte + const revertOnError = slice(mode, 1, 2) // Second byte + const selector = slice(mode, 3, 7) as Hex // bytes 5-8 + const context = slice(mode, 7) as Hex // bytes 9-32 + + let type: CallType + switch (BigInt(callType)) { + case BigInt(0x00): + type = "call" + break + case BigInt(0x01): + type = "batchcall" + break + case BigInt(0xff): + type = "delegatecall" + break + default: + throw new Error("Invalid call type") + } + + const decodedMode: ExecutionMode = { + type, + revertOnError: BigInt(revertOnError) === BigInt(1), + selector, + context + } + + if (decodedMode.type === "batchcall") { + const [calls] = decodeAbiParameters( + [ + { + name: "executionBatch", + type: "tuple[]", + components: [ + { + name: "target", + type: "address" + }, + { + name: "value", + type: "uint256" + }, + { + name: "callData", + type: "bytes" + } + ] + } + ], + executionCalldata + ) + + return { + mode: decodedMode, + callData: calls.map((call) => ({ + to: call.target, + value: call.value, + data: call.callData + })) + } + } + + // Single call - calldata is encoded as concatenated (to, value, data) + const to = getAddress(slice(executionCalldata, 0, 20)) // 20 bytes address with 0x prefix + const value = BigInt(slice(executionCalldata, 20, 52)) // 32 bytes value + + const data = + size(executionCalldata) > 52 ? slice(executionCalldata, 52) : "0x" // Remaining bytes are calldata + + return { + mode: decodedMode, + callData: [ + { + to, + value, + data + } + ] + } +} diff --git a/packages/permissionless/utils/getEstimationCallData.ts b/packages/permissionless/utils/getEstimationCallData.ts new file mode 100644 index 00000000..71d7a765 --- /dev/null +++ b/packages/permissionless/utils/getEstimationCallData.ts @@ -0,0 +1,279 @@ +import { type Address, type Hex, encodeFunctionData } from "viem" +import { + type UserOperation, + entryPoint06Abi, + toPackedUserOperation +} from "viem/account-abstraction" + +function getPimlicoEstimationCallData06({ + userOperation, + entrypoint +}: { + userOperation: UserOperation<"0.6"> + entrypoint: { + address: Address + version: "0.6" + } +}): { to: Address; data: Hex } { + return { + to: entrypoint.address, + data: encodeFunctionData({ + abi: entryPoint06Abi, + functionName: "simulateHandleOp", + args: [ + { + callData: userOperation.callData, + callGasLimit: userOperation.callGasLimit, + initCode: userOperation.initCode ?? "0x", + maxFeePerGas: userOperation.maxFeePerGas, + maxPriorityFeePerGas: userOperation.maxPriorityFeePerGas, + nonce: userOperation.nonce, + paymasterAndData: userOperation.paymasterAndData ?? "0x", + preVerificationGas: userOperation.preVerificationGas, + sender: userOperation.sender, + signature: userOperation.signature, + verificationGasLimit: userOperation.verificationGasLimit + }, + "0x", + "0x" + ] + }) + } +} + +function encodeSimulateHandleOpLast({ + userOperation +}: { + userOperation: UserOperation<"0.7"> +}): Hex { + const userOperations = [userOperation] + const packedUserOperations = userOperations.map((uop) => ({ + packedUserOperation: toPackedUserOperation(uop) + })) + + const simulateHandleOpCallData = encodeFunctionData({ + abi: [ + { + type: "function", + name: "simulateHandleOpLast", + inputs: [ + { + name: "ops", + type: "tuple[]", + internalType: "struct PackedUserOperation[]", + components: [ + { + name: "sender", + type: "address", + internalType: "address" + }, + { + name: "nonce", + type: "uint256", + internalType: "uint256" + }, + { + name: "initCode", + type: "bytes", + internalType: "bytes" + }, + { + name: "callData", + type: "bytes", + internalType: "bytes" + }, + { + name: "accountGasLimits", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "preVerificationGas", + type: "uint256", + internalType: "uint256" + }, + { + name: "gasFees", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "paymasterAndData", + type: "bytes", + internalType: "bytes" + }, + { + name: "signature", + type: "bytes", + internalType: "bytes" + } + ] + } + ], + outputs: [ + { + name: "", + type: "tuple", + internalType: + "struct IEntryPointSimulations.ExecutionResult", + components: [ + { + name: "preOpGas", + type: "uint256", + internalType: "uint256" + }, + { + name: "paid", + type: "uint256", + internalType: "uint256" + }, + { + name: "accountValidationData", + type: "uint256", + internalType: "uint256" + }, + { + name: "paymasterValidationData", + type: "uint256", + internalType: "uint256" + }, + { + name: "paymasterVerificationGasLimit", + type: "uint256", + internalType: "uint256" + }, + { + name: "paymasterPostOpGasLimit", + type: "uint256", + internalType: "uint256" + }, + { + name: "targetSuccess", + type: "bool", + internalType: "bool" + }, + { + name: "targetResult", + type: "bytes", + internalType: "bytes" + } + ] + } + ], + stateMutability: "nonpayable" + } + ], + functionName: "simulateHandleOpLast", + args: [packedUserOperations.map((uop) => uop.packedUserOperation)] + }) + + return simulateHandleOpCallData +} + +const PIMLICO_ESTIMATION_ADDRESS = "0x949CeCa936909f75E5A40bD285d9985eFBb9B0D3" + +function getPimlicoEstimationCallData07({ + userOperation, + estimationAddress, + entrypoint +}: { + userOperation: UserOperation<"0.7"> + estimationAddress?: Address + entrypoint: { + address: Address + version: "0.7" + } +}): { to: Address; data: Hex } { + const simulateHandleOpLast = encodeSimulateHandleOpLast({ + userOperation + }) + + return { + to: estimationAddress ?? PIMLICO_ESTIMATION_ADDRESS, + data: encodeFunctionData({ + abi: [ + { + inputs: [ + { + internalType: "address payable", + name: "ep", + type: "address" + }, + { + internalType: "bytes[]", + name: "data", + type: "bytes[]" + } + ], + name: "simulateEntryPoint", + outputs: [ + { + internalType: "bytes[]", + name: "", + type: "bytes[]" + } + ], + stateMutability: "nonpayable", + type: "function" + } + ], + functionName: "simulateEntryPoint", + args: [entrypoint.address, [simulateHandleOpLast]] + }) + } +} + +export type GetPimlicoEstimationCallDataParams< + entryPointVersion extends "0.6" | "0.7" +> = { + userOperation: UserOperation + entrypoint: { + address: Address + version: entryPointVersion + } +} & (entryPointVersion extends "0.6" + ? { + estimationAddress: never + } + : { estimationAddress?: Address }) + +function isEntryPoint06( + args: GetPimlicoEstimationCallDataParams<"0.6" | "0.7"> +): args is GetPimlicoEstimationCallDataParams<"0.6"> { + return args.entrypoint.version === "0.6" +} + +function isEntryPoint07( + args: GetPimlicoEstimationCallDataParams<"0.6" | "0.7"> +): args is GetPimlicoEstimationCallDataParams<"0.7"> { + return args.entrypoint.version === "0.7" +} + +export function getPimlicoEstimationCallData< + entryPointVersion extends "0.6" | "0.7" +>( + args: GetPimlicoEstimationCallDataParams +): { to: Address; data: Hex } { + if (isEntryPoint06(args)) { + return getPimlicoEstimationCallData06({ + userOperation: args.userOperation, + entrypoint: { + address: args.entrypoint.address, + version: "0.6" + } + }) + } + + if (isEntryPoint07(args)) { + return getPimlicoEstimationCallData07({ + userOperation: args.userOperation, + estimationAddress: args.estimationAddress, + entrypoint: { + address: args.entrypoint.address, + version: "0.7" + } + }) + } + + throw new Error("Invalid entrypoint version") +} diff --git a/packages/permissionless/utils/index.ts b/packages/permissionless/utils/index.ts index 3da817b5..312ce41e 100644 --- a/packages/permissionless/utils/index.ts +++ b/packages/permissionless/utils/index.ts @@ -20,6 +20,12 @@ import { type EncodeCallDataParams, encode7579Calls } from "./encode7579Calls.js" + +import { + type DecodeCallDataReturnType, + decode7579Calls +} from "./decode7579Calls.js" + import { type Erc20AllowanceOverrideParameters, erc20AllowanceOverride @@ -44,6 +50,8 @@ export { encodeInstallModule, type EncodeCallDataParams, encode7579Calls, + decode7579Calls, + type DecodeCallDataReturnType, erc20AllowanceOverride, erc20BalanceOverride, type Erc20AllowanceOverrideParameters,