From 2a9d09d7dfa39e7cc0ad2619e62797eb47d36fe4 Mon Sep 17 00:00:00 2001 From: clarkwu Date: Wed, 29 May 2024 15:15:10 +0800 Subject: [PATCH] add smart account --- .../biconomy/abi/BiconomySmartAccountAbi.ts | 105 ++ .../privateKeyToBiconomySmartAccount.ts | 44 + .../biconomy/signerToBiconomySmartAccount.ts | 439 ++++++++ src/accounts/index.ts | 89 ++ src/accounts/kernel/abi/KernelAccountAbi.ts | 87 ++ src/accounts/kernel/abi/KernelV3AccountAbi.ts | 88 ++ .../kernel/abi/KernelV3MetaFactoryAbi.ts | 17 + src/accounts/kernel/constants.ts | 21 + .../kernel/signerToEcdsaKernelSmartAccount.ts | 609 ++++++++++++ src/accounts/kernel/utils/encodeCallData.ts | 114 +++ src/accounts/kernel/utils/getExecMode.ts | 18 + src/accounts/kernel/utils/getNonceKey.ts | 32 + src/accounts/kernel/utils/signMessage.ts | 48 + src/accounts/kernel/utils/signTypedData.ts | 74 ++ src/accounts/kernel/utils/wrapMessageHash.ts | 38 + .../light/privateKeyToLightSmartAccount.ts | 37 + .../light/signerToLightSmartAccount.ts | 407 ++++++++ .../safe/privateKeyToSafeSmartAccount.ts | 37 + src/accounts/safe/signerToSafeSmartAccount.ts | 936 ++++++++++++++++++ .../simple/privateKeyToSimpleSmartAccount.ts | 52 + .../simple/signerToSimpleSmartAccount.ts | 350 +++++++ src/accounts/toSmartAccount.ts | 167 ++++ src/accounts/types.ts | 71 ++ src/actions/public/getAccountNonce.ts | 80 ++ src/actions/public/getSenderAddress.ts | 211 ++++ src/errors/account.ts | 345 +++++++ src/errors/bundler.ts | 62 ++ src/errors/estimateUserOperationGas.ts | 70 ++ src/errors/gas.ts | 120 +++ src/errors/index.ts | 118 +++ src/errors/paymaster.ts | 257 +++++ src/errors/sendUserOperation.ts | 43 + src/errors/utils.ts | 19 + src/index.ts | 4 +- src/types/bundler.ts | 131 +++ src/types/entrypoint.ts | 13 + src/types/index.ts | 45 + src/types/userOperation.ts | 102 ++ src/utils/deepHexlify.ts | 35 + src/utils/errors/getBundlerError.ts | 224 +++++ .../getEstimateUserOperationGasError.ts | 57 ++ src/utils/errors/getSendUserOperationError.ts | 26 + ...etAddressFromInitCodeOrPaymasterAndData.ts | 13 + src/utils/getEntryPointVersion.ts | 31 + src/utils/getPackedUserOperation.ts | 120 +++ src/utils/getRequiredPrefund.ts | 57 ++ src/utils/getUserOperationHash.ts | 158 +++ src/utils/index.ts | 52 + src/utils/isSmartAccountDeployed.ts | 16 + src/utils/observe.ts | 74 ++ src/utils/providerToSmartAccountSigner.ts | 34 + src/utils/signUserOperationHashWithECDSA.ts | 134 +++ src/utils/walletClientToSmartAccountSigner.ts | 46 + 53 files changed, 6575 insertions(+), 2 deletions(-) create mode 100644 src/accounts/biconomy/abi/BiconomySmartAccountAbi.ts create mode 100644 src/accounts/biconomy/privateKeyToBiconomySmartAccount.ts create mode 100644 src/accounts/biconomy/signerToBiconomySmartAccount.ts create mode 100644 src/accounts/index.ts create mode 100644 src/accounts/kernel/abi/KernelAccountAbi.ts create mode 100644 src/accounts/kernel/abi/KernelV3AccountAbi.ts create mode 100644 src/accounts/kernel/abi/KernelV3MetaFactoryAbi.ts create mode 100644 src/accounts/kernel/constants.ts create mode 100644 src/accounts/kernel/signerToEcdsaKernelSmartAccount.ts create mode 100644 src/accounts/kernel/utils/encodeCallData.ts create mode 100644 src/accounts/kernel/utils/getExecMode.ts create mode 100644 src/accounts/kernel/utils/getNonceKey.ts create mode 100644 src/accounts/kernel/utils/signMessage.ts create mode 100644 src/accounts/kernel/utils/signTypedData.ts create mode 100644 src/accounts/kernel/utils/wrapMessageHash.ts create mode 100644 src/accounts/light/privateKeyToLightSmartAccount.ts create mode 100644 src/accounts/light/signerToLightSmartAccount.ts create mode 100644 src/accounts/safe/privateKeyToSafeSmartAccount.ts create mode 100644 src/accounts/safe/signerToSafeSmartAccount.ts create mode 100644 src/accounts/simple/privateKeyToSimpleSmartAccount.ts create mode 100644 src/accounts/simple/signerToSimpleSmartAccount.ts create mode 100644 src/accounts/toSmartAccount.ts create mode 100644 src/accounts/types.ts create mode 100644 src/actions/public/getAccountNonce.ts create mode 100644 src/actions/public/getSenderAddress.ts create mode 100644 src/errors/account.ts create mode 100644 src/errors/bundler.ts create mode 100644 src/errors/estimateUserOperationGas.ts create mode 100644 src/errors/gas.ts create mode 100644 src/errors/index.ts create mode 100644 src/errors/paymaster.ts create mode 100644 src/errors/sendUserOperation.ts create mode 100644 src/errors/utils.ts create mode 100644 src/types/bundler.ts create mode 100644 src/types/entrypoint.ts create mode 100644 src/types/index.ts create mode 100644 src/types/userOperation.ts create mode 100644 src/utils/deepHexlify.ts create mode 100644 src/utils/errors/getBundlerError.ts create mode 100644 src/utils/errors/getEstimateUserOperationGasError.ts create mode 100644 src/utils/errors/getSendUserOperationError.ts create mode 100644 src/utils/getAddressFromInitCodeOrPaymasterAndData.ts create mode 100644 src/utils/getEntryPointVersion.ts create mode 100644 src/utils/getPackedUserOperation.ts create mode 100644 src/utils/getRequiredPrefund.ts create mode 100644 src/utils/getUserOperationHash.ts create mode 100644 src/utils/index.ts create mode 100644 src/utils/isSmartAccountDeployed.ts create mode 100644 src/utils/observe.ts create mode 100644 src/utils/providerToSmartAccountSigner.ts create mode 100644 src/utils/signUserOperationHashWithECDSA.ts create mode 100644 src/utils/walletClientToSmartAccountSigner.ts diff --git a/src/accounts/biconomy/abi/BiconomySmartAccountAbi.ts b/src/accounts/biconomy/abi/BiconomySmartAccountAbi.ts new file mode 100644 index 0000000..db6ee1a --- /dev/null +++ b/src/accounts/biconomy/abi/BiconomySmartAccountAbi.ts @@ -0,0 +1,105 @@ +/** + * The exeuctor abi, used to execute transactions on the Biconomy Modular Smart Account + */ +export const BiconomyExecuteAbi = [ + { + inputs: [ + { + internalType: "address", + name: "dest", + type: "address" + }, + { + internalType: "uint256", + name: "value", + type: "uint256" + }, + { + internalType: "bytes", + name: "func", + type: "bytes" + } + ], + name: "execute_ncC", + outputs: [], + stateMutability: "nonpayable", + type: "function" + }, + { + inputs: [ + { + internalType: "address[]", + name: "dest", + type: "address[]" + }, + { + internalType: "uint256[]", + name: "value", + type: "uint256[]" + }, + { + internalType: "bytes[]", + name: "func", + type: "bytes[]" + } + ], + name: "executeBatch_y6U", + outputs: [], + stateMutability: "nonpayable", + type: "function" + } +] as const + +/** + * The init abi, used to initialise Biconomy Modular Smart Account / setup default ECDSA module + */ +export const BiconomyInitAbi = [ + { + inputs: [ + { + internalType: "address", + name: "handler", + type: "address" + }, + { + internalType: "address", + name: "moduleSetupContract", + type: "address" + }, + { + internalType: "bytes", + name: "moduleSetupData", + type: "bytes" + } + ], + name: "init", + outputs: [ + { + internalType: "address", + name: "", + type: "address" + } + ], + stateMutability: "nonpayable", + type: "function" + }, + { + inputs: [ + { + internalType: "address", + name: "eoaOwner", + type: "address" + } + ], + name: "initForSmartAccount", + outputs: [ + { + internalType: "address", + name: "", + type: "address" + } + ], + stateMutability: "nonpayable", + type: "function" + } +] diff --git a/src/accounts/biconomy/privateKeyToBiconomySmartAccount.ts b/src/accounts/biconomy/privateKeyToBiconomySmartAccount.ts new file mode 100644 index 0000000..155af1d --- /dev/null +++ b/src/accounts/biconomy/privateKeyToBiconomySmartAccount.ts @@ -0,0 +1,44 @@ +import { type Chain, type Client, type Hex, type Transport } from "viem" +import { privateKeyToAccount } from "viem/accounts" +import type { ENTRYPOINT_ADDRESS_V06_TYPE, Prettify } from "../../types" +import { + type BiconomySmartAccount, + type SignerToBiconomySmartAccountParameters, + signerToBiconomySmartAccount +} from "./signerToBiconomySmartAccount" + +export type PrivateKeyToBiconomySmartAccountParameters< + entryPoint extends ENTRYPOINT_ADDRESS_V06_TYPE +> = Prettify< + { + privateKey: Hex + } & Omit, "signer"> +> + +/** + * @description Creates a Biconomy Smart Account from a private key. + * + * @returns A Private Key Biconomy Smart Account using ECDSA as default validation module. + */ +export async function privateKeyToBiconomySmartAccount< + entryPoint extends ENTRYPOINT_ADDRESS_V06_TYPE, + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined +>( + client: Client, + { + privateKey, + ...rest + }: PrivateKeyToBiconomySmartAccountParameters +): Promise> { + const privateKeyAccount = privateKeyToAccount(privateKey) + return signerToBiconomySmartAccount< + entryPoint, + TTransport, + TChain, + "privateKey" + >(client, { + signer: privateKeyAccount, + ...rest + }) +} diff --git a/src/accounts/biconomy/signerToBiconomySmartAccount.ts b/src/accounts/biconomy/signerToBiconomySmartAccount.ts new file mode 100644 index 0000000..7901bed --- /dev/null +++ b/src/accounts/biconomy/signerToBiconomySmartAccount.ts @@ -0,0 +1,439 @@ +import type { TypedData } from "viem" +import { + type Address, + type Chain, + type Client, + type Hex, + type LocalAccount, + type Transport, + type TypedDataDefinition, + concatHex, + encodeAbiParameters, + encodeFunctionData, + encodePacked, + getContractAddress, + hexToBigInt, + keccak256, + parseAbiParameters +} from "viem" +import { getChainId, signMessage, signTypedData } from "viem/actions" +import { getAccountNonce } from "../../actions/public/getAccountNonce" +import type { Prettify } from "../../types/" +import type { ENTRYPOINT_ADDRESS_V06_TYPE } from "../../types/entrypoint" +import { getEntryPointVersion } from "../../utils" +import { getUserOperationHash } from "../../utils/getUserOperationHash" +import { isSmartAccountDeployed } from "../../utils/isSmartAccountDeployed" +import { toSmartAccount } from "../toSmartAccount" +import { + SignTransactionNotSupportedBySmartAccount, + type SmartAccount, + type SmartAccountSigner +} from "../types" +import { + BiconomyExecuteAbi, + BiconomyInitAbi +} from "./abi/BiconomySmartAccountAbi" + +export type BiconomySmartAccount< + entryPoint extends ENTRYPOINT_ADDRESS_V06_TYPE, + transport extends Transport = Transport, + chain extends Chain | undefined = Chain | undefined +> = SmartAccount + +/** + * The account creation ABI for Biconomy Smart Account (from the biconomy SmartAccountFactory) + */ + +const createAccountAbi = [ + { + inputs: [ + { + internalType: "address", + name: "moduleSetupContract", + type: "address" + }, + { + internalType: "bytes", + name: "moduleSetupData", + type: "bytes" + }, + { + internalType: "uint256", + name: "index", + type: "uint256" + } + ], + name: "deployCounterFactualAccount", + outputs: [ + { + internalType: "address", + name: "proxy", + type: "address" + } + ], + stateMutability: "nonpayable", + type: "function" + } +] as const + +/** + * Default addresses for Biconomy Smart Account + */ +const BICONOMY_ADDRESSES: { + ECDSA_OWNERSHIP_REGISTRY_MODULE: Address + ACCOUNT_V2_0_LOGIC: Address + FACTORY_ADDRESS: Address + DEFAULT_FALLBACK_HANDLER_ADDRESS: Address +} = { + ECDSA_OWNERSHIP_REGISTRY_MODULE: + "0x0000001c5b32F37F5beA87BDD5374eB2aC54eA8e", + ACCOUNT_V2_0_LOGIC: "0x0000002512019Dafb59528B82CB92D3c5D2423aC", + FACTORY_ADDRESS: "0x000000a56Aaca3e9a4C479ea6b6CD0DbcB6634F5", + DEFAULT_FALLBACK_HANDLER_ADDRESS: + "0x0bBa6d96BD616BedC6BFaa341742FD43c60b83C1" +} + +const BICONOMY_PROXY_CREATION_CODE = + "0x6080346100aa57601f61012038819003918201601f19168301916001600160401b038311848410176100af578084926020946040528339810103126100aa57516001600160a01b0381168082036100aa5715610065573055604051605a90816100c68239f35b60405162461bcd60e51b815260206004820152601e60248201527f496e76616c696420696d706c656d656e746174696f6e206164647265737300006044820152606490fd5b600080fd5b634e487b7160e01b600052604160045260246000fdfe608060405230546000808092368280378136915af43d82803e156020573d90f35b3d90fdfea2646970667358221220a03b18dce0be0b4c9afe58a9eb85c35205e2cf087da098bbf1d23945bf89496064736f6c63430008110033" + +/** + * Get the account initialization code for Biconomy smart account with ECDSA as default authorization module + * @param owner + * @param index + * @param factoryAddress + * @param ecdsaValidatorAddress + */ +const getAccountInitCode = async ({ + owner, + index, + ecdsaModuleAddress +}: { + owner: Address + index: bigint + ecdsaModuleAddress: Address +}): Promise => { + if (!owner) throw new Error("Owner account not found") + + // Build the module setup data + const ecdsaOwnershipInitData = encodeFunctionData({ + abi: BiconomyInitAbi, + functionName: "initForSmartAccount", + args: [owner] + }) + + // Build the account init code + return encodeFunctionData({ + abi: createAccountAbi, + functionName: "deployCounterFactualAccount", + args: [ecdsaModuleAddress, ecdsaOwnershipInitData, index] + }) +} + +const getAccountAddress = async ({ + factoryAddress, + accountLogicAddress, + fallbackHandlerAddress, + ecdsaModuleAddress, + owner, + index = BigInt(0) +}: { + factoryAddress: Address + accountLogicAddress: Address + fallbackHandlerAddress: Address + ecdsaModuleAddress: Address + owner: Address + index?: bigint +}): Promise
=> { + // Build the module setup data + const ecdsaOwnershipInitData = encodeFunctionData({ + abi: BiconomyInitAbi, + functionName: "initForSmartAccount", + args: [owner] + }) + + // Build account init code + const initialisationData = encodeFunctionData({ + abi: BiconomyInitAbi, + functionName: "init", + args: [ + fallbackHandlerAddress, + ecdsaModuleAddress, + ecdsaOwnershipInitData + ] + }) + + const deploymentCode = encodePacked( + ["bytes", "uint256"], + [BICONOMY_PROXY_CREATION_CODE, hexToBigInt(accountLogicAddress)] + ) + + const salt = keccak256( + encodePacked( + ["bytes32", "uint256"], + [keccak256(encodePacked(["bytes"], [initialisationData])), index] + ) + ) + + return getContractAddress({ + from: factoryAddress, + salt, + bytecode: deploymentCode, + opcode: "CREATE2" + }) +} + +export type SignerToBiconomySmartAccountParameters< + entryPoint extends ENTRYPOINT_ADDRESS_V06_TYPE, + TSource extends string = string, + TAddress extends Address = Address +> = Prettify<{ + signer: SmartAccountSigner + entryPoint: entryPoint + address?: Address + index?: bigint + factoryAddress?: Address + accountLogicAddress?: Address + fallbackHandlerAddress?: Address + ecdsaModuleAddress?: Address +}> + +/** + * Build a Biconomy modular smart account from a private key, that use the ECDSA signer behind the scene + * @param client + * @param privateKey + * @param entryPoint + * @param index + * @param factoryAddress + * @param accountLogicAddress + * @param ecdsaModuleAddress + */ +export async function signerToBiconomySmartAccount< + entryPoint extends ENTRYPOINT_ADDRESS_V06_TYPE, + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined, + TSource extends string = string, + TAddress extends Address = Address +>( + client: Client, + { + signer, + address, + entryPoint: entryPointAddress, + index = BigInt(0), + factoryAddress = BICONOMY_ADDRESSES.FACTORY_ADDRESS, + accountLogicAddress = BICONOMY_ADDRESSES.ACCOUNT_V2_0_LOGIC, + fallbackHandlerAddress = BICONOMY_ADDRESSES.DEFAULT_FALLBACK_HANDLER_ADDRESS, + ecdsaModuleAddress = BICONOMY_ADDRESSES.ECDSA_OWNERSHIP_REGISTRY_MODULE + }: SignerToBiconomySmartAccountParameters +): Promise> { + const entryPointVersion = getEntryPointVersion(entryPointAddress) + + if (entryPointVersion !== "v0.6") { + throw new Error("Only EntryPoint 0.6 is supported") + } + + // Get the private key related account + const viemSigner: LocalAccount = { + ...signer, + signTransaction: (_, __) => { + throw new SignTransactionNotSupportedBySmartAccount() + } + } as LocalAccount + + // Helper to generate the init code for the smart account + const generateInitCode = () => + getAccountInitCode({ + owner: viemSigner.address, + index, + ecdsaModuleAddress + }) + + // Fetch account address and chain id + const [accountAddress, chainId] = await Promise.all([ + address ?? + getAccountAddress({ + owner: viemSigner.address, + ecdsaModuleAddress, + factoryAddress, + accountLogicAddress, + fallbackHandlerAddress, + index + }), + client.chain?.id ?? getChainId(client) + ]) + + if (!accountAddress) throw new Error("Account address not found") + + let smartAccountDeployed = await isSmartAccountDeployed( + client, + accountAddress + ) + + return toSmartAccount({ + address: accountAddress, + async signMessage({ message }) { + let signature: Hex = await signMessage(client, { + account: viemSigner, + message + }) + const potentiallyIncorrectV = parseInt(signature.slice(-2), 16) + if (![27, 28].includes(potentiallyIncorrectV)) { + const correctV = potentiallyIncorrectV + 27 + signature = (signature.slice(0, -2) + + correctV.toString(16)) as Hex + } + return encodeAbiParameters( + [{ type: "bytes" }, { type: "address" }], + [signature, ecdsaModuleAddress] + ) + }, + async signTransaction(_, __) { + throw new SignTransactionNotSupportedBySmartAccount() + }, + async signTypedData< + const TTypedData extends TypedData | Record, + TPrimaryType extends + | keyof TTypedData + | "EIP712Domain" = keyof TTypedData + >(typedData: TypedDataDefinition) { + let signature: Hex = await signTypedData< + TTypedData, + TPrimaryType, + TChain, + undefined + >(client, { + account: viemSigner, + ...typedData + }) + const potentiallyIncorrectV = parseInt(signature.slice(-2), 16) + if (![27, 28].includes(potentiallyIncorrectV)) { + const correctV = potentiallyIncorrectV + 27 + signature = (signature.slice(0, -2) + + correctV.toString(16)) as Hex + } + return encodeAbiParameters( + [{ type: "bytes" }, { type: "address" }], + [signature, ecdsaModuleAddress] + ) + }, + client: client, + publicKey: accountAddress, + entryPoint: entryPointAddress, + source: "biconomySmartAccount", + + // Get the nonce of the smart account + async getNonce() { + return getAccountNonce(client, { + sender: accountAddress, + entryPoint: entryPointAddress + }) + }, + + // Sign a user operation + async signUserOperation(userOperation) { + const hash = getUserOperationHash({ + userOperation: { + ...userOperation, + signature: "0x" + }, + entryPoint: entryPointAddress, + chainId: chainId + }) + const signature = await signMessage(client, { + account: viemSigner, + message: { raw: hash } + }) + // userOp signature is encoded module signature + module address + const signatureWithModuleAddress = encodeAbiParameters( + parseAbiParameters("bytes, address"), + [signature, ecdsaModuleAddress] + ) + return signatureWithModuleAddress + }, + + async getFactory() { + if (smartAccountDeployed) return undefined + + smartAccountDeployed = await isSmartAccountDeployed( + client, + accountAddress + ) + + if (smartAccountDeployed) return undefined + + return factoryAddress + }, + + async getFactoryData() { + if (smartAccountDeployed) return undefined + + smartAccountDeployed = await isSmartAccountDeployed( + client, + accountAddress + ) + + if (smartAccountDeployed) return undefined + return generateInitCode() + }, + + // Encode the init code + async getInitCode() { + if (smartAccountDeployed) return "0x" + + smartAccountDeployed = await isSmartAccountDeployed( + client, + accountAddress + ) + + if (smartAccountDeployed) return "0x" + + return concatHex([factoryAddress, await generateInitCode()]) + }, + + // Encode the deploy call data + async encodeDeployCallData(_) { + throw new Error("Doesn't support account deployment") + }, + + // Encode a call + async encodeCallData(args) { + if (Array.isArray(args)) { + // Encode a batched call + const argsArray = args as { + to: Address + value: bigint + data: Hex + }[] + + return encodeFunctionData({ + abi: BiconomyExecuteAbi, + functionName: "executeBatch_y6U", + args: [ + argsArray.map((a) => a.to), + argsArray.map((a) => a.value), + argsArray.map((a) => a.data) + ] + }) + } + const { to, value, data } = args as { + to: Address + value: bigint + data: Hex + } + // Encode a simple call + return encodeFunctionData({ + abi: BiconomyExecuteAbi, + functionName: "execute_ncC", + args: [to, value, data] + }) + }, + + // Get simple dummy signature for ECDSA module authorization + async getDummySignature(_userOperation) { + const moduleAddress = + BICONOMY_ADDRESSES.ECDSA_OWNERSHIP_REGISTRY_MODULE + const dynamicPart = moduleAddress.substring(2).padEnd(40, "0") + return `0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000${dynamicPart}000000000000000000000000000000000000000000000000000000000000004181d4b4981670cb18f99f0b4a66446df1bf5b204d24cfcb659bf38ba27a4359b5711649ec2423c5e1247245eba2964679b6a1dbb85c992ae40b9b00c6935b02ff1b00000000000000000000000000000000000000000000000000000000000000` + } + }) +} diff --git a/src/accounts/index.ts b/src/accounts/index.ts new file mode 100644 index 0000000..778ebd3 --- /dev/null +++ b/src/accounts/index.ts @@ -0,0 +1,89 @@ +import { + type PrivateKeyToSimpleSmartAccountParameters, + privateKeyToSimpleSmartAccount +} from "./simple/privateKeyToSimpleSmartAccount" + +import { + type SignerToSimpleSmartAccountParameters, + type SimpleSmartAccount, + signerToSimpleSmartAccount +} from "./simple/signerToSimpleSmartAccount" + +import { + type PrivateKeyToLightSmartAccountParameters, + privateKeyToLightSmartAccount +} from "./light/privateKeyToLightSmartAccount" + +import { + type LightSmartAccount, + type SignerToLightSmartAccountParameters, + signerToLightSmartAccount +} from "./light/signerToLightSmartAccount" + +import { + type PrivateKeyToSafeSmartAccountParameters, + privateKeyToSafeSmartAccount +} from "./safe/privateKeyToSafeSmartAccount" + +import { + type SafeSmartAccount, + type SafeVersion, + type SignerToSafeSmartAccountParameters, + signerToSafeSmartAccount +} from "./safe/signerToSafeSmartAccount" + +import { + type KernelEcdsaSmartAccount, + type SignerToEcdsaKernelSmartAccountParameters, + signerToEcdsaKernelSmartAccount +} from "./kernel/signerToEcdsaKernelSmartAccount" + +import { + type BiconomySmartAccount, + type SignerToBiconomySmartAccountParameters, + signerToBiconomySmartAccount +} from "./biconomy/signerToBiconomySmartAccount" + +import { + type PrivateKeyToBiconomySmartAccountParameters, + privateKeyToBiconomySmartAccount +} from "./biconomy/privateKeyToBiconomySmartAccount" + +import { + SignTransactionNotSupportedBySmartAccount, + type SmartAccount, + type SmartAccountSigner +} from "./types" + +import { toSmartAccount } from "./toSmartAccount" + +export { + type SafeVersion, + type SmartAccountSigner, + type SafeSmartAccount, + signerToSafeSmartAccount, + type SimpleSmartAccount, + signerToSimpleSmartAccount, + type LightSmartAccount, + signerToLightSmartAccount, + SignTransactionNotSupportedBySmartAccount, + privateKeyToBiconomySmartAccount, + privateKeyToSimpleSmartAccount, + privateKeyToLightSmartAccount, + type SmartAccount, + privateKeyToSafeSmartAccount, + type KernelEcdsaSmartAccount, + signerToEcdsaKernelSmartAccount, + type BiconomySmartAccount, + signerToBiconomySmartAccount, + toSmartAccount, + type SignerToSimpleSmartAccountParameters, + type SignerToLightSmartAccountParameters, + type SignerToSafeSmartAccountParameters, + type PrivateKeyToSimpleSmartAccountParameters, + type PrivateKeyToLightSmartAccountParameters, + type PrivateKeyToSafeSmartAccountParameters, + type SignerToEcdsaKernelSmartAccountParameters, + type SignerToBiconomySmartAccountParameters, + type PrivateKeyToBiconomySmartAccountParameters +} diff --git a/src/accounts/kernel/abi/KernelAccountAbi.ts b/src/accounts/kernel/abi/KernelAccountAbi.ts new file mode 100644 index 0000000..7ceb6e7 --- /dev/null +++ b/src/accounts/kernel/abi/KernelAccountAbi.ts @@ -0,0 +1,87 @@ +/** + * The exeute abi, used to execute a transaction on the kernel smart account + */ +export const KernelExecuteAbi = [ + { + inputs: [ + { + internalType: "address", + name: "to", + type: "address" + }, + { + internalType: "uint256", + name: "value", + type: "uint256" + }, + { + internalType: "bytes", + name: "data", + type: "bytes" + }, + { + internalType: "enum Operation", + name: "", + type: "uint8" + } + ], + name: "execute", + outputs: [], + stateMutability: "payable", + type: "function" + }, + { + inputs: [ + { + components: [ + { + internalType: "address", + name: "to", + type: "address" + }, + { + internalType: "uint256", + name: "value", + type: "uint256" + }, + { + internalType: "bytes", + name: "data", + type: "bytes" + } + ], + internalType: "struct Call[]", + name: "calls", + type: "tuple[]" + } + ], + name: "executeBatch", + outputs: [], + stateMutability: "payable", + type: "function" + } +] as const + +/** + * The init abi, used to initialise kernel account + */ +export const KernelInitAbi = [ + { + inputs: [ + { + internalType: "contract IKernelValidator", + name: "_defaultValidator", + type: "address" + }, + { + internalType: "bytes", + name: "_data", + type: "bytes" + } + ], + name: "initialize", + outputs: [], + stateMutability: "payable", + type: "function" + } +] as const diff --git a/src/accounts/kernel/abi/KernelV3AccountAbi.ts b/src/accounts/kernel/abi/KernelV3AccountAbi.ts new file mode 100644 index 0000000..6e0645d --- /dev/null +++ b/src/accounts/kernel/abi/KernelV3AccountAbi.ts @@ -0,0 +1,88 @@ +export const KernelV3InitAbi = [ + { + type: "function", + name: "initialize", + inputs: [ + { + name: "_rootValidator", + type: "bytes21", + internalType: "ValidationId" + }, + { name: "hook", type: "address", internalType: "contract IHook" }, + { name: "validatorData", type: "bytes", internalType: "bytes" }, + { name: "hookData", type: "bytes", internalType: "bytes" } + ], + outputs: [], + stateMutability: "nonpayable" + } +] as const + +export const KernelV3ExecuteAbi = [ + { + type: "function", + name: "execute", + inputs: [ + { name: "execMode", type: "bytes32", internalType: "ExecMode" }, + { name: "executionCalldata", type: "bytes", internalType: "bytes" } + ], + outputs: [], + stateMutability: "payable" + }, + { + type: "function", + name: "executeFromExecutor", + inputs: [ + { name: "execMode", type: "bytes32", internalType: "ExecMode" }, + { name: "executionCalldata", type: "bytes", internalType: "bytes" } + ], + outputs: [ + { name: "returnData", type: "bytes[]", internalType: "bytes[]" } + ], + stateMutability: "payable" + }, + { + type: "function", + name: "executeUserOp", + inputs: [ + { + name: "userOp", + 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" } + ] + }, + { name: "userOpHash", type: "bytes32", internalType: "bytes32" } + ], + outputs: [], + stateMutability: "payable" + } +] as const diff --git a/src/accounts/kernel/abi/KernelV3MetaFactoryAbi.ts b/src/accounts/kernel/abi/KernelV3MetaFactoryAbi.ts new file mode 100644 index 0000000..8358a95 --- /dev/null +++ b/src/accounts/kernel/abi/KernelV3MetaFactoryAbi.ts @@ -0,0 +1,17 @@ +export const KernelV3MetaFactoryDeployWithFactoryAbi = [ + { + type: "function", + name: "deployWithFactory", + inputs: [ + { + name: "factory", + type: "address", + internalType: "contract KernelFactory" + }, + { name: "createData", type: "bytes", internalType: "bytes" }, + { name: "salt", type: "bytes32", internalType: "bytes32" } + ], + outputs: [{ name: "", type: "address", internalType: "address" }], + stateMutability: "payable" + } +] as const diff --git a/src/accounts/kernel/constants.ts b/src/accounts/kernel/constants.ts new file mode 100644 index 0000000..90d7825 --- /dev/null +++ b/src/accounts/kernel/constants.ts @@ -0,0 +1,21 @@ +export const DUMMY_ECDSA_SIGNATURE = + "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c" +export const ROOT_MODE_KERNEL_V2 = "0x00000000" +export enum CALL_TYPE { + SINGLE = "0x00", + BATCH = "0x01", + DELEGATE_CALL = "0xFF" +} +export enum EXEC_TYPE { + DEFAULT = "0x00", + TRY_EXEC = "0x01" +} +export const VALIDATOR_TYPE = { + ROOT: "0x00", + VALIDATOR: "0x01", + PERMISSION: "0x02" +} as const +export enum VALIDATOR_MODE { + DEFAULT = "0x00", + ENABLE = "0x01" +} diff --git a/src/accounts/kernel/signerToEcdsaKernelSmartAccount.ts b/src/accounts/kernel/signerToEcdsaKernelSmartAccount.ts new file mode 100644 index 0000000..bf8b2e3 --- /dev/null +++ b/src/accounts/kernel/signerToEcdsaKernelSmartAccount.ts @@ -0,0 +1,609 @@ +import type { TypedData } from "viem" +import { + type Address, + type Chain, + type Client, + type Hex, + type LocalAccount, + type Transport, + type TypedDataDefinition, + concatHex, + encodeFunctionData, + isAddressEqual, + toHex, + zeroAddress +} from "viem" +import { + getChainId, + readContract, + signMessage as _signMessage +} from "viem/actions" +import { getAccountNonce } from "../../actions/public/getAccountNonce" +import { getSenderAddress } from "../../actions/public/getSenderAddress" +import type { + ENTRYPOINT_ADDRESS_V07_TYPE, + EntryPoint, + Prettify +} from "../../types" +import type { ENTRYPOINT_ADDRESS_V06_TYPE } from "../../types/entrypoint" +import { ENTRYPOINT_ADDRESS_V06, getEntryPointVersion } from "../../utils" +import { getUserOperationHash } from "../../utils/getUserOperationHash" +import { isSmartAccountDeployed } from "../../utils/isSmartAccountDeployed" +import { toSmartAccount } from "../toSmartAccount" +import type { SmartAccount } from "../types" +import { + SignTransactionNotSupportedBySmartAccount, + type SmartAccountSigner +} from "../types" +import { KernelInitAbi } from "./abi/KernelAccountAbi" +import { KernelV3InitAbi } from "./abi/KernelV3AccountAbi" +import { KernelV3MetaFactoryDeployWithFactoryAbi } from "./abi/KernelV3MetaFactoryAbi" +import { + DUMMY_ECDSA_SIGNATURE, + ROOT_MODE_KERNEL_V2, + VALIDATOR_TYPE +} from "./constants" +import { encodeCallData } from "./utils/encodeCallData" +import { getNonceKeyWithEncoding } from "./utils/getNonceKey" +import { signMessage } from "./utils/signMessage" +import { signTypedData } from "./utils/signTypedData" + +export type KernelEcdsaSmartAccount< + entryPoint extends EntryPoint, + transport extends Transport = Transport, + chain extends Chain | undefined = Chain | undefined +> = SmartAccount + +/** + * The account creation ABI for a kernel smart account (from the KernelFactory) + */ +const createAccountAbi = [ + { + inputs: [ + { + internalType: "address", + name: "_implementation", + type: "address" + }, + { + internalType: "bytes", + name: "_data", + type: "bytes" + }, + { + internalType: "uint256", + name: "_index", + type: "uint256" + } + ], + name: "createAccount", + outputs: [ + { + internalType: "address", + name: "proxy", + type: "address" + } + ], + stateMutability: "payable", + type: "function" + } +] as const + +export type KernelVersion = "0.2.2" | "0.3.0-beta" + +/** + * Default addresses map for different kernel smart account versions + */ +export const KERNEL_VERSION_TO_ADDRESSES_MAP: { + [key in KernelVersion]: { + ECDSA_VALIDATOR: Address + ACCOUNT_LOGIC: Address + FACTORY_ADDRESS: Address + META_FACTORY_ADDRESS?: Address + } +} = { + "0.2.2": { + ECDSA_VALIDATOR: "0xd9AB5096a832b9ce79914329DAEE236f8Eea0390", + ACCOUNT_LOGIC: "0x0DA6a956B9488eD4dd761E59f52FDc6c8068E6B5", + FACTORY_ADDRESS: "0x5de4839a76cf55d0c90e2061ef4386d962E15ae3" + }, + "0.3.0-beta": { + ECDSA_VALIDATOR: "0x8104e3Ad430EA6d354d013A6789fDFc71E671c43", + ACCOUNT_LOGIC: "0x94F097E1ebEB4ecA3AAE54cabb08905B239A7D27", + FACTORY_ADDRESS: "0x6723b44Abeec4E71eBE3232BD5B455805baDD22f", + META_FACTORY_ADDRESS: "0xd703aaE79538628d27099B8c4f621bE4CCd142d5" + } +} + +/** + * Get supported Kernel Smart Account version based on entryPoint + * @param entryPoint + */ +const getKernelVersion = (entryPoint: EntryPoint): KernelVersion => { + return entryPoint === ENTRYPOINT_ADDRESS_V06 ? "0.2.2" : "0.3.0-beta" +} + +type KERNEL_ADDRESSES = { + ecdsaValidatorAddress: Address + accountLogicAddress: Address + factoryAddress: Address + metaFactoryAddress: Address +} + +/** + * Get default addresses for Kernel Smart Account based on entryPoint or user input + * @param entryPointAddress + * @param ecdsaValidatorAddress + * @param accountLogicAddress + * @param factoryAddress + * @param metaFactoryAddress + */ +const getDefaultAddresses = ( + entryPointAddress: entryPoint, + { + ecdsaValidatorAddress: _ecdsaValidatorAddress, + accountLogicAddress: _accountLogicAddress, + factoryAddress: _factoryAddress, + metaFactoryAddress: _metaFactoryAddress + }: Partial +): KERNEL_ADDRESSES => { + const kernelVersion = getKernelVersion(entryPointAddress) + const addresses = KERNEL_VERSION_TO_ADDRESSES_MAP[kernelVersion] + const ecdsaValidatorAddress = + _ecdsaValidatorAddress ?? addresses.ECDSA_VALIDATOR + const accountLogicAddress = _accountLogicAddress ?? addresses.ACCOUNT_LOGIC + const factoryAddress = _factoryAddress ?? addresses.FACTORY_ADDRESS + const metaFactoryAddress = + _metaFactoryAddress ?? addresses?.META_FACTORY_ADDRESS ?? zeroAddress // Meta Factory doesn't exists for Kernel v2.2 + + return { + ecdsaValidatorAddress, + accountLogicAddress, + factoryAddress, + metaFactoryAddress + } +} + +export const getEcdsaRootIdentifierForKernelV3 = ( + validatorAddress: Address +) => { + return concatHex([VALIDATOR_TYPE.VALIDATOR, validatorAddress]) +} + +/** + * Get the initialization data for a kernel smart account + * @param entryPoint + * @param owner + * @param ecdsaValidatorAddress + */ +const getInitialisationData = ({ + entryPoint: entryPointAddress, + owner, + ecdsaValidatorAddress +}: { + entryPoint: entryPoint + owner: Address + ecdsaValidatorAddress: Address +}) => { + const entryPointVersion = getEntryPointVersion(entryPointAddress) + + if (entryPointVersion === "v0.6") { + return encodeFunctionData({ + abi: KernelInitAbi, + functionName: "initialize", + args: [ecdsaValidatorAddress, owner] + }) + } + + return encodeFunctionData({ + abi: KernelV3InitAbi, + functionName: "initialize", + args: [ + getEcdsaRootIdentifierForKernelV3(ecdsaValidatorAddress), + zeroAddress /* hookAddress */, + owner, + "0x" /* hookData */ + ] + }) +} + +/** + * Get the account initialization code for a kernel smart account + * @param entryPoint + * @param owner + * @param index + * @param factoryAddress + * @param accountLogicAddress + * @param ecdsaValidatorAddress + */ +const getAccountInitCode = async ({ + entryPoint: entryPointAddress, + owner, + index, + factoryAddress, + accountLogicAddress, + ecdsaValidatorAddress +}: { + entryPoint: entryPoint + owner: Address + index: bigint + factoryAddress: Address + accountLogicAddress: Address + ecdsaValidatorAddress: Address +}): Promise => { + if (!owner) throw new Error("Owner account not found") + const entryPointVersion = getEntryPointVersion(entryPointAddress) + + // Build the account initialization data + const initialisationData = getInitialisationData({ + entryPoint: entryPointAddress, + ecdsaValidatorAddress, + owner + }) + + // Build the account init code + + if (entryPointVersion === "v0.6") { + return encodeFunctionData({ + abi: createAccountAbi, + functionName: "createAccount", + args: [accountLogicAddress, initialisationData, index] + }) + } + + return encodeFunctionData({ + abi: KernelV3MetaFactoryDeployWithFactoryAbi, + functionName: "deployWithFactory", + args: [factoryAddress, initialisationData, toHex(index, { size: 32 })] + }) +} + +/** + * Check the validity of an existing account address, or fetch the pre-deterministic account address for a kernel smart wallet + * @param client + * @param owner + * @param entryPoint + * @param ecdsaValidatorAddress + * @param initCodeProvider + * @param deployedAccountAddress + * @param factoryAddress + */ +const getAccountAddress = async < + entryPoint extends EntryPoint, + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined +>({ + client, + owner, + entryPoint: entryPointAddress, + initCodeProvider, + ecdsaValidatorAddress, + deployedAccountAddress, + factoryAddress +}: { + client: Client + owner: Address + initCodeProvider: () => Promise + factoryAddress: Address + entryPoint: entryPoint + ecdsaValidatorAddress: Address + deployedAccountAddress?: Address +}): Promise
=> { + // If we got an already deployed account, ensure it's well deployed, and the validator & signer are correct + if (deployedAccountAddress !== undefined) { + // Get the owner of the deployed account, ensure it's the same as the owner given in params + const deployedAccountOwner = await readContract(client, { + address: ecdsaValidatorAddress, + abi: [ + { + inputs: [ + { + internalType: "address", + name: "", + type: "address" + } + ], + name: "ecdsaValidatorStorage", + outputs: [ + { + internalType: "address", + name: "owner", + type: "address" + } + ], + stateMutability: "view", + type: "function" + } + ], + functionName: "ecdsaValidatorStorage", + args: [deployedAccountAddress] + }) + + // Ensure the address match + if (!isAddressEqual(deployedAccountOwner, owner)) { + throw new Error("Invalid owner for the already deployed account") + } + + // If ok, return the address + return deployedAccountAddress + } + + // Find the init code for this account + const factoryData = await initCodeProvider() + + const entryPointVersion = getEntryPointVersion(entryPointAddress) + if (entryPointVersion === "v0.6") { + return getSenderAddress(client, { + initCode: concatHex([factoryAddress, factoryData]), + entryPoint: entryPointAddress as ENTRYPOINT_ADDRESS_V06_TYPE + }) + } + return getSenderAddress(client, { + factory: factoryAddress, + factoryData: factoryData, + entryPoint: entryPointAddress as ENTRYPOINT_ADDRESS_V07_TYPE + }) +} + +export type SignerToEcdsaKernelSmartAccountParameters< + entryPoint extends EntryPoint, + TSource extends string = string, + TAddress extends Address = Address +> = Prettify<{ + signer: SmartAccountSigner + entryPoint: entryPoint + address?: Address + index?: bigint + factoryAddress?: Address + metaFactoryAddress?: Address + accountLogicAddress?: Address + ecdsaValidatorAddress?: Address + deployedAccountAddress?: Address +}> +/** + * Build a kernel smart account from a private key, that use the ECDSA signer behind the scene + * @param client + * @param privateKey + * @param entryPoint + * @param index + * @param factoryAddress + * @param accountLogicAddress + * @param ecdsaValidatorAddress + * @param deployedAccountAddress + */ +export async function signerToEcdsaKernelSmartAccount< + entryPoint extends EntryPoint, + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined, + TSource extends string = string, + TAddress extends Address = Address +>( + client: Client, + { + signer, + address, + entryPoint: entryPointAddress, + index = BigInt(0), + factoryAddress: _factoryAddress, + metaFactoryAddress: _metaFactoryAddress, + accountLogicAddress: _accountLogicAddress, + ecdsaValidatorAddress: _ecdsaValidatorAddress, + deployedAccountAddress + }: SignerToEcdsaKernelSmartAccountParameters +): Promise> { + const entryPointVersion = getEntryPointVersion(entryPointAddress) + const kernelVersion = getKernelVersion(entryPointAddress) + + const { + accountLogicAddress, + ecdsaValidatorAddress, + factoryAddress, + metaFactoryAddress + } = getDefaultAddresses(entryPointAddress, { + ecdsaValidatorAddress: _ecdsaValidatorAddress, + accountLogicAddress: _accountLogicAddress, + factoryAddress: _factoryAddress, + metaFactoryAddress: _metaFactoryAddress + }) + + // Get the private key related account + const viemSigner: LocalAccount = { + ...signer, + signTransaction: (_, __) => { + throw new SignTransactionNotSupportedBySmartAccount() + } + } as LocalAccount + + // Helper to generate the init code for the smart account + const generateInitCode = () => + getAccountInitCode({ + entryPoint: entryPointAddress, + owner: viemSigner.address, + index, + factoryAddress, + accountLogicAddress, + ecdsaValidatorAddress + }) + + // Fetch account address and chain id + const [accountAddress, chainId] = await Promise.all([ + address ?? + getAccountAddress({ + client, + entryPoint: entryPointAddress, + owner: viemSigner.address, + ecdsaValidatorAddress, + initCodeProvider: generateInitCode, + deployedAccountAddress, + factoryAddress: + entryPointVersion === "v0.6" + ? factoryAddress + : metaFactoryAddress + }), + client.chain?.id ?? getChainId(client) + ]) + + if (!accountAddress) throw new Error("Account address not found") + + let smartAccountDeployed = await isSmartAccountDeployed( + client, + accountAddress + ) + + return toSmartAccount({ + address: accountAddress, + async signMessage({ message }) { + const signature = await signMessage(client, { + account: viemSigner, + message, + accountAddress, + accountVersion: kernelVersion, + chainId + }) + + if (kernelVersion === "0.2.2") { + return signature + } + + return concatHex([ + getEcdsaRootIdentifierForKernelV3(ecdsaValidatorAddress), + signature + ]) + }, + async signTransaction(_, __) { + throw new SignTransactionNotSupportedBySmartAccount() + }, + async signTypedData< + const TTypedData extends TypedData | Record, + TPrimaryType extends + | keyof TTypedData + | "EIP712Domain" = keyof TTypedData + >(typedData: TypedDataDefinition) { + const signature = await signTypedData< + TTypedData, + TPrimaryType, + TChain, + undefined + >(client, { + account: viemSigner, + ...typedData, + accountAddress, + accountVersion: kernelVersion, + chainId + }) + + if (kernelVersion === "0.2.2") { + return signature + } + + return concatHex([ + getEcdsaRootIdentifierForKernelV3(ecdsaValidatorAddress), + signature + ]) + }, + client: client, + publicKey: accountAddress, + entryPoint: entryPointAddress, + source: "kernelEcdsaSmartAccount", + + // Get the nonce of the smart account + async getNonce() { + const key = getNonceKeyWithEncoding( + kernelVersion, + ecdsaValidatorAddress + // @dev specify the custom nonceKey here when integrating the said feature + /*, nonceKey */ + ) + return getAccountNonce(client, { + sender: accountAddress, + entryPoint: entryPointAddress, + key + }) + }, + + // Sign a user operation + async signUserOperation(userOperation) { + const hash = getUserOperationHash({ + userOperation: { + ...userOperation, + signature: "0x" + }, + entryPoint: entryPointAddress, + chainId: chainId + }) + const signature = await _signMessage(client, { + account: viemSigner, + message: { raw: hash } + }) + // Always use the sudo mode, since we will use external paymaster + if (kernelVersion === "0.2.2") { + return concatHex(["0x00000000", signature]) + } + return signature + }, + + // Encode the init code + async getInitCode() { + if (smartAccountDeployed) return "0x" + + smartAccountDeployed = await isSmartAccountDeployed( + client, + accountAddress + ) + + if (smartAccountDeployed) return "0x" + + const _factoryAddress = + entryPointVersion === "v0.6" + ? factoryAddress + : metaFactoryAddress + return concatHex([_factoryAddress, await generateInitCode()]) + }, + + async getFactory() { + if (smartAccountDeployed) return undefined + + smartAccountDeployed = await isSmartAccountDeployed( + client, + accountAddress + ) + + if (smartAccountDeployed) return undefined + + return entryPointVersion === "v0.6" + ? factoryAddress + : metaFactoryAddress + }, + + async getFactoryData() { + if (smartAccountDeployed) return undefined + + smartAccountDeployed = await isSmartAccountDeployed( + client, + accountAddress + ) + + if (smartAccountDeployed) return undefined + + return generateInitCode() + }, + + // Encode the deploy call data + async encodeDeployCallData(_) { + throw new Error("Simple account doesn't support account deployment") + }, + + // Encode a call + async encodeCallData(_tx) { + return encodeCallData(_tx, kernelVersion) + }, + + // Get simple dummy signature + async getDummySignature(_userOperation) { + if (kernelVersion === "0.2.2") { + return concatHex([ROOT_MODE_KERNEL_V2, DUMMY_ECDSA_SIGNATURE]) + } + return DUMMY_ECDSA_SIGNATURE + } + }) +} diff --git a/src/accounts/kernel/utils/encodeCallData.ts b/src/accounts/kernel/utils/encodeCallData.ts new file mode 100644 index 0000000..626e074 --- /dev/null +++ b/src/accounts/kernel/utils/encodeCallData.ts @@ -0,0 +1,114 @@ +import { + type Address, + type Hex, + concatHex, + encodeAbiParameters, + encodeFunctionData, + toHex +} from "viem" +import { KernelExecuteAbi } from "../abi/KernelAccountAbi" +import { KernelV3ExecuteAbi } from "../abi/KernelV3AccountAbi" +import { CALL_TYPE, EXEC_TYPE } from "../constants" +import { type KernelVersion } from "../signerToEcdsaKernelSmartAccount" +import { getExecMode } from "./getExecMode" + +export const encodeCallData = ( + _tx: + | { + to: Address + value: bigint + data: Hex + } + | { + to: Address + value: bigint + data: Hex + }[], + accountVersion: KernelVersion +) => { + if (accountVersion === "0.2.2") { + if (Array.isArray(_tx)) { + // Encode a batched call + return encodeFunctionData({ + abi: KernelExecuteAbi, + functionName: "executeBatch", + args: [ + _tx.map((tx) => ({ + to: tx.to, + value: tx.value, + data: tx.data + })) + ] + }) + } + // Encode a simple call + return encodeFunctionData({ + abi: KernelExecuteAbi, + functionName: "execute", + args: [_tx.to, _tx.value, _tx.data, 0] + }) + } + if (Array.isArray(_tx)) { + // Encode a batched call + const calldata = encodeAbiParameters( + [ + { + name: "executionBatch", + type: "tuple[]", + components: [ + { + name: "target", + type: "address" + }, + { + name: "value", + type: "uint256" + }, + { + name: "callData", + type: "bytes" + } + ] + } + ], + [ + _tx.map((arg) => { + return { + target: arg.to, + value: arg.value, + callData: arg.data + } + }) + ] + ) + return encodeFunctionData({ + abi: KernelV3ExecuteAbi, + functionName: "execute", + args: [ + getExecMode({ + callType: CALL_TYPE.BATCH, + execType: EXEC_TYPE.DEFAULT + }), + calldata + ] + }) + } + + const calldata = concatHex([ + _tx.to, + toHex(_tx.value, { size: 32 }), + _tx.data + ]) + + return encodeFunctionData({ + abi: KernelV3ExecuteAbi, + functionName: "execute", + args: [ + getExecMode({ + callType: CALL_TYPE.SINGLE, + execType: EXEC_TYPE.DEFAULT + }), + calldata + ] + }) +} diff --git a/src/accounts/kernel/utils/getExecMode.ts b/src/accounts/kernel/utils/getExecMode.ts new file mode 100644 index 0000000..e33faec --- /dev/null +++ b/src/accounts/kernel/utils/getExecMode.ts @@ -0,0 +1,18 @@ +import { type Hex, concatHex, pad } from "viem" +import { CALL_TYPE, EXEC_TYPE } from "../constants" + +export const getExecMode = ({ + callType, + execType +}: { + callType: CALL_TYPE + execType: EXEC_TYPE +}): Hex => { + return concatHex([ + callType, // 1 byte + execType, // 1 byte + "0x00000000", // 4 bytes + "0x00000000", // 4 bytes + pad("0x00000000", { size: 22 }) + ]) +} diff --git a/src/accounts/kernel/utils/getNonceKey.ts b/src/accounts/kernel/utils/getNonceKey.ts new file mode 100644 index 0000000..e5f017f --- /dev/null +++ b/src/accounts/kernel/utils/getNonceKey.ts @@ -0,0 +1,32 @@ +import { type Address, concatHex, maxUint16, pad, toHex } from "viem" +import { VALIDATOR_MODE, VALIDATOR_TYPE } from "../constants" +import { type KernelVersion } from "../signerToEcdsaKernelSmartAccount" + +export const getNonceKeyWithEncoding = ( + accountVerion: KernelVersion, + validatorAddress: Address, + nonceKey = 0n +) => { + if (accountVerion === "0.2.2") { + return nonceKey + } + + if (nonceKey > maxUint16) + throw new Error( + `nonce key must be equal or less than 2 bytes(maxUint16) for Kernel version ${accountVerion}` + ) + + const validatorMode = VALIDATOR_MODE.DEFAULT + const validatorType = VALIDATOR_TYPE.ROOT + const encoding = pad( + concatHex([ + validatorMode, // 1 byte + validatorType, // 1 byte + validatorAddress, // 20 bytes + toHex(nonceKey, { size: 2 }) // 2 byte + ]), + { size: 24 } + ) // 24 bytes + const encodedNonceKey = BigInt(encoding) + return encodedNonceKey +} diff --git a/src/accounts/kernel/utils/signMessage.ts b/src/accounts/kernel/utils/signMessage.ts new file mode 100644 index 0000000..27821ba --- /dev/null +++ b/src/accounts/kernel/utils/signMessage.ts @@ -0,0 +1,48 @@ +import { + type Account, + type Chain, + type Client, + type LocalAccount, + type SignMessageParameters, + type SignMessageReturnType, + type Transport, + hashMessage, + publicActions +} from "viem" +import { signMessage as _signMessage } from "viem/actions" +import { type WrapMessageHashParams, wrapMessageHash } from "./wrapMessageHash" + +export async function signMessage< + TChain extends Chain | undefined, + TAccount extends Account | undefined +>( + client: Client, + { + account: account_ = client.account, + message, + accountAddress, + accountVersion + }: SignMessageParameters & WrapMessageHashParams +): Promise { + if (accountVersion === "0.2.2") { + return _signMessage(client, { + account: account_ as LocalAccount, + message + }) + } + + const wrappedMessageHash = wrapMessageHash(hashMessage(message), { + accountVersion, + accountAddress, + chainId: client.chain + ? client.chain.id + : await client.extend(publicActions).getChainId() + }) + + const signature = await _signMessage(client, { + account: account_ as LocalAccount, + message: { raw: wrappedMessageHash } + }) + + return signature +} diff --git a/src/accounts/kernel/utils/signTypedData.ts b/src/accounts/kernel/utils/signTypedData.ts new file mode 100644 index 0000000..a75f1cc --- /dev/null +++ b/src/accounts/kernel/utils/signTypedData.ts @@ -0,0 +1,74 @@ +import { + type Account, + type Chain, + type Client, + type LocalAccount, + type SignTypedDataParameters, + type SignTypedDataReturnType, + type Transport, + type TypedData, + getTypesForEIP712Domain, + hashTypedData, + publicActions, + validateTypedData +} from "viem" + +import { + signMessage as _signMessage, + signTypedData as _signTypedData +} from "viem/actions" +import { type WrapMessageHashParams, wrapMessageHash } from "./wrapMessageHash" + +export async function signTypedData< + const typedData extends TypedData | Record, + primaryType extends keyof typedData | "EIP712Domain", + chain extends Chain | undefined, + account extends Account | undefined +>( + client: Client, + parameters: SignTypedDataParameters & + WrapMessageHashParams +): Promise { + const { + account: account_, + accountAddress, + accountVersion, + ...typedData + } = parameters as unknown as SignTypedDataParameters & WrapMessageHashParams + if (accountVersion === "0.2.2") { + return _signTypedData(client, { account: account_, ...typedData }) + } + const { message, primaryType, types: _types, domain } = typedData + const types = { + EIP712Domain: getTypesForEIP712Domain({ + domain: domain + }), + ..._types + } + + // Need to do a runtime validation check on addresses, byte ranges, integer ranges, etc + // as we can't statically check this with TypeScript. + validateTypedData({ + domain, + message, + primaryType, + types + }) + + const typedHash = hashTypedData({ message, primaryType, types, domain }) + + const wrappedMessageHash = wrapMessageHash(typedHash, { + accountVersion, + accountAddress, + chainId: client.chain + ? client.chain.id + : await client.extend(publicActions).getChainId() + }) + + const signature = await _signMessage(client, { + account: account_ as LocalAccount, + message: { raw: wrappedMessageHash } + }) + + return signature +} diff --git a/src/accounts/kernel/utils/wrapMessageHash.ts b/src/accounts/kernel/utils/wrapMessageHash.ts new file mode 100644 index 0000000..2d7352f --- /dev/null +++ b/src/accounts/kernel/utils/wrapMessageHash.ts @@ -0,0 +1,38 @@ +import { + type Hex, + concatHex, + encodeAbiParameters, + keccak256, + stringToHex +} from "viem" +import { type Address, domainSeparator } from "viem" + +export type WrapMessageHashParams = { + accountVersion: string + accountAddress: Address + chainId?: number +} + +export const wrapMessageHash = ( + messageHash: Hex, + { accountAddress, accountVersion, chainId }: WrapMessageHashParams +) => { + const _domainSeparator = domainSeparator({ + domain: { + name: "Kernel", + version: accountVersion, + chainId, + verifyingContract: accountAddress + } + }) + const wrappedMessageHash = keccak256( + encodeAbiParameters( + [{ type: "bytes32" }, { type: "bytes32" }], + [keccak256(stringToHex("Kernel(bytes32 hash)")), messageHash] + ) + ) + const digest = keccak256( + concatHex(["0x1901", _domainSeparator, wrappedMessageHash]) + ) + return digest +} diff --git a/src/accounts/light/privateKeyToLightSmartAccount.ts b/src/accounts/light/privateKeyToLightSmartAccount.ts new file mode 100644 index 0000000..6b06f19 --- /dev/null +++ b/src/accounts/light/privateKeyToLightSmartAccount.ts @@ -0,0 +1,37 @@ +import { type Chain, type Client, type Hex, type Transport } from "viem" +import { privateKeyToAccount } from "viem/accounts" +import type { ENTRYPOINT_ADDRESS_V06_TYPE, Prettify } from "../../types/index" +import { + type LightSmartAccount, + type SignerToLightSmartAccountParameters, + signerToLightSmartAccount +} from "./signerToLightSmartAccount" + +export type PrivateKeyToLightSmartAccountParameters< + entryPoint extends ENTRYPOINT_ADDRESS_V06_TYPE +> = Prettify< + { + privateKey: Hex + } & Omit, "signer"> +> + +/** + * @description Creates an Simple Account from a private key. + * + * @returns A Private Key Simple Account. + */ +export async function privateKeyToLightSmartAccount< + entryPoint extends ENTRYPOINT_ADDRESS_V06_TYPE, + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined +>( + client: Client, + { privateKey, ...rest }: PrivateKeyToLightSmartAccountParameters +): Promise> { + const privateKeyAccount = privateKeyToAccount(privateKey) + + return signerToLightSmartAccount(client, { + signer: privateKeyAccount, + ...rest + }) +} diff --git a/src/accounts/light/signerToLightSmartAccount.ts b/src/accounts/light/signerToLightSmartAccount.ts new file mode 100644 index 0000000..e103230 --- /dev/null +++ b/src/accounts/light/signerToLightSmartAccount.ts @@ -0,0 +1,407 @@ +import { + type Address, + type Chain, + type Client, + type Hex, + type LocalAccount, + type Transport, + type TypedData, + type TypedDataDefinition, + concatHex, + encodeFunctionData, + hashMessage, + hashTypedData +} from "viem" +import { getChainId, signMessage } from "viem/actions" +import { getAccountNonce } from "../../actions/public/getAccountNonce" +import { getSenderAddress } from "../../actions/public/getSenderAddress" +import type { + ENTRYPOINT_ADDRESS_V06_TYPE, + ENTRYPOINT_ADDRESS_V07_TYPE, + Prettify +} from "../../types/" +import type { EntryPoint } from "../../types/entrypoint" +import { getEntryPointVersion } from "../../utils" +import { getUserOperationHash } from "../../utils/getUserOperationHash" +import { isSmartAccountDeployed } from "../../utils/isSmartAccountDeployed" +import { toSmartAccount } from "../toSmartAccount" +import { + SignTransactionNotSupportedBySmartAccount, + type SmartAccount, + type SmartAccountSigner +} from "../types" + +export type LightSmartAccount< + entryPoint extends EntryPoint, + transport extends Transport = Transport, + chain extends Chain | undefined = Chain | undefined +> = SmartAccount + +const getAccountInitCode = async ( + owner: Address, + index = BigInt(0) +): Promise => { + if (!owner) throw new Error("Owner account not found") + + return encodeFunctionData({ + abi: [ + { + inputs: [ + { + internalType: "address", + name: "owner", + type: "address" + }, + { + internalType: "uint256", + name: "salt", + type: "uint256" + } + ], + name: "createAccount", + outputs: [ + { + internalType: "contract LightAccount", + name: "ret", + type: "address" + } + ], + stateMutability: "nonpayable", + type: "function" + } + ], + functionName: "createAccount", + args: [owner, index] + }) +} + +const getAccountAddress = async < + entryPoint extends EntryPoint, + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined +>({ + client, + factoryAddress, + entryPoint: entryPointAddress, + owner, + index = BigInt(0) +}: { + client: Client + factoryAddress: Address + owner: Address + entryPoint: entryPoint + index?: bigint +}): Promise
=> { + const entryPointVersion = getEntryPointVersion(entryPointAddress) + + const factoryData = await getAccountInitCode(owner, index) + + if (entryPointVersion === "v0.6") { + return getSenderAddress(client, { + initCode: concatHex([factoryAddress, factoryData]), + entryPoint: entryPointAddress as ENTRYPOINT_ADDRESS_V06_TYPE + }) + } + + // Get the sender address based on the init code + return getSenderAddress(client, { + factory: factoryAddress, + factoryData, + entryPoint: entryPointAddress as ENTRYPOINT_ADDRESS_V07_TYPE + }) +} + +export type LightAccountVersion = "1.1.0" + +export type SignerToLightSmartAccountParameters< + entryPoint extends EntryPoint, + TSource extends string = string, + TAddress extends Address = Address +> = Prettify<{ + signer: SmartAccountSigner + lightAccountVersion: LightAccountVersion + entryPoint: entryPoint + factoryAddress?: Address + index?: bigint + address?: Address +}> + +async function signWith1271WrapperV1< + TSource extends string = string, + TAddress extends Address = Address +>( + signer: SmartAccountSigner, + chainId: number, + accountAddress: Address, + hashedMessage: Hex +): Promise { + return signer.signTypedData({ + domain: { + chainId: Number(chainId), + name: "LightAccount", + verifyingContract: accountAddress, + version: "1" + }, + types: { + LightAccountMessage: [{ name: "message", type: "bytes" }] + }, + message: { + message: hashedMessage + }, + primaryType: "LightAccountMessage" + }) +} + +const LIGHT_VERSION_TO_ADDRESSES_MAP: { + [key in LightAccountVersion]: { + factoryAddress: Address + } +} = { + "1.1.0": { + factoryAddress: "0x00004EC70002a32400f8ae005A26081065620D20" + } +} + +const getDefaultAddresses = ( + lightAccountVersion: LightAccountVersion, + { + factoryAddress: _factoryAddress + }: { + factoryAddress?: Address + } +) => { + const factoryAddress = + _factoryAddress ?? + LIGHT_VERSION_TO_ADDRESSES_MAP[lightAccountVersion].factoryAddress + + return { + factoryAddress + } +} + +/** + * @description Creates an Light Account from a private key. + * + * @returns A Private Key Light Account. + */ +export async function signerToLightSmartAccount< + entryPoint extends EntryPoint, + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined, + TSource extends string = string, + TAddress extends Address = Address +>( + client: Client, + { + signer, + address, + lightAccountVersion, + entryPoint: entryPointAddress, + index = BigInt(0), + factoryAddress: _factoryAddress + }: SignerToLightSmartAccountParameters +): Promise> { + const viemSigner: LocalAccount = { + ...signer, + signTransaction: (_, __) => { + throw new SignTransactionNotSupportedBySmartAccount() + } + } as LocalAccount + + if (lightAccountVersion !== "1.1.0") { + throw new Error( + "Only Light Account version 1.1.0 is supported at the moment" + ) + } + + const { factoryAddress } = getDefaultAddresses(lightAccountVersion, { + factoryAddress: _factoryAddress + }) + + const [accountAddress, chainId] = await Promise.all([ + address ?? + getAccountAddress({ + client, + factoryAddress, + entryPoint: entryPointAddress, + owner: viemSigner.address, + index + }), + client.chain?.id ?? getChainId(client) + ]) + + if (!accountAddress) throw new Error("Account address not found") + + let smartAccountDeployed = await isSmartAccountDeployed( + client, + accountAddress + ) + + return toSmartAccount({ + address: accountAddress, + signMessage: async ({ message }) => { + return signWith1271WrapperV1( + signer, + chainId, + accountAddress, + hashMessage(message) + ) + }, + signTransaction: (_, __) => { + throw new SignTransactionNotSupportedBySmartAccount() + }, + async signTypedData< + const TTypedData extends TypedData | Record, + TPrimaryType extends + | keyof TTypedData + | "EIP712Domain" = keyof TTypedData + >(typedData: TypedDataDefinition) { + return signWith1271WrapperV1( + signer, + chainId, + accountAddress, + hashTypedData(typedData) + ) + }, + client: client, + publicKey: accountAddress, + entryPoint: entryPointAddress, + source: "LightSmartAccount", + async getNonce() { + return getAccountNonce(client, { + sender: accountAddress, + entryPoint: entryPointAddress + }) + }, + async signUserOperation(userOperation) { + return signMessage(client, { + account: viemSigner, + message: { + raw: getUserOperationHash({ + userOperation, + entryPoint: entryPointAddress, + chainId: chainId + }) + } + }) + }, + async getInitCode() { + if (smartAccountDeployed) return "0x" + + smartAccountDeployed = await isSmartAccountDeployed( + client, + accountAddress + ) + + if (smartAccountDeployed) return "0x" + + return concatHex([ + factoryAddress, + await getAccountInitCode(viemSigner.address, index) + ]) + }, + async getFactory() { + if (smartAccountDeployed) return undefined + smartAccountDeployed = await isSmartAccountDeployed( + client, + accountAddress + ) + if (smartAccountDeployed) return undefined + return factoryAddress + }, + async getFactoryData() { + if (smartAccountDeployed) return undefined + smartAccountDeployed = await isSmartAccountDeployed( + client, + accountAddress + ) + if (smartAccountDeployed) return undefined + return getAccountInitCode(viemSigner.address, index) + }, + async encodeDeployCallData(_) { + throw new Error("Light account doesn't support account deployment") + }, + async encodeCallData(args) { + if (Array.isArray(args)) { + const argsArray = args as { + to: Address + value: bigint + data: Hex + }[] + + return encodeFunctionData({ + 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" + } + ], + functionName: "executeBatch", + args: [ + argsArray.map((a) => a.to), + argsArray.map((a) => a.value), + argsArray.map((a) => a.data) + ] + }) + } + + const { to, value, data } = args as { + to: Address + value: bigint + data: Hex + } + + return encodeFunctionData({ + 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" + } + ], + functionName: "execute", + args: [to, value, data] + }) + }, + async getDummySignature(_userOperation) { + return "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c" + } + }) +} diff --git a/src/accounts/safe/privateKeyToSafeSmartAccount.ts b/src/accounts/safe/privateKeyToSafeSmartAccount.ts new file mode 100644 index 0000000..e8fff18 --- /dev/null +++ b/src/accounts/safe/privateKeyToSafeSmartAccount.ts @@ -0,0 +1,37 @@ +import { type Chain, type Client, type Hex, type Transport } from "viem" +import { privateKeyToAccount } from "viem/accounts" +import type { ENTRYPOINT_ADDRESS_V06_TYPE, Prettify } from "../../types" +import { + type SafeSmartAccount, + type SignerToSafeSmartAccountParameters, + signerToSafeSmartAccount +} from "./signerToSafeSmartAccount" + +export type PrivateKeyToSafeSmartAccountParameters< + entryPoint extends ENTRYPOINT_ADDRESS_V06_TYPE +> = Prettify< + { + privateKey: Hex + } & Omit, "signer"> +> + +/** + * @description Creates an Simple Account from a private key. + * + * @returns A Private Key Simple Account. + */ +export async function privateKeyToSafeSmartAccount< + entryPoint extends ENTRYPOINT_ADDRESS_V06_TYPE, + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined +>( + client: Client, + { privateKey, ...rest }: PrivateKeyToSafeSmartAccountParameters +): Promise> { + const privateKeyAccount = privateKeyToAccount(privateKey) + + return signerToSafeSmartAccount(client, { + signer: privateKeyAccount, + ...rest + }) +} diff --git a/src/accounts/safe/signerToSafeSmartAccount.ts b/src/accounts/safe/signerToSafeSmartAccount.ts new file mode 100644 index 0000000..7dc228f --- /dev/null +++ b/src/accounts/safe/signerToSafeSmartAccount.ts @@ -0,0 +1,936 @@ +import { + type Address, + type Chain, + type Client, + type Hex, + type LocalAccount, + type SignableMessage, + type Transport, + type TypedData, + type TypedDataDefinition, + concat, + concatHex, + encodeFunctionData, + encodePacked, + getContractAddress, + hashMessage, + hashTypedData, + hexToBigInt, + keccak256, + pad, + toBytes, + toHex, + zeroAddress +} from "viem" +import { + getChainId, + readContract, + signMessage, + signTypedData +} from "viem/actions" +import { getAccountNonce } from "../../actions/public/getAccountNonce" +import type { + EntryPointVersion, + GetEntryPointVersion, + Prettify +} from "../../types" +import type { EntryPoint, UserOperation } from "../../types" +import { + getEntryPointVersion, + isUserOperationVersion06, + isUserOperationVersion07 +} from "../../utils/getEntryPointVersion" +import { isSmartAccountDeployed } from "../../utils/isSmartAccountDeployed" +import { toSmartAccount } from "../toSmartAccount" +import { + SignTransactionNotSupportedBySmartAccount, + type SmartAccount, + type SmartAccountSigner +} from "../types" + +export type SafeVersion = "1.4.1" + +const EIP712_SAFE_OPERATION_TYPE_V06 = { + SafeOp: [ + { type: "address", name: "safe" }, + { type: "uint256", name: "nonce" }, + { type: "bytes", name: "initCode" }, + { type: "bytes", name: "callData" }, + { type: "uint256", name: "callGasLimit" }, + { type: "uint256", name: "verificationGasLimit" }, + { type: "uint256", name: "preVerificationGas" }, + { type: "uint256", name: "maxFeePerGas" }, + { type: "uint256", name: "maxPriorityFeePerGas" }, + { type: "bytes", name: "paymasterAndData" }, + { type: "uint48", name: "validAfter" }, + { type: "uint48", name: "validUntil" }, + { type: "address", name: "entryPoint" } + ] +} + +const EIP712_SAFE_OPERATION_TYPE_V07 = { + SafeOp: [ + { type: "address", name: "safe" }, + { type: "uint256", name: "nonce" }, + { type: "bytes", name: "initCode" }, + { type: "bytes", name: "callData" }, + { type: "uint128", name: "verificationGasLimit" }, + { type: "uint128", name: "callGasLimit" }, + { type: "uint256", name: "preVerificationGas" }, + { type: "uint128", name: "maxPriorityFeePerGas" }, + { type: "uint128", name: "maxFeePerGas" }, + { type: "bytes", name: "paymasterAndData" }, + { type: "uint48", name: "validAfter" }, + { type: "uint48", name: "validUntil" }, + { type: "address", name: "entryPoint" } + ] +} + +const SAFE_VERSION_TO_ADDRESSES_MAP: { + [key in SafeVersion]: { + [key in EntryPointVersion]: { + SAFE_MODULE_SETUP_ADDRESS: Address + SAFE_4337_MODULE_ADDRESS: Address + SAFE_PROXY_FACTORY_ADDRESS: Address + SAFE_SINGLETON_ADDRESS: Address + MULTI_SEND_ADDRESS: Address + MULTI_SEND_CALL_ONLY_ADDRESS: Address + } + } +} = { + "1.4.1": { + "v0.6": { + SAFE_MODULE_SETUP_ADDRESS: + "0x8EcD4ec46D4D2a6B64fE960B3D64e8B94B2234eb", + SAFE_4337_MODULE_ADDRESS: + "0xa581c4A4DB7175302464fF3C06380BC3270b4037", + SAFE_PROXY_FACTORY_ADDRESS: + "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", + SAFE_SINGLETON_ADDRESS: + "0x41675C099F32341bf84BFc5382aF534df5C7461a", + MULTI_SEND_ADDRESS: "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", + MULTI_SEND_CALL_ONLY_ADDRESS: + "0x9641d764fc13c8B624c04430C7356C1C7C8102e2" + }, + "v0.7": { + SAFE_MODULE_SETUP_ADDRESS: + "0x2dd68b007B46fBe91B9A7c3EDa5A7a1063cB5b47", + SAFE_4337_MODULE_ADDRESS: + "0x75cf11467937ce3F2f357CE24ffc3DBF8fD5c226", + SAFE_PROXY_FACTORY_ADDRESS: + "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", + SAFE_SINGLETON_ADDRESS: + "0x41675C099F32341bf84BFc5382aF534df5C7461a", + MULTI_SEND_ADDRESS: "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526", + MULTI_SEND_CALL_ONLY_ADDRESS: + "0x9641d764fc13c8B624c04430C7356C1C7C8102e2" + } + } +} + +const adjustVInSignature = ( + signingMethod: "eth_sign" | "eth_signTypedData", + signature: string +): Hex => { + const ETHEREUM_V_VALUES = [0, 1, 27, 28] + const MIN_VALID_V_VALUE_FOR_SAFE_ECDSA = 27 + let signatureV = parseInt(signature.slice(-2), 16) + if (!ETHEREUM_V_VALUES.includes(signatureV)) { + throw new Error("Invalid signature") + } + if (signingMethod === "eth_sign") { + if (signatureV < MIN_VALID_V_VALUE_FOR_SAFE_ECDSA) { + signatureV += MIN_VALID_V_VALUE_FOR_SAFE_ECDSA + } + signatureV += 4 + } + if (signingMethod === "eth_signTypedData") { + if (signatureV < MIN_VALID_V_VALUE_FOR_SAFE_ECDSA) { + signatureV += MIN_VALID_V_VALUE_FOR_SAFE_ECDSA + } + } + return (signature.slice(0, -2) + signatureV.toString(16)) as Hex +} + +const generateSafeMessageMessage = < + const TTypedData extends TypedData | { [key: string]: unknown }, + TPrimaryType extends keyof TTypedData | "EIP712Domain" = keyof TTypedData +>( + message: SignableMessage | TypedDataDefinition +): Hex => { + const signableMessage = message as SignableMessage + + if (typeof signableMessage === "string" || signableMessage.raw) { + return hashMessage(signableMessage) + } + + return hashTypedData( + message as TypedDataDefinition + ) +} + +const encodeInternalTransaction = (tx: { + to: Address + data: Address + value: bigint + operation: 0 | 1 +}): string => { + const encoded = encodePacked( + ["uint8", "address", "uint256", "uint256", "bytes"], + [ + tx.operation, + tx.to, + tx.value, + BigInt(tx.data.slice(2).length / 2), + tx.data + ] + ) + return encoded.slice(2) +} + +const encodeMultiSend = ( + txs: { + to: Address + data: Address + value: bigint + operation: 0 | 1 + }[] +): `0x${string}` => { + const data: `0x${string}` = `0x${txs + .map((tx) => encodeInternalTransaction(tx)) + .join("")}` + + return encodeFunctionData({ + abi: [ + { + inputs: [ + { + internalType: "bytes", + name: "transactions", + type: "bytes" + } + ], + name: "multiSend", + outputs: [], + stateMutability: "payable", + type: "function" + } + ], + functionName: "multiSend", + args: [data] + }) +} + +export type SafeSmartAccount< + entryPoint extends EntryPoint, + transport extends Transport = Transport, + chain extends Chain | undefined = Chain | undefined +> = SmartAccount + +const getInitializerCode = async ({ + owner, + safeModuleSetupAddress, + safe4337ModuleAddress, + multiSendAddress, + setupTransactions = [], + safeModules = [] +}: { + owner: Address + safeModuleSetupAddress: Address + safe4337ModuleAddress: Address + multiSendAddress: Address + setupTransactions?: { + to: Address + data: Address + value: bigint + }[] + safeModules?: Address[] +}) => { + const multiSendCallData = encodeMultiSend([ + { + to: safeModuleSetupAddress, + data: encodeFunctionData({ + abi: [ + { + inputs: [ + { + internalType: "address[]", + name: "modules", + type: "address[]" + } + ], + name: "enableModules", + outputs: [], + stateMutability: "nonpayable", + type: "function" + } + ], + functionName: "enableModules", + args: [[safe4337ModuleAddress, ...safeModules]] + }), + value: BigInt(0), + operation: 1 + }, + ...setupTransactions.map((tx) => ({ ...tx, operation: 0 as 0 | 1 })) + ]) + + return encodeFunctionData({ + abi: [ + { + inputs: [ + { + internalType: "address[]", + name: "_owners", + type: "address[]" + }, + { + internalType: "uint256", + name: "_threshold", + type: "uint256" + }, + { + internalType: "address", + name: "to", + type: "address" + }, + { + internalType: "bytes", + name: "data", + type: "bytes" + }, + { + internalType: "address", + name: "fallbackHandler", + type: "address" + }, + { + internalType: "address", + name: "paymentToken", + type: "address" + }, + { + internalType: "uint256", + name: "payment", + type: "uint256" + }, + { + internalType: "address payable", + name: "paymentReceiver", + type: "address" + } + ], + name: "setup", + outputs: [], + stateMutability: "nonpayable", + type: "function" + } + ], + functionName: "setup", + args: [ + [owner], + BigInt(1), + multiSendAddress, + multiSendCallData, + safe4337ModuleAddress, + zeroAddress, + BigInt(0), + zeroAddress + ] + }) +} + +function getPaymasterAndData(unpackedUserOperation: UserOperation<"v0.7">) { + return unpackedUserOperation.paymaster + ? concat([ + unpackedUserOperation.paymaster, + pad( + toHex( + unpackedUserOperation.paymasterVerificationGasLimit || + BigInt(0) + ), + { + size: 16 + } + ), + pad( + toHex( + unpackedUserOperation.paymasterPostOpGasLimit || BigInt(0) + ), + { + size: 16 + } + ), + unpackedUserOperation.paymasterData || ("0x" as Hex) + ]) + : "0x" +} + +const getAccountInitCode = async ({ + owner, + safeModuleSetupAddress, + safe4337ModuleAddress, + safeSingletonAddress, + multiSendAddress, + saltNonce = BigInt(0), + setupTransactions = [], + safeModules = [] +}: { + owner: Address + safeModuleSetupAddress: Address + safe4337ModuleAddress: Address + safeSingletonAddress: Address + multiSendAddress: Address + saltNonce?: bigint + setupTransactions?: { + to: Address + data: Address + value: bigint + }[] + safeModules?: Address[] +}): Promise => { + if (!owner) throw new Error("Owner account not found") + const initializer = await getInitializerCode({ + owner, + safeModuleSetupAddress, + safe4337ModuleAddress, + multiSendAddress, + setupTransactions, + safeModules + }) + + const initCodeCallData = encodeFunctionData({ + abi: [ + { + inputs: [ + { + internalType: "address", + name: "_singleton", + type: "address" + }, + { + internalType: "bytes", + name: "initializer", + type: "bytes" + }, + { + internalType: "uint256", + name: "saltNonce", + type: "uint256" + } + ], + name: "createProxyWithNonce", + outputs: [ + { + internalType: "contract SafeProxy", + name: "proxy", + type: "address" + } + ], + stateMutability: "nonpayable", + type: "function" + } + ], + functionName: "createProxyWithNonce", + args: [safeSingletonAddress, initializer, saltNonce] + }) + + return initCodeCallData +} + +const getAccountAddress = async < + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined +>({ + client, + owner, + safeModuleSetupAddress, + safe4337ModuleAddress, + safeProxyFactoryAddress, + safeSingletonAddress, + multiSendAddress, + setupTransactions = [], + safeModules = [], + saltNonce = BigInt(0) +}: { + client: Client + owner: Address + safeModuleSetupAddress: Address + safe4337ModuleAddress: Address + safeProxyFactoryAddress: Address + safeSingletonAddress: Address + multiSendAddress: Address + setupTransactions: { + to: Address + data: Address + value: bigint + }[] + safeModules?: Address[] + saltNonce?: bigint +}): Promise
=> { + const proxyCreationCode = await readContract(client, { + abi: [ + { + inputs: [], + name: "proxyCreationCode", + outputs: [ + { + internalType: "bytes", + name: "", + type: "bytes" + } + ], + stateMutability: "pure", + type: "function" + } + ], + address: safeProxyFactoryAddress, + functionName: "proxyCreationCode" + }) + + const deploymentCode = encodePacked( + ["bytes", "uint256"], + [proxyCreationCode, hexToBigInt(safeSingletonAddress)] + ) + + const initializer = await getInitializerCode({ + owner, + safeModuleSetupAddress, + safe4337ModuleAddress, + multiSendAddress, + setupTransactions, + safeModules + }) + + const salt = keccak256( + encodePacked( + ["bytes32", "uint256"], + [keccak256(encodePacked(["bytes"], [initializer])), saltNonce] + ) + ) + + return getContractAddress({ + from: safeProxyFactoryAddress, + salt, + bytecode: deploymentCode, + opcode: "CREATE2" + }) +} + +const getDefaultAddresses = ( + safeVersion: SafeVersion, + entryPointAddress: EntryPoint, + { + addModuleLibAddress: _addModuleLibAddress, + safeModuleSetupAddress: _safeModuleSetupAddress, + safe4337ModuleAddress: _safe4337ModuleAddress, + safeProxyFactoryAddress: _safeProxyFactoryAddress, + safeSingletonAddress: _safeSingletonAddress, + multiSendAddress: _multiSendAddress, + multiSendCallOnlyAddress: _multiSendCallOnlyAddress + }: { + addModuleLibAddress?: Address + safeModuleSetupAddress?: Address + safe4337ModuleAddress?: Address + safeProxyFactoryAddress?: Address + safeSingletonAddress?: Address + multiSendAddress?: Address + multiSendCallOnlyAddress?: Address + } +) => { + const entryPointVersion = getEntryPointVersion(entryPointAddress) + + const safeModuleSetupAddress = + _safeModuleSetupAddress ?? + _addModuleLibAddress ?? + SAFE_VERSION_TO_ADDRESSES_MAP[safeVersion][entryPointVersion] + .SAFE_MODULE_SETUP_ADDRESS + const safe4337ModuleAddress = + _safe4337ModuleAddress ?? + SAFE_VERSION_TO_ADDRESSES_MAP[safeVersion][entryPointVersion] + .SAFE_4337_MODULE_ADDRESS + const safeProxyFactoryAddress = + _safeProxyFactoryAddress ?? + SAFE_VERSION_TO_ADDRESSES_MAP[safeVersion][entryPointVersion] + .SAFE_PROXY_FACTORY_ADDRESS + const safeSingletonAddress = + _safeSingletonAddress ?? + SAFE_VERSION_TO_ADDRESSES_MAP[safeVersion][entryPointVersion] + .SAFE_SINGLETON_ADDRESS + const multiSendAddress = + _multiSendAddress ?? + SAFE_VERSION_TO_ADDRESSES_MAP[safeVersion][entryPointVersion] + .MULTI_SEND_ADDRESS + + const multiSendCallOnlyAddress = + _multiSendCallOnlyAddress ?? + SAFE_VERSION_TO_ADDRESSES_MAP[safeVersion][ + getEntryPointVersion(entryPointAddress) + ].MULTI_SEND_CALL_ONLY_ADDRESS + + return { + safeModuleSetupAddress, + safe4337ModuleAddress, + safeProxyFactoryAddress, + safeSingletonAddress, + multiSendAddress, + multiSendCallOnlyAddress + } +} + +export type SignerToSafeSmartAccountParameters< + entryPoint extends EntryPoint, + TSource extends string = string, + TAddress extends Address = Address +> = Prettify<{ + signer: SmartAccountSigner + safeVersion: SafeVersion + entryPoint: entryPoint + address?: Address + safeModuleSetupAddress?: Address + safe4337ModuleAddress?: Address + safeProxyFactoryAddress?: Address + safeSingletonAddress?: Address + multiSendAddress?: Address + multiSendCallOnlyAddress?: Address + saltNonce?: bigint + validUntil?: number + validAfter?: number + setupTransactions?: { + to: Address + data: Address + value: bigint + }[] + safeModules?: Address[] +}> + +/** + * @description Creates an Simple Account from a private key. + * + * @returns A Private Key Simple Account. + */ +export async function signerToSafeSmartAccount< + entryPoint extends EntryPoint, + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined, + TSource extends string = string, + TAddress extends Address = Address +>( + client: Client, + { + signer, + address, + safeVersion, + entryPoint: entryPointAddress, + safeModuleSetupAddress: _safeModuleSetupAddress, + safe4337ModuleAddress: _safe4337ModuleAddress, + safeProxyFactoryAddress: _safeProxyFactoryAddress, + safeSingletonAddress: _safeSingletonAddress, + multiSendAddress: _multiSendAddress, + multiSendCallOnlyAddress: _multiSendCallOnlyAddress, + saltNonce = BigInt(0), + validUntil = 0, + validAfter = 0, + safeModules = [], + setupTransactions = [] + }: SignerToSafeSmartAccountParameters +): Promise> { + const chainId = client.chain?.id ?? (await getChainId(client)) + + const viemSigner: LocalAccount = { + ...signer, + signTransaction: (_, __) => { + throw new SignTransactionNotSupportedBySmartAccount() + } + } as LocalAccount + + const { + safeModuleSetupAddress, + safe4337ModuleAddress, + safeProxyFactoryAddress, + safeSingletonAddress, + multiSendAddress, + multiSendCallOnlyAddress + } = getDefaultAddresses(safeVersion, entryPointAddress, { + safeModuleSetupAddress: _safeModuleSetupAddress, + safe4337ModuleAddress: _safe4337ModuleAddress, + safeProxyFactoryAddress: _safeProxyFactoryAddress, + safeSingletonAddress: _safeSingletonAddress, + multiSendAddress: _multiSendAddress, + multiSendCallOnlyAddress: _multiSendCallOnlyAddress + }) + + const accountAddress = + address ?? + (await getAccountAddress({ + client, + owner: viemSigner.address, + safeModuleSetupAddress, + safe4337ModuleAddress, + safeProxyFactoryAddress, + safeSingletonAddress, + multiSendAddress, + saltNonce, + setupTransactions, + safeModules + })) + + if (!accountAddress) throw new Error("Account address not found") + + let safeDeployed = await isSmartAccountDeployed(client, accountAddress) + + const safeSmartAccount: SafeSmartAccount = + toSmartAccount({ + address: accountAddress, + async signMessage({ message }) { + const messageHash = hashTypedData({ + domain: { + chainId: chainId, + verifyingContract: accountAddress + }, + types: { + SafeMessage: [{ name: "message", type: "bytes" }] + }, + primaryType: "SafeMessage", + message: { + message: generateSafeMessageMessage(message) + } + }) + + return adjustVInSignature( + "eth_sign", + await signMessage(client, { + account: viemSigner, + message: { + raw: toBytes(messageHash) + } + }) + ) + }, + async signTransaction(_, __) { + throw new SignTransactionNotSupportedBySmartAccount() + }, + async signTypedData< + const TTypedData extends TypedData | Record, + TPrimaryType extends + | keyof TTypedData + | "EIP712Domain" = keyof TTypedData + >(typedData: TypedDataDefinition) { + return adjustVInSignature( + "eth_signTypedData", + await signTypedData(client, { + account: viemSigner, + domain: { + chainId: chainId, + verifyingContract: accountAddress + }, + types: { + SafeMessage: [{ name: "message", type: "bytes" }] + }, + primaryType: "SafeMessage", + message: { + message: generateSafeMessageMessage< + TTypedData, + TPrimaryType + >(typedData) + } + }) + ) + }, + client: client, + publicKey: accountAddress, + entryPoint: entryPointAddress, + source: "SafeSmartAccount", + async getNonce() { + return getAccountNonce(client, { + sender: accountAddress, + entryPoint: entryPointAddress + }) + }, + async signUserOperation( + userOperation: UserOperation> + ) { + const message = { + safe: accountAddress, + callData: userOperation.callData, + nonce: userOperation.nonce, + initCode: userOperation.initCode ?? "0x", + maxFeePerGas: userOperation.maxFeePerGas, + maxPriorityFeePerGas: userOperation.maxPriorityFeePerGas, + preVerificationGas: userOperation.preVerificationGas, + verificationGasLimit: userOperation.verificationGasLimit, + callGasLimit: userOperation.callGasLimit, + paymasterAndData: userOperation.paymasterAndData ?? "0x", + validAfter: validAfter, + validUntil: validUntil, + entryPoint: entryPointAddress + } + + if ( + isUserOperationVersion06(entryPointAddress, userOperation) + ) { + message.paymasterAndData = userOperation.paymasterAndData + } + + if ( + isUserOperationVersion07(entryPointAddress, userOperation) + ) { + if (userOperation.factory && userOperation.factoryData) { + message.initCode = concatHex([ + userOperation.factory, + userOperation.factoryData + ]) + } + message.paymasterAndData = + getPaymasterAndData(userOperation) + } + + const signatures = [ + { + signer: viemSigner.address, + data: await signTypedData(client, { + account: viemSigner, + domain: { + chainId: chainId, + verifyingContract: safe4337ModuleAddress + }, + types: + getEntryPointVersion(entryPointAddress) === + "v0.6" + ? EIP712_SAFE_OPERATION_TYPE_V06 + : EIP712_SAFE_OPERATION_TYPE_V07, + primaryType: "SafeOp", + message: message + }) + } + ] + + signatures.sort((left, right) => + left.signer + .toLowerCase() + .localeCompare(right.signer.toLowerCase()) + ) + + const signatureBytes = concat(signatures.map((sig) => sig.data)) + + return encodePacked( + ["uint48", "uint48", "bytes"], + [validAfter, validUntil, signatureBytes] + ) + }, + async getInitCode() { + safeDeployed = + safeDeployed || + (await isSmartAccountDeployed(client, accountAddress)) + + if (safeDeployed) return "0x" + + return concatHex([ + (await this.getFactory()) ?? "0x", + (await this.getFactoryData()) ?? "0x" + ]) + }, + async getFactory() { + safeDeployed = + safeDeployed || + (await isSmartAccountDeployed(client, accountAddress)) + + if (safeDeployed) return undefined + + return safeProxyFactoryAddress + }, + async getFactoryData() { + safeDeployed = + safeDeployed || + (await isSmartAccountDeployed(client, accountAddress)) + + if (safeDeployed) return undefined + + return await getAccountInitCode({ + owner: viemSigner.address, + safeModuleSetupAddress, + safe4337ModuleAddress, + safeSingletonAddress, + multiSendAddress, + saltNonce, + setupTransactions, + safeModules + }) + }, + async encodeDeployCallData(_) { + throw new Error( + "Safe account doesn't support account deployment" + ) + }, + async encodeCallData(args) { + let to: Address + let value: bigint + let data: Hex + let operationType = 0 + + if (Array.isArray(args)) { + const argsArray = args as { + to: Address + value: bigint + data: Hex + }[] + + to = multiSendCallOnlyAddress + value = BigInt(0) + + data = encodeMultiSend( + argsArray.map((tx) => ({ ...tx, operation: 0 })) + ) + operationType = 1 + } else { + const singleTransaction = args as { + to: Address + value: bigint + data: Hex + } + to = singleTransaction.to + data = singleTransaction.data + value = singleTransaction.value + } + + return encodeFunctionData({ + abi: [ + { + inputs: [ + { + internalType: "address", + name: "to", + type: "address" + }, + { + internalType: "uint256", + name: "value", + type: "uint256" + }, + { + internalType: "bytes", + name: "data", + type: "bytes" + }, + { + internalType: "uint8", + name: "operation", + type: "uint8" + } + ], + name: "executeUserOpWithErrorString", + outputs: [], + stateMutability: "nonpayable", + type: "function" + } + ], + functionName: "executeUserOpWithErrorString", + args: [to, value, data, operationType] + }) + }, + async getDummySignature(_userOperation) { + return "0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + } + }) + + return safeSmartAccount +} diff --git a/src/accounts/simple/privateKeyToSimpleSmartAccount.ts b/src/accounts/simple/privateKeyToSimpleSmartAccount.ts new file mode 100644 index 0000000..856b5c8 --- /dev/null +++ b/src/accounts/simple/privateKeyToSimpleSmartAccount.ts @@ -0,0 +1,52 @@ +import { + type Address, + type Chain, + type Client, + type Hex, + type Transport +} from "viem" +import { privateKeyToAccount } from "viem/accounts" +import type { EntryPoint, Prettify } from "../../types/" +import { + type SignerToSimpleSmartAccountParameters, + type SimpleSmartAccount, + signerToSimpleSmartAccount +} from "./signerToSimpleSmartAccount" + +export type PrivateKeyToSimpleSmartAccountParameters< + entryPoint extends EntryPoint +> = Prettify< + { + privateKey: Hex + } & Omit, "signer"> +> + +/** + * @description Creates an Simple Account from a private key. + * + * @returns A Private Key Simple Account. + */ +export async function privateKeyToSimpleSmartAccount< + entryPoint extends EntryPoint, + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined +>( + client: Client, + { + privateKey, + ...rest + }: PrivateKeyToSimpleSmartAccountParameters +): Promise> { + const privateKeyAccount = privateKeyToAccount(privateKey) + + return signerToSimpleSmartAccount< + entryPoint, + TTransport, + TChain, + "privateKey", + Address + >(client, { + signer: privateKeyAccount, + ...rest + }) +} diff --git a/src/accounts/simple/signerToSimpleSmartAccount.ts b/src/accounts/simple/signerToSimpleSmartAccount.ts new file mode 100644 index 0000000..f7103bf --- /dev/null +++ b/src/accounts/simple/signerToSimpleSmartAccount.ts @@ -0,0 +1,350 @@ +import { + type Address, + type Chain, + type Client, + type Hex, + type LocalAccount, + type Transport, + concatHex, + encodeFunctionData +} from "viem" +import { getChainId, signMessage } from "viem/actions" +import { getAccountNonce } from "../../actions/public/getAccountNonce" +import { getSenderAddress } from "../../actions/public/getSenderAddress" +import type { + ENTRYPOINT_ADDRESS_V06_TYPE, + ENTRYPOINT_ADDRESS_V07_TYPE, + Prettify +} from "../../types/" +import type { EntryPoint } from "../../types/entrypoint" +import { getEntryPointVersion } from "../../utils" +import { getUserOperationHash } from "../../utils/getUserOperationHash" +import { isSmartAccountDeployed } from "../../utils/isSmartAccountDeployed" +import { toSmartAccount } from "../toSmartAccount" +import { + SignTransactionNotSupportedBySmartAccount, + type SmartAccount, + type SmartAccountSigner +} from "../types" + +export type SimpleSmartAccount< + entryPoint extends EntryPoint, + transport extends Transport = Transport, + chain extends Chain | undefined = Chain | undefined +> = SmartAccount + +const getAccountInitCode = async ( + owner: Address, + index = BigInt(0) +): Promise => { + if (!owner) throw new Error("Owner account not found") + + return encodeFunctionData({ + abi: [ + { + inputs: [ + { + internalType: "address", + name: "owner", + type: "address" + }, + { + internalType: "uint256", + name: "salt", + type: "uint256" + } + ], + name: "createAccount", + outputs: [ + { + internalType: "contract SimpleAccount", + name: "ret", + type: "address" + } + ], + stateMutability: "nonpayable", + type: "function" + } + ], + functionName: "createAccount", + args: [owner, index] + }) +} + +const getAccountAddress = async < + entryPoint extends EntryPoint, + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined +>({ + client, + factoryAddress, + entryPoint: entryPointAddress, + owner, + index = BigInt(0) +}: { + client: Client + factoryAddress: Address + owner: Address + entryPoint: entryPoint + index?: bigint +}): Promise
=> { + const entryPointVersion = getEntryPointVersion(entryPointAddress) + + const factoryData = await getAccountInitCode(owner, index) + + if (entryPointVersion === "v0.6") { + return getSenderAddress(client, { + initCode: concatHex([factoryAddress, factoryData]), + entryPoint: entryPointAddress as ENTRYPOINT_ADDRESS_V06_TYPE + }) + } + + // Get the sender address based on the init code + return getSenderAddress(client, { + factory: factoryAddress, + factoryData, + entryPoint: entryPointAddress as ENTRYPOINT_ADDRESS_V07_TYPE + }) +} + +export type SignerToSimpleSmartAccountParameters< + entryPoint extends EntryPoint, + TSource extends string = string, + TAddress extends Address = Address +> = Prettify<{ + signer: SmartAccountSigner + factoryAddress: Address + entryPoint: entryPoint + index?: bigint + address?: Address +}> + +/** + * @description Creates an Simple Account from a private key. + * + * @returns A Private Key Simple Account. + */ +export async function signerToSimpleSmartAccount< + entryPoint extends EntryPoint, + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined, + TSource extends string = string, + TAddress extends Address = Address +>( + client: Client, + { + signer, + factoryAddress, + entryPoint: entryPointAddress, + index = BigInt(0), + address + }: SignerToSimpleSmartAccountParameters +): Promise> { + const viemSigner: LocalAccount = { + ...signer, + signTransaction: (_, __) => { + throw new SignTransactionNotSupportedBySmartAccount() + } + } as LocalAccount + + const [accountAddress, chainId] = await Promise.all([ + address ?? + getAccountAddress({ + client, + factoryAddress, + entryPoint: entryPointAddress, + owner: viemSigner.address, + index + }), + client.chain?.id ?? getChainId(client) + ]) + + if (!accountAddress) throw new Error("Account address not found") + + let smartAccountDeployed = await isSmartAccountDeployed( + client, + accountAddress + ) + + return toSmartAccount({ + address: accountAddress, + signMessage: async (_) => { + throw new Error("Simple account isn't 1271 compliant") + }, + signTransaction: (_, __) => { + throw new SignTransactionNotSupportedBySmartAccount() + }, + signTypedData: async (_) => { + throw new Error("Simple account isn't 1271 compliant") + }, + client: client, + publicKey: accountAddress, + entryPoint: entryPointAddress, + source: "SimpleSmartAccount", + async getNonce() { + return getAccountNonce(client, { + sender: accountAddress, + entryPoint: entryPointAddress + }) + }, + async signUserOperation(userOperation) { + return signMessage(client, { + account: viemSigner, + message: { + raw: getUserOperationHash({ + userOperation, + entryPoint: entryPointAddress, + chainId: chainId + }) + } + }) + }, + async getInitCode() { + if (smartAccountDeployed) return "0x" + + smartAccountDeployed = await isSmartAccountDeployed( + client, + accountAddress + ) + + if (smartAccountDeployed) return "0x" + + return concatHex([ + factoryAddress, + await getAccountInitCode(viemSigner.address, index) + ]) + }, + async getFactory() { + if (smartAccountDeployed) return undefined + smartAccountDeployed = await isSmartAccountDeployed( + client, + accountAddress + ) + if (smartAccountDeployed) return undefined + return factoryAddress + }, + async getFactoryData() { + if (smartAccountDeployed) return undefined + smartAccountDeployed = await isSmartAccountDeployed( + client, + accountAddress + ) + if (smartAccountDeployed) return undefined + return getAccountInitCode(viemSigner.address, index) + }, + async encodeDeployCallData(_) { + throw new Error("Simple account doesn't support account deployment") + }, + async encodeCallData(args) { + if (Array.isArray(args)) { + const argsArray = args as { + to: Address + value: bigint + data: Hex + }[] + + if (getEntryPointVersion(entryPointAddress) === "v0.6") { + return encodeFunctionData({ + abi: [ + { + inputs: [ + { + internalType: "address[]", + name: "dest", + type: "address[]" + }, + { + internalType: "bytes[]", + name: "func", + type: "bytes[]" + } + ], + name: "executeBatch", + outputs: [], + stateMutability: "nonpayable", + type: "function" + } + ], + functionName: "executeBatch", + args: [ + argsArray.map((a) => a.to), + argsArray.map((a) => a.data) + ] + }) + } + return encodeFunctionData({ + 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" + } + ], + functionName: "executeBatch", + args: [ + argsArray.map((a) => a.to), + argsArray.map((a) => a.value), + argsArray.map((a) => a.data) + ] + }) + } + + const { to, value, data } = args as { + to: Address + value: bigint + data: Hex + } + + return encodeFunctionData({ + 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" + } + ], + functionName: "execute", + args: [to, value, data] + }) + }, + async getDummySignature(_userOperation) { + return "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c" + } + }) +} diff --git a/src/accounts/toSmartAccount.ts b/src/accounts/toSmartAccount.ts new file mode 100644 index 0000000..704bff6 --- /dev/null +++ b/src/accounts/toSmartAccount.ts @@ -0,0 +1,167 @@ +import { + type Abi, + type Address, + type Chain, + type Client, + type CustomSource, + type EncodeDeployDataParameters, + type Hex, + type SignableMessage, + type Transport, + type TypedDataDefinition, + concat, + encodeAbiParameters +} from "viem" +import { toAccount } from "viem/accounts" +import type { UserOperation } from "../types/userOperation" +import type { EntryPoint, GetEntryPointVersion } from "../types/entrypoint" +import { isSmartAccountDeployed } from "../utils/isSmartAccountDeployed" +import { + SignTransactionNotSupportedBySmartAccount, + type SmartAccount +} from "./types" + +const MAGIC_BYTES = + "0x6492649264926492649264926492649264926492649264926492649264926492" + +export function toSmartAccount< + TAccountSource extends CustomSource, + TEntryPoint extends EntryPoint, + TSource extends string = string, + transport extends Transport = Transport, + chain extends Chain | undefined = Chain | undefined, + TAbi extends Abi | readonly unknown[] = Abi +>({ + address, + client, + source, + entryPoint, + getNonce, + getInitCode, + getFactory, + getFactoryData, + encodeCallData, + getDummySignature, + encodeDeployCallData, + signUserOperation, + signMessage, + signTypedData +}: TAccountSource & { + source: TSource + client: Client + entryPoint: TEntryPoint + getNonce: () => Promise + getInitCode: () => Promise + getFactory: () => Promise
+ getFactoryData: () => Promise + encodeCallData: ( + args: + | { + to: Address + value: bigint + data: Hex + } + | { + to: Address + value: bigint + data: Hex + }[] + ) => Promise + getDummySignature( + userOperation: UserOperation> + ): Promise + encodeDeployCallData: ({ + abi, + args, + bytecode + }: EncodeDeployDataParameters) => Promise + signUserOperation: ( + userOperation: UserOperation> + ) => Promise +}): SmartAccount { + const account = toAccount({ + address: address, + signMessage: async ({ message }: { message: SignableMessage }) => { + const isDeployed = await isSmartAccountDeployed(client, address) + const signature = await signMessage({ message }) + + if (isDeployed) return signature + + const abiEncodedMessage = encodeAbiParameters( + [ + { + type: "address", + name: "create2Factory" + }, + { + type: "bytes", + name: "factoryCalldata" + }, + { + type: "bytes", + name: "originalERC1271Signature" + } + ], + [ + (await getFactory()) ?? "0x", // "0x should never happen if it's deployed" + (await getFactoryData()) ?? "0x", // "0x should never happen if it's deployed" + signature + ] + ) + + return concat([abiEncodedMessage, MAGIC_BYTES]) + }, + signTypedData: async (typedData) => { + const isDeployed = await isSmartAccountDeployed(client, address) + const signature = await signTypedData( + typedData as TypedDataDefinition + ) + + if (isDeployed) return signature + + const abiEncodedMessage = encodeAbiParameters( + [ + { + type: "address", + name: "create2Factory" + }, + { + type: "bytes", + name: "factoryCalldata" + }, + { + type: "bytes", + name: "originalERC1271Signature" + } + ], + [ + (await getFactory()) ?? "0x", // "0x should never happen if it's deployed" + (await getFactoryData()) ?? "0x", // "0x should never happen if it's deployed" + signature + ] + ) + + return concat([abiEncodedMessage, MAGIC_BYTES]) + }, + async signTransaction(_, __) { + throw new SignTransactionNotSupportedBySmartAccount() + } + }) + + return { + ...account, + source, + client, + type: "local", + entryPoint, + publicKey: address, + getNonce, + getInitCode, + getFactory, + getFactoryData, + encodeCallData, + getDummySignature, + encodeDeployCallData, + signUserOperation + } as SmartAccount +} diff --git a/src/accounts/types.ts b/src/accounts/types.ts new file mode 100644 index 0000000..187a232 --- /dev/null +++ b/src/accounts/types.ts @@ -0,0 +1,71 @@ +import { + type Abi, + type Address, + BaseError, + type Client, + type Hex, + type LocalAccount +} from "viem" +import type { Chain, EncodeDeployDataParameters, Transport } from "viem" +import type { UserOperation } from "../types/userOperation" +import type { EntryPoint, GetEntryPointVersion } from "../types/entrypoint" + +export class SignTransactionNotSupportedBySmartAccount extends BaseError { + override name = "SignTransactionNotSupportedBySmartAccount" + constructor({ docsPath }: { docsPath?: string } = {}) { + super( + [ + "A smart account cannot sign or send transaction, it can only sign message or userOperation.", + "Please send user operation instead." + ].join("\n"), + { + docsPath, + docsSlug: "account" + } + ) + } +} + +export type SmartAccount< + entryPoint extends EntryPoint, + TSource extends string = string, + transport extends Transport = Transport, + chain extends Chain | undefined = Chain | undefined, + TAbi extends Abi | readonly unknown[] = Abi +> = LocalAccount & { + client: Client + entryPoint: entryPoint + getNonce: () => Promise + getInitCode: () => Promise + getFactory: () => Promise
+ getFactoryData: () => Promise + encodeCallData: ( + args: + | { + to: Address + value: bigint + data: Hex + } + | { + to: Address + value: bigint + data: Hex + }[] + ) => Promise + getDummySignature( + userOperation: UserOperation> + ): Promise + encodeDeployCallData: ({ + abi, + args, + bytecode + }: EncodeDeployDataParameters) => Promise + signUserOperation: ( + userOperation: UserOperation> + ) => Promise +} + +export type SmartAccountSigner< + TSource extends string = string, + TAddress extends Address = Address +> = Omit, "signTransaction"> diff --git a/src/actions/public/getAccountNonce.ts b/src/actions/public/getAccountNonce.ts new file mode 100644 index 0000000..218f429 --- /dev/null +++ b/src/actions/public/getAccountNonce.ts @@ -0,0 +1,80 @@ +import type { Address, Chain, Client, Transport } from "viem" +import { readContract } from "viem/actions" +import { getAction } from "viem/utils" +import type { Prettify } from "../../types/" +import type { EntryPoint } from "../../types/entrypoint" + +export type GetAccountNonceParams = { + sender: Address + entryPoint: EntryPoint + key?: bigint +} + +/** + * Returns the nonce of the account with the entry point. + * + * - Docs: https://docs.pimlico.io/permissionless/reference/public-actions/getAccountNonce + * + * @param client {@link client} that you created using viem's createPublicClient. + * @param args {@link GetAccountNonceParams} address, entryPoint & key + * @returns bigint nonce + * + * @example + * import { createPublicClient } from "viem" + * import { getAccountNonce } from "permissionless/actions" + * + * const client = createPublicClient({ + * chain: goerli, + * transport: http("https://goerli.infura.io/v3/your-infura-key") + * }) + * + * const nonce = await getAccountNonce(client, { + * address, + * entryPoint, + * key + * }) + * + * // Return 0n + */ +export const getAccountNonce = async < + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined +>( + client: Client, + args: Prettify +): Promise => { + const { sender, entryPoint, key = BigInt(0) } = args + + return await getAction( + client, + readContract, + "readContract" + )({ + address: entryPoint, + abi: [ + { + inputs: [ + { + name: "sender", + type: "address" + }, + { + name: "key", + type: "uint192" + } + ], + name: "getNonce", + outputs: [ + { + name: "nonce", + type: "uint256" + } + ], + stateMutability: "view", + type: "function" + } + ], + functionName: "getNonce", + args: [sender, key] + }) +} diff --git a/src/actions/public/getSenderAddress.ts b/src/actions/public/getSenderAddress.ts new file mode 100644 index 0000000..d8ade9d --- /dev/null +++ b/src/actions/public/getSenderAddress.ts @@ -0,0 +1,211 @@ +import { + type Address, + BaseError, + type CallExecutionErrorType, + type Chain, + type Client, + type ContractFunctionExecutionErrorType, + type ContractFunctionRevertedErrorType, + type Hex, + type RpcRequestErrorType, + type Transport, + concat, + decodeErrorResult +} from "viem" + +import { simulateContract } from "viem/actions" +import { getAction } from "viem/utils" +import type { Prettify } from "../../types/" +import type { + ENTRYPOINT_ADDRESS_V06_TYPE, + EntryPoint +} from "../../types/entrypoint" + +export type GetSenderAddressParams = + entryPoint extends ENTRYPOINT_ADDRESS_V06_TYPE + ? { + initCode: Hex + entryPoint: entryPoint + factory?: never + factoryData?: never + } + : { + entryPoint: entryPoint + factory: Address + factoryData: Hex + initCode?: never + } + +export class InvalidEntryPointError extends BaseError { + override name = "InvalidEntryPointError" + + constructor({ + cause, + entryPoint + }: { cause?: BaseError; entryPoint?: Address } = {}) { + super( + `The entry point address (\`entryPoint\`${ + entryPoint ? ` = ${entryPoint}` : "" + }) is not a valid entry point. getSenderAddress did not revert with a SenderAddressResult error.`, + { + cause + } + ) + } +} + +/** + * Returns the address of the account that will be deployed with the given init code. + * + * - Docs: https://docs.pimlico.io/permissionless/reference/public-actions/getSenderAddress + * + * @param client {@link Client} that you created using viem's createPublicClient. + * @param args {@link GetSenderAddressParams} initCode & entryPoint + * @returns Sender's Address + * + * @example + * import { createPublicClient } from "viem" + * import { getSenderAddress } from "permissionless/actions" + * + * const publicClient = createPublicClient({ + * chain: goerli, + * transport: http("https://goerli.infura.io/v3/your-infura-key") + * }) + * + * const senderAddress = await getSenderAddress(publicClient, { + * initCode, + * entryPoint + * }) + * + * // Return '0x7a88a206ba40b37a8c07a2b5688cf8b287318b63' + */ +export const getSenderAddress = async < + entryPoint extends EntryPoint, + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined +>( + client: Client, + args: Prettify> +): Promise
=> { + const { initCode, entryPoint, factory, factoryData } = args + + if (!initCode && !factory && !factoryData) { + throw new Error( + "Either `initCode` or `factory` and `factoryData` must be provided" + ) + } + + try { + await getAction( + client, + simulateContract, + "simulateContract" + )({ + address: entryPoint, + abi: [ + { + inputs: [ + { + internalType: "address", + name: "sender", + type: "address" + } + ], + name: "SenderAddressResult", + type: "error" + }, + { + inputs: [ + { + internalType: "bytes", + name: "initCode", + type: "bytes" + } + ], + name: "getSenderAddress", + outputs: [], + stateMutability: "nonpayable", + type: "function" + } + ], + functionName: "getSenderAddress", + args: [initCode || concat([factory as Hex, factoryData as Hex])] + }) + } catch (e) { + const err = e as ContractFunctionExecutionErrorType + + if (err.cause.name === "ContractFunctionRevertedError") { + const revertError = err.cause as ContractFunctionRevertedErrorType + const errorName = revertError.data?.errorName ?? "" + if ( + errorName === "SenderAddressResult" && + revertError.data?.args && + revertError.data?.args[0] + ) { + return revertError.data?.args[0] as Address + } + } + + if (err.cause.name === "CallExecutionError") { + const callExecutionError = err.cause as CallExecutionErrorType + if (callExecutionError.cause.name === "RpcRequestError") { + const revertError = + callExecutionError.cause as RpcRequestErrorType + // biome-ignore lint/suspicious/noExplicitAny: fuse issues + const data = (revertError as unknown as any).cause.data.split( + " " + )[1] + + const error = decodeErrorResult({ + abi: [ + { + inputs: [ + { + internalType: "address", + name: "sender", + type: "address" + } + ], + name: "SenderAddressResult", + type: "error" + } + ], + data + }) + + return error.args[0] as Address + } + + if (callExecutionError.cause.name === "InvalidInputRpcError") { + //Ganache local testing returns "InvalidInputRpcError" with data in regular format + const revertError = + callExecutionError.cause as RpcRequestErrorType + // biome-ignore lint/suspicious/noExplicitAny: fuse issues + const data = (revertError as unknown as any).cause.data + + const error = decodeErrorResult({ + abi: [ + { + inputs: [ + { + internalType: "address", + name: "sender", + type: "address" + } + ], + name: "SenderAddressResult", + type: "error" + } + ], + data + }) + + return error.args[0] as Address + } + } + + throw e + } + + throw new InvalidEntryPointError({ entryPoint }) +} diff --git a/src/errors/account.ts b/src/errors/account.ts new file mode 100644 index 0000000..629f7d4 --- /dev/null +++ b/src/errors/account.ts @@ -0,0 +1,345 @@ +import { type Address, BaseError } from "viem" + +export type SenderAlreadyDeployedErrorType = SenderAlreadyDeployedError & { + name: "SenderAlreadyDeployedError" +} +export class SenderAlreadyDeployedError extends BaseError { + static message = /aa10/ + override name = "SenderAlreadyDeployedError" as const + constructor({ + cause, + sender, + docsPath + }: { cause?: BaseError; sender?: Address; docsPath?: string } = {}) { + super( + [ + `Smart account ${sender} is already deployed.`, + "", + "Possible solutions:", + `• Remove the initCode from the user operation and set it to "0x"`, + "", + docsPath ? `Docs: ${docsPath}` : "" + ].join("\n"), + { + cause + } + ) + } +} + +export type InitCodeRevertedErrorType = InitCodeRevertedError & { + name: "InitCodeRevertedError" +} +export class InitCodeRevertedError extends BaseError { + static message = /aa13/ + override name = "InitCodeRevertedError" as const + constructor({ + cause, + docsPath + }: { cause?: BaseError; docsPath?: string } = {}) { + super( + [ + "EntryPoint failed to create the smart account with the initCode provided.", + "", + "Possible reasons:", + "• The initCode ran out of gas", + "• The initCode reverted during the account deployment process", + "", + "Possible solutions:", + "• Verify that the factory address in the initCode is correct (the factory address is the first 20 bytes of the initCode).", + "• Verify that the initCode is correct.", + "• Check whether the verificationGasLimit is sufficient for the initCode to complete without running out of gas.", + "", + docsPath ? `Docs: ${docsPath}` : "" + ].join("\n"), + { + cause + } + ) + } +} + +export type SenderAddressMismatchErrorType = SenderAddressMismatchError & { + name: "SenderAddressMismatchError" +} +export class SenderAddressMismatchError extends BaseError { + static message = /aa14/ + override name = "SenderAddressMismatchError" as const + constructor({ + cause, + sender, + docsPath + }: { + cause?: BaseError + sender: Address + docsPath?: string + }) { + super( + [ + "The initCode returned a different smart account address than expected.", + `Expected: ${sender}`, + "", + "Possible reasons:", + "• Account deployed with the initCode provided does not match match the sender address provided", + "", + "Possible solutions:", + "• Verify that the sender address was generated deterministically from the initCode. (consider leveraging functions like getSenderAddress)", + "• Verify that the factory address in the initCode is correct (the factory address is the first 20 bytes of the initCode)", + "• Verify that the initCode is correct.", + "", + docsPath ? `Docs: ${docsPath}` : "" + ].join("\n"), + { + cause + } + ) + } +} + +export type InitCodeDidNotDeploySenderErrorType = + InitCodeDidNotDeploySenderError & { + name: "InitCodeDidNotDeploySenderError" + } +export class InitCodeDidNotDeploySenderError extends BaseError { + static message = /aa15/ + override name = "InitCodeDidNotDeploySenderError" as const + constructor({ + cause, + sender, + docsPath + }: { + cause?: BaseError + sender: Address + docsPath?: string + }) { + super( + [ + `The initCode did not deploy the sender at the address ${sender}.`, + "", + "Possible reasons:", + "• The initCode factory is not creating an account.", + "• The initCode factory is creating an account, but is not implemented correctly as it is not deploying at the sender address", + "", + "Possible solutions:", + "• Verify that the factory address in the initCode is correct (the factory address is the first 20 bytes of the initCode).", + "• Verify that the initCode factory is implemented correctly. The factory must deploy the smart account at the sender address.", + "", + docsPath ? `Docs: ${docsPath}` : "" + ].join("\n"), + { + cause + } + ) + } +} + +export type SenderNotDeployedErrorType = SenderNotDeployedError & { + name: "SenderNotDeployedError" +} +export class SenderNotDeployedError extends BaseError { + static message = /aa20/ + override name = "SenderNotDeployedError" as const + constructor({ + cause, + sender, + docsPath + }: { + cause?: BaseError + sender: Address + docsPath?: string + }) { + super( + [ + `Smart account ${sender} is not deployed.`, + "", + "Possible reasons:", + "• An initCode was not specified, but the sender address (i.e. the smart account) is not deployed.", + "", + "Possible solutions:", + "• If this is the first transaction by this account, make sure the initCode is included in the user operation.", + "• If the smart account is already supposed to be deployed, verify that you have selected the correct sender address for the user operation.", + "", + docsPath ? `Docs: ${docsPath}` : "" + ].join("\n"), + { + cause + } + ) + } +} + +export type SmartAccountInsufficientFundsErrorType = + SmartAccountInsufficientFundsError & { + name: "SmartAccountInsufficientFundsError" + } +export class SmartAccountInsufficientFundsError extends BaseError { + static message = /aa21/ + override name = "SmartAccountInsufficientFundsError" as const + constructor({ + cause, + sender, + docsPath + }: { + cause?: BaseError + sender: Address + docsPath?: string + }) { + super( + [ + `You are not using a paymaster, and the ${sender} address did not have enough native tokens to cover the gas costs associated with the user operation.`, + "", + "Possible solutions:", + "• If you are not using a paymaster, verify that the sender address has enough native tokens to cover the required prefund. Consider leveraging functions like getRequiredPrefund.", + "• If you are looking to use a paymaster to cover the gas fees, verify that the paymasterAndData field is set.", + "", + docsPath ? `Docs: ${docsPath}` : "" + ].join("\n"), + { + cause + } + ) + } +} + +export type SmartAccountSignatureValidityPeriodErrorType = + SmartAccountSignatureValidityPeriodError & { + name: "SmartAccountSignatureValidityPeriodError" + } +export class SmartAccountSignatureValidityPeriodError extends BaseError { + static message = /aa22/ + override name = "SmartAccountSignatureValidityPeriodError" as const + constructor({ + cause, + docsPath + }: { + cause?: BaseError + docsPath?: string + }) { + super( + [ + "The signature used in the user operation is not valid, because it is outside of the time range it specified.", + "", + "Possible reasons:", + "• This error occurs when the block.timestamp falls after the validUntil timestamp, or before the validAfter timestamp.", + "", + "Possible solutions:", + "• If you are looking to use time-based signatures, verify that the validAfter and validUntil fields are set correctly and that the user operation is sent within the specified range.", + "• If you are not looking to use time-based signatures, verify that the validAfter and validUntil fields are set to 0.", + "", + docsPath ? `Docs: ${docsPath}` : "" + ].join("\n"), + { + cause + } + ) + } +} + +export type SmartAccountValidationRevertedErrorType = + SmartAccountValidationRevertedError & { + name: "SmartAccountValidationRevertedError" + } +export class SmartAccountValidationRevertedError extends BaseError { + static message = /aa23/ + override name = "SmartAccountValidationRevertedError" as const + constructor({ + cause, + sender, + docsPath + }: { + cause?: BaseError + sender: Address + docsPath?: string + }) { + super( + [ + `The smart account ${sender} reverted or ran out of gas during the validation of the user operation.`, + "", + "Possible solutions:", + "• Verify that the verificationGasLimit is high enough to cover the validateUserOp function's gas costs.", + "• Make sure validateUserOp returns uint(1) for invalid signatures, and MUST NOT REVERT when the signature is invalid", + "• If you are not using a paymaster, verify that the sender address has enough native tokens to cover the required pre fund. Consider leveraging functions like getRequiredPrefund.", + "• Verify that the validateUserOp function is implemented with the correct logic, and that the user operation is supposed to be valid.", + "", + docsPath ? `Docs: ${docsPath}` : "" + ].join("\n"), + { + cause + } + ) + } +} + +export type InvalidSmartAccountSignatureErrorType = + InvalidSmartAccountSignatureError & { + name: "InvalidSmartAccountSignatureError" + } +export class InvalidSmartAccountSignatureError extends BaseError { + static message = /aa24/ + override name = "InvalidSmartAccountSignatureError" as const + constructor({ + cause, + sender, + docsPath + }: { + cause?: BaseError + sender: Address + docsPath?: string + }) { + super( + [ + `The smart account ${sender} signature is invalid.`, + "", + "Possible solutions:", + "• Verify that the user operation was correctly signed, and that the signature was correctly encoded in the signature field of the user operation.", + "• Most smart account implementations sign over the userOpHash. Make sure that the userOpHash is correctly computed. Consider leveraging functions like getUserOperationHash.", + "• Make sure you have selected the correct chainId and entryPointAddress when computing the userOpHash.", + "• Make sure the smart account signature verification function is correctly implemented.", + "", + docsPath ? `Docs: ${docsPath}` : "" + ].join("\n"), + { + cause + } + ) + } +} + +export type InvalidSmartAccountNonceErrorType = + InvalidSmartAccountNonceError & { + name: "InvalidSmartAccountNonceError" + } +export class InvalidSmartAccountNonceError extends BaseError { + static message = /aa25/ + override name = "InvalidSmartAccountNonceError" as const + constructor({ + cause, + sender, + nonce, + docsPath + }: { + cause?: BaseError + sender: Address + docsPath?: string + nonce: bigint + }) { + const nonceKey = nonce >> BigInt(64) // first 192 bits of nonce + const nonceSequence = nonce & 0xffffffffffffffffn // last 64 bits of nonce + + super( + [ + `The smart account ${sender} nonce is invalid.`, + `Nonce sent: ${nonce} (key: ${nonceKey}, sequence: ${nonceSequence})`, + "", + "Possible solutions:", + "• Verify that you are using the correct nonce for the user operation. The nonce should be the current nonce of the smart account for the selected key. Consider leveraging functions like getAccountNonce.", + "• Verify that the nonce is formatted correctly.", + "", + docsPath ? `Docs: ${docsPath}` : "" + ].join("\n"), + { + cause + } + ) + } +} diff --git a/src/errors/bundler.ts b/src/errors/bundler.ts new file mode 100644 index 0000000..aac2e7c --- /dev/null +++ b/src/errors/bundler.ts @@ -0,0 +1,62 @@ +import { BaseError } from "viem" + +export type InvalidBeneficiaryAddressErrorType = + InvalidBeneficiaryAddressError & { + name: "InvalidBeneficiaryAddressError" + } +export class InvalidBeneficiaryAddressError extends BaseError { + static message = /aa9[01]/ + override name = "InvalidBeneficiaryAddressError" + constructor({ + cause, + docsPath + }: { + cause?: BaseError + docsPath?: string + }) { + super( + [ + "The bundler did not set a beneficiary address when bundling the user operation.", + "", + "Possible solutions:", + "• If you encounter this error when running self-hosted bundler, make sure you have configured the bundler correctly.", + "• If you are using a bundler provider, reach out to them.", + "", + docsPath ? `Docs: ${docsPath}` : "" + ].join("\n"), + { + cause + } + ) + } +} + +export type InvalidAggregatorErrorType = InvalidAggregatorError & { + name: "InvalidAggregatorError" +} +export class InvalidAggregatorError extends BaseError { + static message = /aa96/ + override name = "InvalidAggregatorError" + constructor({ + cause, + docsPath + }: { + cause?: BaseError + docsPath?: string + }) { + super( + [ + "The bundler tried to bundle the user operation with an invalid aggregator.", + "", + "Possible solutions:", + "• If you are using your own bundler, configure it to use a valid aggregator.", + "• If you are using a bundler provider, reach out to them.", + "", + docsPath ? `Docs: ${docsPath}` : "" + ].join("\n"), + { + cause + } + ) + } +} diff --git a/src/errors/estimateUserOperationGas.ts b/src/errors/estimateUserOperationGas.ts new file mode 100644 index 0000000..4bc10c2 --- /dev/null +++ b/src/errors/estimateUserOperationGas.ts @@ -0,0 +1,70 @@ +import { BaseError } from "viem" + +import type { EntryPoint, GetEntryPointVersion } from "../types/entrypoint" +import { prettyPrint } from "./utils" +import { PartialBy, UserOperation } from "@/types" +export type EstimateUserOperationGasParameters = + { + userOperation: GetEntryPointVersion extends "v0.6" + ? PartialBy< + UserOperation<"v0.6">, + "callGasLimit" | "preVerificationGas" | "verificationGasLimit" + > + : PartialBy< + UserOperation<"v0.7">, + | "callGasLimit" + | "preVerificationGas" + | "verificationGasLimit" + | "paymasterVerificationGasLimit" + | "paymasterPostOpGasLimit" + > + entryPoint: entryPoint + } +export type EstimateUserOperationGasErrorType = + EstimateUserOperationGasError & { + name: "EstimateUserOperationGasError" + } +export class EstimateUserOperationGasError< + entryPoint extends EntryPoint +> extends BaseError { + cause: BaseError + + override name = "EstimateUserOperationGasError" + + constructor( + cause: BaseError, + { + userOperation, + entryPoint, + docsPath + }: EstimateUserOperationGasParameters & { + docsPath?: string + } + ) { + const prettyArgs = prettyPrint({ + sender: userOperation.sender, + nonce: userOperation.nonce, + initCode: userOperation.initCode, + callData: userOperation.callData, + callGasLimit: userOperation.callGasLimit, + verificationGasLimit: userOperation.verificationGasLimit, + preVerificationGas: userOperation.preVerificationGas, + maxFeePerGas: userOperation.maxFeePerGas, + maxPriorityFeePerGas: userOperation.maxPriorityFeePerGas, + paymasterAndData: userOperation.paymasterAndData, + signature: userOperation.signature, + entryPoint + }) + + super(cause.shortMessage, { + cause, + docsPath, + metaMessages: [ + ...(cause.metaMessages ? [...cause.metaMessages, " "] : []), + "Estimate Gas Arguments:", + prettyArgs + ].filter(Boolean) as string[] + }) + this.cause = cause + } +} diff --git a/src/errors/gas.ts b/src/errors/gas.ts new file mode 100644 index 0000000..26dec10 --- /dev/null +++ b/src/errors/gas.ts @@ -0,0 +1,120 @@ +import { BaseError } from "viem" + +export type VerificationGasLimitTooLowErrorType = + VerificationGasLimitTooLowError & { + name: "VerificationGasLimitTooLowError" + } +export class VerificationGasLimitTooLowError extends BaseError { + static message = /aa4[01]/ + override name = "VerificationGasLimitTooLowError" + constructor({ + cause, + verificationGasLimit, + docsPath + }: { + cause?: BaseError + verificationGasLimit?: bigint + docsPath?: string + }) { + super( + [ + `The smart account and paymaster verification exceeded the verificationGasLimit ${verificationGasLimit} set for the user operation.`, + "", + "Possible solutions:", + "• Verify that the verificationGasLimit set for the user operation is high enough to cover the gas used during smart account and paymaster verification.", + "• If you are using the eth_estimateUserOperationGas or pm_sponsorUserOperation method from bundler provider to set user operation gas limits and the EntryPoint throws this error during submission, reach out to them.", + "", + docsPath ? `Docs: ${docsPath}` : "" + ].join("\n"), + { + cause + } + ) + } +} + +export type ActualGasCostTooHighErrorType = ActualGasCostTooHighError & { + name: "ActualGasCostTooHighError" +} +export class ActualGasCostTooHighError extends BaseError { + static message = /aa51/ + override name = "ActualGasCostTooHighError" + constructor({ + cause, + docsPath + }: { + cause?: BaseError + docsPath?: string + }) { + super( + [ + "The actual gas cost of the user operation ended up being higher than the funds paid by the smart account or the paymaster.", + "", + "Possible solutions:", + "• If you encounter this error, try increasing the verificationGasLimit set for the user operation.", + "• If you are using the eth_estimateUserOperationGas or pm_sponsorUserOperation method from bundler provider to set user operation gas limits and the EntryPoint throws this error during submission, reach out to them.", + "", + docsPath ? `Docs: ${docsPath}` : "" + ].join("\n"), + { + cause + } + ) + } +} + +export type GasValuesOverflowErrorType = GasValuesOverflowError & { + name: "GasValuesOverflowError" +} +export class GasValuesOverflowError extends BaseError { + static message = /aa94/ + override name = "GasValuesOverflowError" + constructor({ + cause, + docsPath + }: { + cause?: BaseError + docsPath?: string + }) { + super( + [ + "The gas limit values of the user operation overflowed, they must fit in uint160.", + "", + docsPath ? `Docs: ${docsPath}` : "" + ].join("\n"), + { + cause + } + ) + } +} + +export type BundlerOutOfGasErrorType = BundlerOutOfGasError & { + name: "BundlerOutOfGasError" +} +export class BundlerOutOfGasError extends BaseError { + static message = /aa95/ + override name = "BundlerOutOfGasError" + constructor({ + cause, + docsPath + }: { + cause?: BaseError + docsPath?: string + }) { + super( + [ + "The bundler tried to bundle the user operation with the gas limit set too low.", + "", + "Possible solutions:", + "• If you are using your own bundler, configure it send gas limits properly.", + "• If you are using a bundler provider, reach out to them.", + "", + docsPath ? `Docs: ${docsPath}` : "" + ].join("\n"), + { + cause + } + ) + } +} diff --git a/src/errors/index.ts b/src/errors/index.ts new file mode 100644 index 0000000..f629f35 --- /dev/null +++ b/src/errors/index.ts @@ -0,0 +1,118 @@ +import { + InitCodeDidNotDeploySenderError, + type InitCodeDidNotDeploySenderErrorType, + InitCodeRevertedError, + type InitCodeRevertedErrorType, + InvalidSmartAccountNonceError, + type InvalidSmartAccountNonceErrorType, + InvalidSmartAccountSignatureError, + type InvalidSmartAccountSignatureErrorType, + SenderAddressMismatchError, + type SenderAddressMismatchErrorType, + SenderAlreadyDeployedError, + type SenderAlreadyDeployedErrorType, + SenderNotDeployedError, + type SenderNotDeployedErrorType, + SmartAccountInsufficientFundsError, + type SmartAccountInsufficientFundsErrorType, + SmartAccountSignatureValidityPeriodError, + type SmartAccountSignatureValidityPeriodErrorType, + SmartAccountValidationRevertedError, + type SmartAccountValidationRevertedErrorType +} from "./account" +import { + EstimateUserOperationGasError, + type EstimateUserOperationGasErrorType +} from "./estimateUserOperationGas" +import { + SendUserOperationError, + type SendUserOperationErrorType +} from "./sendUserOperation" + +import { + InvalidPaymasterAndDataError, + type InvalidPaymasterAndDataErrorType, + PaymasterDataRejectedError, + type PaymasterDataRejectedErrorType, + PaymasterDepositTooLowError, + type PaymasterDepositTooLowErrorType, + PaymasterNotDeployedError, + type PaymasterNotDeployedErrorType, + PaymasterPostOpRevertedError, + type PaymasterPostOpRevertedErrorType, + PaymasterValidationRevertedError, + type PaymasterValidationRevertedErrorType, + PaymasterValidityPeriodError, + type PaymasterValidityPeriodErrorType +} from "./paymaster" + +import { + InvalidAggregatorError, + type InvalidAggregatorErrorType, + InvalidBeneficiaryAddressError, + type InvalidBeneficiaryAddressErrorType +} from "./bundler" + +import { + ActualGasCostTooHighError, + type ActualGasCostTooHighErrorType, + BundlerOutOfGasError, + type BundlerOutOfGasErrorType, + GasValuesOverflowError, + type GasValuesOverflowErrorType, + VerificationGasLimitTooLowError, + type VerificationGasLimitTooLowErrorType +} from "./gas" + +export { + type InitCodeDidNotDeploySenderErrorType, + type InitCodeRevertedErrorType, + type InvalidSmartAccountNonceErrorType, + type InvalidSmartAccountSignatureErrorType, + type SenderAddressMismatchErrorType, + type SenderAlreadyDeployedErrorType, + type SenderNotDeployedErrorType, + type SmartAccountInsufficientFundsErrorType, + type SmartAccountSignatureValidityPeriodErrorType, + type SmartAccountValidationRevertedErrorType, + type InvalidPaymasterAndDataErrorType, + type PaymasterDataRejectedErrorType, + type PaymasterDepositTooLowErrorType, + type PaymasterNotDeployedErrorType, + type PaymasterPostOpRevertedErrorType, + type PaymasterValidationRevertedErrorType, + type PaymasterValidityPeriodErrorType, + type InvalidAggregatorErrorType, + type InvalidBeneficiaryAddressErrorType, + type ActualGasCostTooHighErrorType, + type BundlerOutOfGasErrorType, + type GasValuesOverflowErrorType, + type VerificationGasLimitTooLowErrorType, + SenderAlreadyDeployedError, + EstimateUserOperationGasError, + InitCodeRevertedError, + SenderAddressMismatchError, + InitCodeDidNotDeploySenderError, + SenderNotDeployedError, + SmartAccountInsufficientFundsError, + SmartAccountSignatureValidityPeriodError, + SmartAccountValidationRevertedError, + InvalidSmartAccountNonceError, + PaymasterNotDeployedError, + PaymasterDepositTooLowError, + InvalidSmartAccountSignatureError, + InvalidBeneficiaryAddressError, + InvalidAggregatorError, + InvalidPaymasterAndDataError, + PaymasterDataRejectedError, + PaymasterValidityPeriodError, + PaymasterValidationRevertedError, + VerificationGasLimitTooLowError, + ActualGasCostTooHighError, + GasValuesOverflowError, + BundlerOutOfGasError, + PaymasterPostOpRevertedError, + SendUserOperationError, + type EstimateUserOperationGasErrorType, + type SendUserOperationErrorType +} diff --git a/src/errors/paymaster.ts b/src/errors/paymaster.ts new file mode 100644 index 0000000..7000f23 --- /dev/null +++ b/src/errors/paymaster.ts @@ -0,0 +1,257 @@ +import { BaseError, type Hex } from "viem" +import { getAddressFromInitCodeOrPaymasterAndData } from "../utils/" + +export type PaymasterNotDeployedErrorType = PaymasterNotDeployedError & { + name: "PaymasterNotDeployedError" +} +export class PaymasterNotDeployedError extends BaseError { + static message = /aa30/ + override name = "PaymasterNotDeployedError" + constructor({ + cause, + paymasterAndData, + docsPath + }: { + cause?: BaseError + paymasterAndData?: Hex + docsPath?: string + } = {}) { + const paymaster = paymasterAndData + ? getAddressFromInitCodeOrPaymasterAndData(paymasterAndData) + : "0x" + + super( + [ + `Paymaster ${paymaster} is not deployed.`, + "", + "Possible solutions:", + "• Verify that the paymasterAndData field is correct, and that the first 20 bytes are the address of the paymaster contract you intend to use.", + "• Verify that the paymaster contract is deployed on the network you are using.", + "", + docsPath ? `Docs: ${docsPath}` : "" + ].join("\n"), + { + cause + } + ) + } +} + +export type PaymasterDepositTooLowErrorType = PaymasterDepositTooLowError & { + name: "PaymasterDepositTooLowError" +} +export class PaymasterDepositTooLowError extends BaseError { + static message = /aa31/ + override name = "PaymasterDepositTooLowError" + constructor({ + cause, + paymasterAndData, + docsPath + }: { + cause?: BaseError + paymasterAndData?: Hex + docsPath?: string + } = {}) { + const paymaster = paymasterAndData + ? getAddressFromInitCodeOrPaymasterAndData(paymasterAndData) + : "0x" + + super( + [ + `Paymaster ${paymaster} contract does not have enough funds deposited into the EntryPoint contract to cover the required funds for the user operation.`, + "", + "Possible solutions:", + "• If you are using your own paymaster contract, deposit more funds into the EntryPoint contract through the deposit() function of the paymaster contract.", + "• Verify that the paymasterAndData field is correct, and that the first 20 bytes are the address of the paymaster contract you intend to useVerify that the paymasterAndData field is correct, and that the first 20 bytes are the address of the paymaster contract you intend to use.", + "• If you are using a paymaster service, reach out to them.", + "", + docsPath ? `Docs: ${docsPath}` : "" + ].join("\n"), + { + cause + } + ) + } +} + +export type PaymasterValidityPeriodErrorType = PaymasterValidityPeriodError & { + name: "PaymasterValidityPeriodError" +} +export class PaymasterValidityPeriodError extends BaseError { + static message = /aa32/ + override name = "PaymasterValidityPeriodError" + constructor({ + cause, + paymasterAndData, + docsPath + }: { + cause?: BaseError + paymasterAndData?: Hex + docsPath?: string + }) { + const paymaster = paymasterAndData + ? getAddressFromInitCodeOrPaymasterAndData(paymasterAndData) + : "0x" + + super( + [ + `Paymaster ${paymaster}'s data used in the paymasterAndData field of the user operation is not valid, because it is outside of the time range it specified.`, + "", + "Possible reasons:", + "• This error occurs when the block.timestamp falls after the validUntil timestamp, or before the validAfter timestamp.", + "", + "Possible solutions:", + "• If you are using your own paymaster contract and using time-based signatures, verify that the validAfter and validUntil fields are set correctly and that the user operation is sent within the specified range.", + "• If you are using your own paymaster contract and not looking to use time-based signatures, verify that the validAfter and validUntil fields are set to 0.", + "• If you are using a service, contact your service provider for their paymaster's validity.", + "", + docsPath ? `Docs: ${docsPath}` : "" + ].join("\n"), + { + cause + } + ) + } +} + +export type PaymasterValidationRevertedErrorType = + PaymasterValidationRevertedError & { + name: "PaymasterValidationRevertedError" + } +export class PaymasterValidationRevertedError extends BaseError { + static message = /aa33/ + override name = "PaymasterValidationRevertedError" + constructor({ + cause, + paymasterAndData, + docsPath + }: { + cause?: BaseError + paymasterAndData?: Hex + docsPath?: string + }) { + const paymaster = paymasterAndData + ? getAddressFromInitCodeOrPaymasterAndData(paymasterAndData) + : "0x" + + super( + [ + `The validatePaymasterUserOp function of the paymaster ${paymaster} either reverted or ran out of gas.`, + "", + "Possible solutions:", + "• Verify that the verificationGasLimit is high enough to cover the validatePaymasterUserOp function's gas costs.", + "• If you are using your own paymaster contract, verify that the validatePaymasterUserOp function is implemented with the correct logic, and that the user operation is supposed to be valid.", + "• If you are using a paymaster service, and the user operation is well formed with a high enough verificationGasLimit, reach out to them.", + "• If you are not looking to use a paymaster to cover the gas fees, verify that the paymasterAndData field is not set.", + "", + docsPath ? `Docs: ${docsPath}` : "" + ].join("\n"), + { + cause + } + ) + } +} + +export type PaymasterDataRejectedErrorType = PaymasterDataRejectedError & { + name: "PaymasterDataRejectedError" +} +export class PaymasterDataRejectedError extends BaseError { + static message = /aa34/ + override name = "PaymasterDataRejectedError" + constructor({ + cause, + paymasterAndData, + docsPath + }: { + cause?: BaseError + paymasterAndData?: Hex + docsPath?: string + }) { + const paymaster = paymasterAndData + ? getAddressFromInitCodeOrPaymasterAndData(paymasterAndData) + : "0x" + + super( + [ + `The validatePaymasterUserOp function of the paymaster ${paymaster} rejected paymasterAndData.`, + "", + "Possible solutions:", + "• If you are using your own paymaster contract, verify that the user operation was correctly signed according to your implementation, and that the paymaster signature was correctly encoded in the paymasterAndData field of the user operation.", + "• If you are using a paymaster service, make sure you do not modify any of the fields of the user operation after the paymaster signs over it (except the signature field).", + "• If you are using a paymaster service and you have not modified any of the fields except the signature but you are still getting this error, reach out to them.", + "", + docsPath ? `Docs: ${docsPath}` : "" + ].join("\n"), + { + cause + } + ) + } +} + +export type PaymasterPostOpRevertedErrorType = PaymasterPostOpRevertedError & { + name: "PaymasterPostOpRevertedError" +} +export class PaymasterPostOpRevertedError extends BaseError { + static message = /aa50/ + override name = "PaymasterPostOpRevertedError" + constructor({ + cause, + paymasterAndData, + docsPath + }: { + cause?: BaseError + paymasterAndData?: Hex + docsPath?: string + }) { + const paymaster = paymasterAndData + ? getAddressFromInitCodeOrPaymasterAndData(paymasterAndData) + : "0x" + + super( + [ + `The postOp function of the paymaster ${paymaster} reverted.`, + "", + "Possible solutions:", + "• If you are using your own paymaster contract, verify that that you have correctly implemented the postOp function (if you are using one). If you do not intent to make use of the postOp function, make sure you do not set the context parameter in the paymaster's validatePaymasterUserOp function.", + "• If you are using a paymaster service and you see this error, reach out to them.", + "", + docsPath ? `Docs: ${docsPath}` : "" + ].join("\n"), + { + cause + } + ) + } +} + +export type InvalidPaymasterAndDataErrorType = InvalidPaymasterAndDataError & { + name: "InvalidPaymasterAndDataError" +} +export class InvalidPaymasterAndDataError extends BaseError { + static message = /aa93/ + override name = "InvalidPaymasterAndDataError" + constructor({ + cause, + docsPath + }: { + cause?: BaseError + docsPath?: string + }) { + super( + [ + "The paymasterAndData field of the user operation is invalid.", + "", + "Possible solutions:", + "• Make sure you have either not set a value for the paymasterAndData, or that it is at least 20 bytes long.", + "• If you are using a paymaster service, reach out to them.", + "", + docsPath ? `Docs: ${docsPath}` : "" + ].join("\n"), + { + cause + } + ) + } +} diff --git a/src/errors/sendUserOperation.ts b/src/errors/sendUserOperation.ts new file mode 100644 index 0000000..4f484fa --- /dev/null +++ b/src/errors/sendUserOperation.ts @@ -0,0 +1,43 @@ +import { BaseError } from "viem" + +import type { EntryPoint } from "../types/entrypoint" +import { prettyPrint } from "./utils" + +export type SendUserOperationErrorType = + SendUserOperationError & { + name: "SendUserOperationError" + } +export class SendUserOperationError< + entryPoint extends EntryPoint +> extends BaseError { + cause: BaseError + + override name = "SendUserOperationError" + + constructor( + cause: BaseError, + { + userOperation, + entryPoint, + docsPath + }: any & { + docsPath?: string + } + ) { + const prettyArgs = prettyPrint({ + ...userOperation, + entryPoint + }) + + super(cause.shortMessage, { + cause, + docsPath, + metaMessages: [ + ...(cause.metaMessages ? [...cause.metaMessages, " "] : []), + "sendUserOperation Arguments:", + prettyArgs + ].filter(Boolean) as string[] + }) + this.cause = cause + } +} diff --git a/src/errors/utils.ts b/src/errors/utils.ts new file mode 100644 index 0000000..a8b7c58 --- /dev/null +++ b/src/errors/utils.ts @@ -0,0 +1,19 @@ +export type ErrorType = Error & { name: name } + +export function prettyPrint( + args: Record +) { + const entries = Object.entries(args) + .map(([key, value]) => { + if (value === undefined || value === false) return null + return [key, value] + }) + .filter(Boolean) as [string, string][] + const maxLength = entries.reduce( + (acc, [key]) => Math.max(acc, key.length), + 0 + ) + return entries + .map(([key, value]) => ` ${`${key}:`.padEnd(maxLength + 1)} ${value}`) + .join("\n") +} diff --git a/src/index.ts b/src/index.ts index 7206810..d51ee99 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,13 +10,13 @@ import Path from "./utils/path"; export function init(isProdUrl = true) { axios.defaults.baseURL = isProdUrl ? "https://EthPaymaster.org" : "https://relay-ethpaymaster-pr-20.onrender.com"; axios.defaults.headers.common['Content-Type'] = 'application/json'; - axios.interceptors.response.use(function (response) { + axios.interceptors.response.use(function (response: any) { // return data value // if (response?.data?.token && response?.data?.expire) { // axios.defaults.headers.common['Authorization'] = 'Bearer ' + response?.data?.token; // } return response?.data || response; - }, function (error) { + }, function (error: any) { return Promise.reject(error.toJSON()); }); } diff --git a/src/types/bundler.ts b/src/types/bundler.ts new file mode 100644 index 0000000..7035566 --- /dev/null +++ b/src/types/bundler.ts @@ -0,0 +1,131 @@ +import type { Address, Hash, Hex } from "viem" +import type { PartialBy } from "viem/types/utils" +import type { EntryPoint, GetEntryPointVersion } from "./entrypoint" +import type { UserOperationWithBigIntAsHex } from "./userOperation" + +export type BundlerRpcSchema = [ + { + Method: "eth_sendUserOperation" + Parameters: [ + userOperation: UserOperationWithBigIntAsHex< + GetEntryPointVersion + >, + entryPoint: entryPoint + ] + ReturnType: Hash + }, + { + Method: "eth_estimateUserOperationGas" + Parameters: [ + userOperation: GetEntryPointVersion extends "v0.6" + ? PartialBy< + UserOperationWithBigIntAsHex<"v0.6">, + | "callGasLimit" + | "preVerificationGas" + | "verificationGasLimit" + > + : PartialBy< + UserOperationWithBigIntAsHex<"v0.7">, + | "callGasLimit" + | "preVerificationGas" + | "verificationGasLimit" + | "paymasterVerificationGasLimit" + | "paymasterPostOpGasLimit" + >, + entryPoint: entryPoint, + stateOverrides?: StateOverrides + ] + ReturnType: GetEntryPointVersion extends "v0.6" + ? { + preVerificationGas: Hex + verificationGasLimit: Hex + callGasLimit: Hex + } + : { + preVerificationGas: Hex + verificationGasLimit: Hex + callGasLimit?: Hex | null + paymasterVerificationGasLimit?: Hex | null + paymasterPostOpGasLimit?: Hex | null + } + }, + { + Method: "eth_supportedEntryPoints" + Parameters: [] + ReturnType: Address[] + }, + { + Method: "eth_chainId" + Parameters: [] + ReturnType: Hex + }, + { + Method: "eth_getUserOperationByHash" + Parameters: [hash: Hash] + ReturnType: { + userOperation: UserOperationWithBigIntAsHex< + GetEntryPointVersion + > + entryPoint: entryPoint + transactionHash: Hash + blockHash: Hash + blockNumber: Hex + } + }, + { + Method: "eth_getUserOperationReceipt" + Parameters: [hash: Hash] + ReturnType: UserOperationReceiptWithBigIntAsHex + } +] + +type UserOperationReceiptWithBigIntAsHex = { + userOpHash: Hash + entryPoint: Address + sender: Address + nonce: Hex + paymaster?: Address + actualGasUsed: Hex + actualGasCost: Hex + success: boolean + reason?: string + receipt: { + transactionHash: Hex + transactionIndex: Hex + blockHash: Hash + blockNumber: Hex + from: Address + to: Address | null + cumulativeGasUsed: Hex + status: "0x0" | "0x1" + gasUsed: Hex + contractAddress: Address | null + logsBloom: Hex + effectiveGasPrice: Hex + } + logs: { + data: Hex + blockNumber: Hex + blockHash: Hash + transactionHash: Hash + logIndex: Hex + transactionIndex: Hex + address: Address + topics: [Hex, ...Hex[]] | [] + removed: boolean + }[] +} + +export type StateOverrides = { + [x: string]: { + balance?: bigint | undefined + nonce?: bigint | number | undefined + code?: Hex | undefined + state?: { + [x: Hex]: Hex + } + stateDiff?: { + [x: Hex]: Hex + } + } +} diff --git a/src/types/entrypoint.ts b/src/types/entrypoint.ts new file mode 100644 index 0000000..32753e5 --- /dev/null +++ b/src/types/entrypoint.ts @@ -0,0 +1,13 @@ +export type EntryPointVersion = "v0.6" | "v0.7" + +export type ENTRYPOINT_ADDRESS_V06_TYPE = + "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789" +export type ENTRYPOINT_ADDRESS_V07_TYPE = + "0x0000000071727De22E5E9d8BAf0edAc6f37da032" + +export type GetEntryPointVersion = + entryPoint extends ENTRYPOINT_ADDRESS_V06_TYPE ? "v0.6" : "v0.7" + +export type EntryPoint = + | ENTRYPOINT_ADDRESS_V06_TYPE + | ENTRYPOINT_ADDRESS_V07_TYPE diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..06a0106 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,45 @@ +import type { Account, Chain, Client, Transport } from "viem" +import type { IsUndefined } from "viem/types/utils" +import type { SmartAccount } from "../accounts/types" +import type { EntryPoint } from "./entrypoint" +import type { UserOperation } from "./userOperation" +export type { UserOperation } +export type { + EntryPointVersion, + ENTRYPOINT_ADDRESS_V06_TYPE, + ENTRYPOINT_ADDRESS_V07_TYPE, + GetEntryPointVersion, + EntryPoint +} from "./entrypoint" + +export type { PackedUserOperation } from "./userOperation" + +export type GetAccountParameterWithClient< + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined, + TAccount extends Account | undefined = Account | undefined +> = IsUndefined extends true + ? { account: Account; client?: Client } + : { client: Client; account?: Account } + +export type GetAccountParameter< + entryPoint extends EntryPoint, + TAccount extends SmartAccount | undefined = + | SmartAccount + | undefined +> = IsUndefined extends true + ? { account: SmartAccount } + : { account?: SmartAccount } + +export type Prettify = { + [K in keyof T]: T[K] +} & {} + +export type PartialBy = Omit & Partial> + +export type PartialPick = Partial> + +// biome-ignore lint/suspicious/noExplicitAny: generic type +export type UnionOmit = T extends any + ? Omit + : never diff --git a/src/types/userOperation.ts b/src/types/userOperation.ts new file mode 100644 index 0000000..abd644d --- /dev/null +++ b/src/types/userOperation.ts @@ -0,0 +1,102 @@ +import type { Address } from "viem" +import type { Hex } from "viem" +import type { EntryPointVersion } from "./entrypoint" + +export type TStatus = "success" | "reverted" + +export type UserOperationWithBigIntAsHex< + entryPointVersion extends EntryPointVersion +> = entryPointVersion extends "v0.6" + ? { + sender: Address + nonce: Hex + initCode: Hex + callData: Hex + callGasLimit: Hex + verificationGasLimit: Hex + preVerificationGas: Hex + maxFeePerGas: Hex + maxPriorityFeePerGas: Hex + paymasterAndData: Hex + signature: Hex + factory?: never + factoryData?: never + paymaster?: never + paymasterVerificationGasLimit?: never + paymasterPostOpGasLimit?: never + paymasterData?: never + } + : { + sender: Address + nonce: Hex + factory: Address + factoryData: Hex + callData: Hex + callGasLimit: Hex + verificationGasLimit: Hex + preVerificationGas: Hex + maxFeePerGas: Hex + maxPriorityFeePerGas: Hex + paymaster: Address + paymasterVerificationGasLimit: Hex + paymasterPostOpGasLimit: Hex + paymasterData: Hex + signature: Hex + initCode?: never + paymasterAndData?: never + } + +export type UserOperation = + entryPointVersion extends "v0.6" + ? { + sender: Address + nonce: bigint + initCode: Hex + callData: Hex + callGasLimit: bigint + verificationGasLimit: bigint + preVerificationGas: bigint + maxFeePerGas: bigint + maxPriorityFeePerGas: bigint + paymasterAndData: Hex + signature: Hex + factory?: never + factoryData?: never + paymaster?: never + paymasterVerificationGasLimit?: never + paymasterPostOpGasLimit?: never + paymasterData?: never + } + : { + sender: Address + nonce: bigint + factory?: Address + factoryData?: Hex + callData: Hex + callGasLimit: bigint + verificationGasLimit: bigint + preVerificationGas: bigint + maxFeePerGas: bigint + maxPriorityFeePerGas: bigint + paymaster?: Address + paymasterVerificationGasLimit?: bigint + paymasterPostOpGasLimit?: bigint + paymasterData?: Hex + signature: Hex + initCode?: never + paymasterAndData?: never + } + +export type Hex32 = `0x${string & { length: 64 }}` + +export type PackedUserOperation = { + sender: Address + nonce: bigint + initCode: Hex + callData: Hex + accountGasLimits: Hex32 + preVerificationGas: bigint + gasFees: Hex32 + paymasterAndData: Hex + signature: Hex +} diff --git a/src/utils/deepHexlify.ts b/src/utils/deepHexlify.ts new file mode 100644 index 0000000..b14d545 --- /dev/null +++ b/src/utils/deepHexlify.ts @@ -0,0 +1,35 @@ +import { toHex } from "viem" + +export const transactionReceiptStatus = { + "0x0": "reverted", + "0x1": "success" +} as const + +// biome-ignore lint/suspicious/noExplicitAny: it's a recursive function, so it's hard to type +export function deepHexlify(obj: any): any { + if (typeof obj === "function") { + return undefined + } + if (obj == null || typeof obj === "string" || typeof obj === "boolean") { + return obj + } + + if (typeof obj === "bigint") { + return toHex(obj) + } + + if (obj._isBigNumber != null || typeof obj !== "object") { + return toHex(obj).replace(/^0x0/, "0x") + } + if (Array.isArray(obj)) { + return obj.map((member) => deepHexlify(member)) + } + return Object.keys(obj).reduce( + // biome-ignore lint/suspicious/noExplicitAny: it's a recursive function, so it's hard to type + (set: any, key: string) => { + set[key] = deepHexlify(obj[key]) + return set + }, + {} + ) +} diff --git a/src/utils/errors/getBundlerError.ts b/src/utils/errors/getBundlerError.ts new file mode 100644 index 0000000..eeadf0f --- /dev/null +++ b/src/utils/errors/getBundlerError.ts @@ -0,0 +1,224 @@ +import { + type Address, + BaseError, + ExecutionRevertedError, + type ExecutionRevertedErrorType, + UnknownNodeError, + type UnknownNodeErrorType +} from "viem" +import { SenderAlreadyDeployedError } from "../../errors" +import { + InitCodeDidNotDeploySenderError, + type InitCodeDidNotDeploySenderErrorType, + InitCodeRevertedError, + type InitCodeRevertedErrorType, + InvalidSmartAccountNonceError, + type InvalidSmartAccountNonceErrorType, + SenderAddressMismatchError, + type SenderAddressMismatchErrorType, + type SenderAlreadyDeployedErrorType, + SenderNotDeployedError, + type SenderNotDeployedErrorType, + SmartAccountInsufficientFundsError, + type SmartAccountInsufficientFundsErrorType, + SmartAccountSignatureValidityPeriodError, + type SmartAccountSignatureValidityPeriodErrorType, + SmartAccountValidationRevertedError, + type SmartAccountValidationRevertedErrorType +} from "../../errors/account" +import { + PaymasterDataRejectedError, + type PaymasterDataRejectedErrorType, + PaymasterDepositTooLowError, + type PaymasterDepositTooLowErrorType, + PaymasterNotDeployedError, + type PaymasterNotDeployedErrorType, + PaymasterValidationRevertedError, + type PaymasterValidationRevertedErrorType, + PaymasterValidityPeriodError, + type PaymasterValidityPeriodErrorType +} from "../../errors/paymaster" +import type { + EntryPoint, + GetEntryPointVersion, + UserOperation +} from "../../types" + +export type GetBundlerErrorParameters = { + userOperation: Partial>> + entryPoint: Address +} + +export type GetBundlerErrorReturnType = + | ExecutionRevertedErrorType + | UnknownNodeErrorType + | SenderAlreadyDeployedErrorType + | InitCodeRevertedErrorType + | SenderAddressMismatchErrorType + | InitCodeDidNotDeploySenderErrorType + | SenderNotDeployedErrorType + | SmartAccountInsufficientFundsErrorType + | SmartAccountSignatureValidityPeriodErrorType + | SmartAccountValidationRevertedErrorType + | InvalidSmartAccountNonceErrorType + | PaymasterNotDeployedErrorType + | PaymasterDepositTooLowErrorType + | PaymasterValidityPeriodErrorType + | PaymasterValidationRevertedErrorType + | PaymasterDataRejectedErrorType + +export function getBundlerError( + err: BaseError, + args: GetBundlerErrorParameters +) { + const message = (err.details || "").toLowerCase() + + const executionRevertedError = + err instanceof BaseError + ? err.walk( + (e) => + (e as { code: number }).code === + ExecutionRevertedError.code + ) + : err + + if (executionRevertedError instanceof BaseError) { + return new ExecutionRevertedError({ + cause: err, + message: executionRevertedError.details + }) as ExecutionRevertedErrorType + } + + // TODO: Add validation Errors + if (args.userOperation.sender === undefined) + return new UnknownNodeError({ cause: err }) + if (args.userOperation.nonce === undefined) + return new UnknownNodeError({ cause: err }) + + if (SenderAlreadyDeployedError.message.test(message)) { + return new SenderAlreadyDeployedError({ + cause: err, + sender: args.userOperation.sender, + docsPath: + "https://docs.pimlico.io/bundler/reference/entrypoint-errors/aa10" + }) as SenderAlreadyDeployedErrorType + } + + if (InitCodeRevertedError.message.test(message)) { + return new InitCodeRevertedError({ + cause: err, + docsPath: + "https://docs.pimlico.io/bundler/reference/entrypoint-errors/aa13" + }) as InitCodeRevertedErrorType + } + + if (SenderAddressMismatchError.message.test(message)) { + return new SenderAddressMismatchError({ + cause: err, + sender: args.userOperation.sender, + docsPath: + "https://docs.pimlico.io/bundler/reference/entrypoint-errors/aa14" + }) as SenderAddressMismatchErrorType + } + + if (InitCodeDidNotDeploySenderError.message.test(message)) { + return new InitCodeDidNotDeploySenderError({ + cause: err, + sender: args.userOperation.sender, + docsPath: + "https://docs.pimlico.io/bundler/reference/entrypoint-errors/aa15" + }) as InitCodeDidNotDeploySenderErrorType + } + + if (SenderNotDeployedError.message.test(message)) { + return new SenderNotDeployedError({ + cause: err, + sender: args.userOperation.sender, + docsPath: + "https://docs.pimlico.io/bundler/reference/entrypoint-errors/aa20" + }) as SenderNotDeployedErrorType + } + + if (SmartAccountInsufficientFundsError.message.test(message)) { + return new SmartAccountInsufficientFundsError({ + cause: err, + sender: args.userOperation.sender, + docsPath: + "https://docs.pimlico.io/bundler/reference/entrypoint-errors/aa21" + }) as SmartAccountInsufficientFundsErrorType + } + + if (SmartAccountSignatureValidityPeriodError.message.test(message)) { + return new SmartAccountSignatureValidityPeriodError({ + cause: err, + docsPath: + "https://docs.pimlico.io/bundler/reference/entrypoint-errors/aa22" + }) as SmartAccountSignatureValidityPeriodErrorType + } + + if (SmartAccountValidationRevertedError.message.test(message)) { + return new SmartAccountValidationRevertedError({ + cause: err, + sender: args.userOperation.sender, + docsPath: + "https://docs.pimlico.io/bundler/reference/entrypoint-errors/aa23" + }) as SmartAccountValidationRevertedErrorType + } + + if (InvalidSmartAccountNonceError.message.test(message)) { + return new InvalidSmartAccountNonceError({ + cause: err, + sender: args.userOperation.sender, + nonce: args.userOperation.nonce, + docsPath: + "https://docs.pimlico.io/bundler/reference/entrypoint-errors/aa25" + }) as InvalidSmartAccountNonceErrorType + } + + if (PaymasterNotDeployedError.message.test(message)) { + return new PaymasterNotDeployedError({ + cause: err, + paymasterAndData: args.userOperation.paymasterAndData, + docsPath: + "https://docs.pimlico.io/bundler/reference/entrypoint-errors/aa30" + }) as PaymasterNotDeployedErrorType + } + + if (PaymasterDepositTooLowError.message.test(message)) { + return new PaymasterDepositTooLowError({ + cause: err, + paymasterAndData: args.userOperation.paymasterAndData, + docsPath: + "https://docs.pimlico.io/bundler/reference/entrypoint-errors/aa31" + }) as PaymasterDepositTooLowErrorType + } + + if (PaymasterValidityPeriodError.message.test(message)) { + return new PaymasterValidityPeriodError({ + cause: err, + paymasterAndData: args.userOperation.paymasterAndData, + docsPath: + "https://docs.pimlico.io/bundler/reference/entrypoint-errors/aa32" + }) as PaymasterValidityPeriodErrorType + } + + if (PaymasterValidationRevertedError.message.test(message)) { + return new PaymasterValidationRevertedError({ + cause: err, + paymasterAndData: args.userOperation.paymasterAndData, + docsPath: + "https://docs.pimlico.io/bundler/reference/entrypoint-errors/aa33" + }) as PaymasterValidationRevertedErrorType + } + + if (PaymasterDataRejectedError.message.test(message)) { + return new PaymasterDataRejectedError({ + cause: err, + paymasterAndData: args.userOperation.paymasterAndData, + docsPath: + "https://docs.pimlico.io/bundler/reference/entrypoint-errors/aa34" + }) as PaymasterDataRejectedErrorType + } + + return new UnknownNodeError({ cause: err }) as UnknownNodeErrorType +} diff --git a/src/utils/errors/getEstimateUserOperationGasError.ts b/src/utils/errors/getEstimateUserOperationGasError.ts new file mode 100644 index 0000000..410759c --- /dev/null +++ b/src/utils/errors/getEstimateUserOperationGasError.ts @@ -0,0 +1,57 @@ +import { BaseError, UnknownNodeError } from "viem" + +import { + EstimateUserOperationGasError, + type EstimateUserOperationGasErrorType +} from "../../errors/estimateUserOperationGas" +import { type ErrorType } from "../../errors/utils" +import type { EntryPoint, GetEntryPointVersion } from "../../types/entrypoint" +import { + type GetBundlerErrorParameters, + type GetBundlerErrorReturnType, + getBundlerError +} from "./getBundlerError" +import { PartialBy, UserOperation } from "@/types" +export type EstimateUserOperationGasParameters = + { + userOperation: GetEntryPointVersion extends "v0.6" + ? PartialBy< + UserOperation<"v0.6">, + "callGasLimit" | "preVerificationGas" | "verificationGasLimit" + > + : PartialBy< + UserOperation<"v0.7">, + | "callGasLimit" + | "preVerificationGas" + | "verificationGasLimit" + | "paymasterVerificationGasLimit" + | "paymasterPostOpGasLimit" + > + entryPoint: entryPoint + } +export type GetEstimateUserOperationGasErrorReturnType< + entryPoint extends EntryPoint, + cause = ErrorType +> = Omit, "cause"> & { + cause: cause | GetBundlerErrorReturnType +} + +export function getEstimateUserOperationGasError< + err extends ErrorType, + entryPoint extends EntryPoint +>(error: err, args: EstimateUserOperationGasParameters) { + const cause = (() => { + const cause = getBundlerError( + // biome-ignore lint/complexity/noBannedTypes: + error as {} as BaseError, + args as GetBundlerErrorParameters + ) + // biome-ignore lint/complexity/noBannedTypes: + if (cause instanceof UnknownNodeError) return error as {} as BaseError + return cause + })() + + throw new EstimateUserOperationGasError(cause, { + ...args + }) as GetEstimateUserOperationGasErrorReturnType +} diff --git a/src/utils/errors/getSendUserOperationError.ts b/src/utils/errors/getSendUserOperationError.ts new file mode 100644 index 0000000..0718b53 --- /dev/null +++ b/src/utils/errors/getSendUserOperationError.ts @@ -0,0 +1,26 @@ +import { BaseError, UnknownNodeError } from "viem" + +import { SendUserOperationError } from "../../errors" +import type { EntryPoint } from "../../types/entrypoint" +import { + type GetBundlerErrorParameters, + getBundlerError +} from "./getBundlerError" + +export function getSendUserOperationError( + err: BaseError, + args: any +) { + const cause = (() => { + const cause = getBundlerError( + err as BaseError, + args as GetBundlerErrorParameters + ) + if (cause instanceof UnknownNodeError) return err as BaseError + return cause + })() + + throw new SendUserOperationError(cause, { + ...args + }) +} diff --git a/src/utils/getAddressFromInitCodeOrPaymasterAndData.ts b/src/utils/getAddressFromInitCodeOrPaymasterAndData.ts new file mode 100644 index 0000000..7ba50e6 --- /dev/null +++ b/src/utils/getAddressFromInitCodeOrPaymasterAndData.ts @@ -0,0 +1,13 @@ +import { type Address, type Hex, getAddress } from "viem" + +export function getAddressFromInitCodeOrPaymasterAndData( + data: Hex +): Address | undefined { + if (!data) { + return undefined + } + if (data.length >= 42) { + return getAddress(data.slice(0, 42)) + } + return undefined +} diff --git a/src/utils/getEntryPointVersion.ts b/src/utils/getEntryPointVersion.ts new file mode 100644 index 0000000..c6a1e3f --- /dev/null +++ b/src/utils/getEntryPointVersion.ts @@ -0,0 +1,31 @@ +import type { + ENTRYPOINT_ADDRESS_V06_TYPE, + ENTRYPOINT_ADDRESS_V07_TYPE, + UserOperation +} from "../types" +import type { EntryPoint, GetEntryPointVersion } from "../types/entrypoint" + +export const ENTRYPOINT_ADDRESS_V06: ENTRYPOINT_ADDRESS_V06_TYPE = + "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789" +export const ENTRYPOINT_ADDRESS_V07: ENTRYPOINT_ADDRESS_V07_TYPE = + "0x0000000071727De22E5E9d8BAf0edAc6f37da032" + +export const getEntryPointVersion = ( + entryPoint: EntryPoint +): GetEntryPointVersion => + entryPoint === ENTRYPOINT_ADDRESS_V06 ? "v0.6" : "v0.7" + +export function isUserOperationVersion06( + entryPoint: EntryPoint, + _operation: UserOperation<"v0.6"> | UserOperation<"v0.7"> +): _operation is UserOperation<"v0.6"> { + return getEntryPointVersion(entryPoint) === "v0.6" +} + +// Type predicate to check if the UserOperation is V07. +export function isUserOperationVersion07( + entryPoint: EntryPoint, + _operation: UserOperation<"v0.6"> | UserOperation<"v0.7"> +): _operation is UserOperation<"v0.7"> { + return getEntryPointVersion(entryPoint) === "v0.7" +} diff --git a/src/utils/getPackedUserOperation.ts b/src/utils/getPackedUserOperation.ts new file mode 100644 index 0000000..241cebd --- /dev/null +++ b/src/utils/getPackedUserOperation.ts @@ -0,0 +1,120 @@ +import { type Hex, concat, getAddress, pad, slice, toHex } from "viem" +import type { UserOperation } from "../types/userOperation" +import type { Hex32, PackedUserOperation } from "../types/userOperation" + +export function getInitCode(unpackedUserOperation: UserOperation<"v0.7">) { + return unpackedUserOperation.factory + ? concat([ + unpackedUserOperation.factory, + unpackedUserOperation.factoryData || ("0x" as Hex) + ]) + : "0x" +} + +export function unPackInitCode(initCode: Hex) { + if (initCode === "0x") { + return { + factory: null, + factoryData: null + } + } + return { + factory: getAddress(slice(initCode, 0, 20)), + factoryData: slice(initCode, 20) + } +} + +export function getAccountGasLimits( + unpackedUserOperation: UserOperation<"v0.7"> +) { + return concat([ + pad(toHex(unpackedUserOperation.verificationGasLimit), { + size: 16 + }), + pad(toHex(unpackedUserOperation.callGasLimit), { size: 16 }) + ]) as Hex32 +} + +export function unpackAccountGasLimits(accountGasLimits: Hex) { + return { + verificationGasLimit: BigInt(slice(accountGasLimits, 0, 16)), + callGasLimit: BigInt(slice(accountGasLimits, 16)) + } +} + +export function getGasLimits(unpackedUserOperation: UserOperation<"v0.7">) { + return concat([ + pad(toHex(unpackedUserOperation.maxPriorityFeePerGas), { + size: 16 + }), + pad(toHex(unpackedUserOperation.maxFeePerGas), { size: 16 }) + ]) as Hex32 +} + +export function unpackGasLimits(gasLimits: Hex) { + return { + maxPriorityFeePerGas: BigInt(slice(gasLimits, 0, 16)), + maxFeePerGas: BigInt(slice(gasLimits, 16)) + } +} + +export function getPaymasterAndData( + unpackedUserOperation: UserOperation<"v0.7"> +) { + return unpackedUserOperation.paymaster + ? concat([ + unpackedUserOperation.paymaster, + pad( + toHex( + unpackedUserOperation.paymasterVerificationGasLimit || + BigInt(0) + ), + { + size: 16 + } + ), + pad( + toHex( + unpackedUserOperation.paymasterPostOpGasLimit || BigInt(0) + ), + { + size: 16 + } + ), + unpackedUserOperation.paymasterData || ("0x" as Hex) + ]) + : "0x" +} + +export function unpackPaymasterAndData(paymasterAndData: Hex) { + if (paymasterAndData === "0x") { + return { + paymaster: null, + paymasterVerificationGasLimit: null, + paymasterPostOpGasLimit: null, + paymasterData: null + } + } + return { + paymaster: getAddress(slice(paymasterAndData, 0, 20)), + paymasterVerificationGasLimit: BigInt(slice(paymasterAndData, 20, 36)), + paymasterPostOpGasLimit: BigInt(slice(paymasterAndData, 36, 52)), + paymasterData: slice(paymasterAndData, 52) + } +} + +export const getPackedUserOperation = ( + userOperation: UserOperation<"v0.7"> +): PackedUserOperation => { + return { + sender: userOperation.sender, + nonce: userOperation.nonce, + initCode: getInitCode(userOperation), + callData: userOperation.callData, + accountGasLimits: getAccountGasLimits(userOperation), + preVerificationGas: userOperation.preVerificationGas, + gasFees: getGasLimits(userOperation), + paymasterAndData: getPaymasterAndData(userOperation), + signature: userOperation.signature + } +} diff --git a/src/utils/getRequiredPrefund.ts b/src/utils/getRequiredPrefund.ts new file mode 100644 index 0000000..e60ed0f --- /dev/null +++ b/src/utils/getRequiredPrefund.ts @@ -0,0 +1,57 @@ +import type { EntryPoint, GetEntryPointVersion, UserOperation } from "../types" +import { ENTRYPOINT_ADDRESS_V06 } from "./getEntryPointVersion" + +export type GetRequiredPrefundReturnType = { + userOperation: UserOperation> + entryPoint: entryPoint +} + +/** + * + * Returns the minimum required funds in the senders's smart account to execute the user operation. + * + * @param arags: {userOperation} as {@link UserOperation} + * @returns requiredPrefund as {@link bigint} + * + * @example + * import { getRequiredPrefund } from "permissionless/utils" + * + * const requiredPrefund = getRequiredPrefund({ + * userOperation + * }) + */ +export const getRequiredPrefund = ({ + userOperation, + entryPoint: entryPointAddress +}: GetRequiredPrefundReturnType): bigint => { + if (entryPointAddress === ENTRYPOINT_ADDRESS_V06) { + const userOperationVersion0_6 = userOperation as UserOperation<"v0.6"> + const multiplier = + userOperationVersion0_6.paymasterAndData.length > 2 + ? BigInt(3) + : BigInt(1) + const requiredGas = + userOperationVersion0_6.callGasLimit + + userOperationVersion0_6.verificationGasLimit * multiplier + + userOperationVersion0_6.preVerificationGas + + return ( + BigInt(requiredGas) * BigInt(userOperationVersion0_6.maxFeePerGas) + ) + } + + const userOperationV07 = userOperation as UserOperation<"v0.7"> + const multiplier = userOperationV07.paymaster ? BigInt(3) : BigInt(1) + + const verificationGasLimit = + userOperationV07.verificationGasLimit + + (userOperationV07.paymasterPostOpGasLimit || BigInt(0)) + + (userOperationV07.paymasterVerificationGasLimit || BigInt(0)) + + const requiredGas = + userOperationV07.callGasLimit + + verificationGasLimit * multiplier + + userOperationV07.preVerificationGas + + return BigInt(requiredGas) * BigInt(userOperationV07.maxFeePerGas) +} diff --git a/src/utils/getUserOperationHash.ts b/src/utils/getUserOperationHash.ts new file mode 100644 index 0000000..de3ea6e --- /dev/null +++ b/src/utils/getUserOperationHash.ts @@ -0,0 +1,158 @@ +import type { Address, Hash, Hex } from "viem" +import { concat, encodeAbiParameters, keccak256, pad, toHex } from "viem" +import type { EntryPoint, GetEntryPointVersion } from "../types" +import type { UserOperation } from "../types/userOperation" +import { isUserOperationVersion06 } from "./getEntryPointVersion" + +function packUserOp({ + userOperation, + entryPoint: entryPointAddress +}: { + userOperation: UserOperation> + entryPoint: entryPoint +}): Hex { + if (isUserOperationVersion06(entryPointAddress, userOperation)) { + const hashedInitCode = keccak256(userOperation.initCode) + const hashedCallData = keccak256(userOperation.callData) + const hashedPaymasterAndData = keccak256(userOperation.paymasterAndData) + + return encodeAbiParameters( + [ + { type: "address" }, + { type: "uint256" }, + { type: "bytes32" }, + { type: "bytes32" }, + { type: "uint256" }, + { type: "uint256" }, + { type: "uint256" }, + { type: "uint256" }, + { type: "uint256" }, + { type: "bytes32" } + ], + [ + userOperation.sender as Address, + userOperation.nonce, + hashedInitCode, + hashedCallData, + userOperation.callGasLimit, + userOperation.verificationGasLimit, + userOperation.preVerificationGas, + userOperation.maxFeePerGas, + userOperation.maxPriorityFeePerGas, + hashedPaymasterAndData + ] + ) + } + + const hashedInitCode = keccak256( + userOperation.factory && userOperation.factoryData + ? concat([userOperation.factory, userOperation.factoryData]) + : "0x" + ) + const hashedCallData = keccak256(userOperation.callData) + const hashedPaymasterAndData = keccak256( + userOperation.paymaster + ? concat([ + userOperation.paymaster, + pad( + toHex( + userOperation.paymasterVerificationGasLimit || + BigInt(0) + ), + { + size: 16 + } + ), + pad( + toHex(userOperation.paymasterPostOpGasLimit || BigInt(0)), + { + size: 16 + } + ), + userOperation.paymasterData || "0x" + ]) + : "0x" + ) + + return encodeAbiParameters( + [ + { type: "address" }, + { type: "uint256" }, + { type: "bytes32" }, + { type: "bytes32" }, + { type: "bytes32" }, + { type: "uint256" }, + { type: "bytes32" }, + { type: "bytes32" } + ], + [ + userOperation.sender as Address, + userOperation.nonce, + hashedInitCode, + hashedCallData, + concat([ + pad(toHex(userOperation.verificationGasLimit), { + size: 16 + }), + pad(toHex(userOperation.callGasLimit), { size: 16 }) + ]), + userOperation.preVerificationGas, + concat([ + pad(toHex(userOperation.maxPriorityFeePerGas), { + size: 16 + }), + pad(toHex(userOperation.maxFeePerGas), { size: 16 }) + ]), + hashedPaymasterAndData + ] + ) +} + +export type GetUserOperationHashParams = { + userOperation: UserOperation> + entryPoint: entryPoint + chainId: number +} + +/** + * + * Returns user operation hash that is a unique identifier of the user operation. + * + * - Docs: https://docs.pimlico.io/permissionless/reference/utils/getUserOperationHash + * + * @param args: userOperation, entryPoint, chainId as {@link GetUserOperationHashParams} + * @returns userOperationHash as {@link Hash} + * + * @example + * import { getUserOperationHash } from "permissionless/utils" + * + * const userOperationHash = getUserOperationHash({ + * userOperation, + * entryPoint, + * chainId + * }) + * + * // Returns "0xe9fad2cd67f9ca1d0b7a6513b2a42066784c8df938518da2b51bb8cc9a89ea34" + * + */ +export const getUserOperationHash = ({ + userOperation, + entryPoint: entryPointAddress, + chainId +}: GetUserOperationHashParams): Hash => { + const encoded = encodeAbiParameters( + [{ type: "bytes32" }, { type: "address" }, { type: "uint256" }], + [ + keccak256( + packUserOp({ + userOperation, + entryPoint: entryPointAddress + }) + ), + entryPointAddress, + BigInt(chainId) + ] + ) as `0x${string}` + + return keccak256(encoded) +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..99d5896 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,52 @@ +import type { Account, Address } from "viem" +import { deepHexlify, transactionReceiptStatus } from "./deepHexlify" +import { getAddressFromInitCodeOrPaymasterAndData } from "./getAddressFromInitCodeOrPaymasterAndData" +import { + type GetRequiredPrefundReturnType, + getRequiredPrefund +} from "./getRequiredPrefund" +import { + type GetUserOperationHashParams, + getUserOperationHash +} from "./getUserOperationHash" +import { isSmartAccountDeployed } from "./isSmartAccountDeployed" +import { providerToSmartAccountSigner } from "./providerToSmartAccountSigner" +import { + AccountOrClientNotFoundError, + type SignUserOperationHashWithECDSAParams, + signUserOperationHashWithECDSA +} from "./signUserOperationHashWithECDSA" +import { walletClientToSmartAccountSigner } from "./walletClientToSmartAccountSigner" + +export function parseAccount(account: Address | Account): Account { + if (typeof account === "string") + return { address: account, type: "json-rpc" } + return account +} +import { + ENTRYPOINT_ADDRESS_V06, + ENTRYPOINT_ADDRESS_V07, + getEntryPointVersion +} from "./getEntryPointVersion" + +import { getPackedUserOperation } from "./getPackedUserOperation" + +export { + transactionReceiptStatus, + deepHexlify, + getUserOperationHash, + getRequiredPrefund, + walletClientToSmartAccountSigner, + type GetRequiredPrefundReturnType, + type GetUserOperationHashParams, + signUserOperationHashWithECDSA, + type SignUserOperationHashWithECDSAParams, + AccountOrClientNotFoundError, + isSmartAccountDeployed, + providerToSmartAccountSigner, + getAddressFromInitCodeOrPaymasterAndData, + getPackedUserOperation, + getEntryPointVersion, + ENTRYPOINT_ADDRESS_V06, + ENTRYPOINT_ADDRESS_V07 +} diff --git a/src/utils/isSmartAccountDeployed.ts b/src/utils/isSmartAccountDeployed.ts new file mode 100644 index 0000000..b39a4c6 --- /dev/null +++ b/src/utils/isSmartAccountDeployed.ts @@ -0,0 +1,16 @@ +import type { Address, Client } from "viem" +import { getBytecode } from "viem/actions" + +export const isSmartAccountDeployed = async ( + client: Client, + address: Address +): Promise => { + const contractCode = await getBytecode(client, { + address: address + }) + + if ((contractCode?.length ?? 0) > 2) { + return true + } + return false +} diff --git a/src/utils/observe.ts b/src/utils/observe.ts new file mode 100644 index 0000000..b4b719a --- /dev/null +++ b/src/utils/observe.ts @@ -0,0 +1,74 @@ +import type { MaybePromise } from "viem/types/utils" + +// biome-ignore lint/suspicious/noExplicitAny: it's a recursive function, so it's hard to type +type Callback = ((...args: any[]) => any) | undefined +type Callbacks = Record + +export const listenersCache = /*#__PURE__*/ new Map< + string, + { id: number; fns: Callbacks }[] +>() +export const cleanupCache = /*#__PURE__*/ new Map void>() + +type EmitFunction = ( + emit: TCallbacks + // biome-ignore lint/suspicious/noConfusingVoidType: +) => MaybePromise void)> + +let callbackCount = 0 + +/** + * @description Sets up an observer for a given function. If another function + * is set up under the same observer id, the function will only be called once + * for both instances of the observer. + */ +export function observe( + observerId: string, + callbacks: TCallbacks, + fn: EmitFunction +) { + const callbackId = ++callbackCount + + const getListeners = () => listenersCache.get(observerId) || [] + + const unsubscribe = () => { + const listeners = getListeners() + listenersCache.set( + observerId, + // biome-ignore lint/suspicious/noExplicitAny: it's a recursive function, so it's hard to type + listeners.filter((cb: any) => cb.id !== callbackId) + ) + } + + const unwatch = () => { + const cleanup = cleanupCache.get(observerId) + if (getListeners().length === 1 && cleanup) cleanup() + unsubscribe() + } + + const listeners = getListeners() + listenersCache.set(observerId, [ + ...listeners, + { id: callbackId, fns: callbacks } + ]) + + if (listeners && listeners.length > 0) return unwatch + + const emit: TCallbacks = {} as TCallbacks + for (const key in callbacks) { + emit[key] = (( + ...args: Parameters> + ) => { + const listeners = getListeners() + if (listeners.length === 0) return + for (const listener of listeners) { + listener.fns[key]?.(...args) + } + }) as TCallbacks[Extract] + } + + const cleanup = fn(emit) + if (typeof cleanup === "function") cleanupCache.set(observerId, cleanup) + + return unwatch +} diff --git a/src/utils/providerToSmartAccountSigner.ts b/src/utils/providerToSmartAccountSigner.ts new file mode 100644 index 0000000..21584b2 --- /dev/null +++ b/src/utils/providerToSmartAccountSigner.ts @@ -0,0 +1,34 @@ +import { + type EIP1193Provider, + type Hex, + createWalletClient, + custom +} from "viem" +import { walletClientToSmartAccountSigner } from "./walletClientToSmartAccountSigner" + +export const providerToSmartAccountSigner = async ( + provider: EIP1193Provider, + params?: { + signerAddress: Hex + } +) => { + let account: Hex + if (!params) { + try { + ;[account] = await provider.request({ + method: "eth_requestAccounts" + }) + } catch { + ;[account] = await provider.request({ + method: "eth_accounts" + }) + } + } else { + account = params.signerAddress + } + const walletClient = createWalletClient({ + account: account as Hex, + transport: custom(provider) + }) + return walletClientToSmartAccountSigner(walletClient) +} diff --git a/src/utils/signUserOperationHashWithECDSA.ts b/src/utils/signUserOperationHashWithECDSA.ts new file mode 100644 index 0000000..e7ab68c --- /dev/null +++ b/src/utils/signUserOperationHashWithECDSA.ts @@ -0,0 +1,134 @@ +import { + type Account, + BaseError, + type Chain, + type Client, + type Hash, + type Hex, + type Transport +} from "viem" +import type { + EntryPoint, + GetAccountParameterWithClient, + GetEntryPointVersion +} from "../types/" +import type { UserOperation } from "../types/userOperation" +import { parseAccount } from "./" +import { getUserOperationHash } from "./getUserOperationHash" + +export type SignUserOperationHashWithECDSAParams< + entryPoint extends EntryPoint, + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined, + TAccount extends Account | undefined = Account | undefined +> = GetAccountParameterWithClient & + ( + | { + hash: Hash + userOperation?: undefined + entryPoint?: undefined + chainId?: undefined + } + | { + hash?: undefined + userOperation: UserOperation> + entryPoint: entryPoint + chainId: number + } + ) + +export class AccountOrClientNotFoundError extends BaseError { + override name = "AccountOrClientNotFoundError" + constructor({ docsPath }: { docsPath?: string } = {}) { + super( + [ + "Could not find an Account to execute with this Action.", + "Please provide an Account with the `account` argument on the Action, or by supplying an `account` to the WalletClient." + ].join("\n"), + { + docsPath, + docsSlug: "account" + } + ) + } +} + +/** + * + * Returns signature for user operation. It signs over user operation hash. + * If you have a custom way of signing user operation hash, you can use this function to sign it with ECDSA. + * + * - Docs: https://docs.pimlico.io/permissionless/reference/utils/signUserOperationHashWithECDSA + * + * @param signer: owner as {@link Client} + * @param params: account & (userOperation, entryPoint, chainId) | hash to sign + * @returns signature as {@link Hash} + * + * @example + * import { signUserOperationHashWithECDSA } from "permissionless/utils" + * + * const userOperationSignature = signUserOperationHashWithECDSA(owner, { + * userOperation, + * entryPoint, + * chainId + * }) + * + * // Returns "0x7d9ae17d5e617e4bf3221dfcb69d64d824959e5ae2ef7078c6ddc3a4fe26c8301ab39277c61160dca68ca90071eb449d9fb2fbbc78b3614d9d7282c860270e291c" + * + */ +export const signUserOperationHashWithECDSA = async < + entryPoint extends EntryPoint, + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined, + TAccount extends Account | undefined = Account | undefined +>({ + client, + account: account_ = client?.account, + hash, + userOperation, + chainId, + entryPoint: entryPointAddress +}: SignUserOperationHashWithECDSAParams< + entryPoint, + TTransport, + TChain, + TAccount +>): Promise => { + if (!account_) + throw new AccountOrClientNotFoundError({ + docsPath: + "/permissionless/reference/utils/signUserOperationHashWithECDSA" + }) + + let userOperationHash: Hash + + if (hash) { + userOperationHash = hash + } else { + userOperationHash = getUserOperationHash({ + userOperation, + chainId, + entryPoint: entryPointAddress + }) + } + + const account = parseAccount(account_) + + if (account.type === "local") + return account.signMessage({ + message: { + raw: userOperationHash + } + }) + + if (!client) + throw new AccountOrClientNotFoundError({ + docsPath: + "/permissionless/reference/utils/signUserOperationHashWithECDSA" + }) + + return client.request({ + method: "personal_sign", + params: [userOperationHash, account.address] + }) +} diff --git a/src/utils/walletClientToSmartAccountSigner.ts b/src/utils/walletClientToSmartAccountSigner.ts new file mode 100644 index 0000000..6edbdc2 --- /dev/null +++ b/src/utils/walletClientToSmartAccountSigner.ts @@ -0,0 +1,46 @@ +import type { + Account, + Address, + Chain, + Hex, + SignableMessage, + Transport, + TypedData, + TypedDataDefinition, + WalletClient +} from "viem" + +import { signTypedData } from "viem/actions" +import type { SmartAccountSigner } from "../accounts/types" + +export function walletClientToSmartAccountSigner< + TChain extends Chain | undefined = Chain | undefined +>( + walletClient: WalletClient +): SmartAccountSigner<"custom", Address> { + return { + address: walletClient.account.address, + type: "local", + source: "custom", + publicKey: walletClient.account.address, + signMessage: async ({ + message + }: { message: SignableMessage }): Promise => { + return walletClient.signMessage({ message }) + }, + async signTypedData< + const TTypedData extends TypedData | Record, + TPrimaryType extends + | keyof TTypedData + | "EIP712Domain" = keyof TTypedData + >(typedData: TypedDataDefinition) { + return signTypedData( + walletClient, + { + account: walletClient.account, + ...typedData + } + ) + } + } +}