@zerodev/sdk
Version:
A utility library for working with ERC-4337
886 lines (832 loc) • 29.8 kB
text/typescript
import { satisfies } from "semver"
import {
type Address,
type Assign,
type EncodeDeployDataParameters,
type Hex,
type LocalAccount,
type SignableMessage,
type TypedDataDefinition,
concatHex,
createNonceManager,
encodeFunctionData,
getTypesForEIP712Domain,
hashMessage,
hashTypedData,
isAddressEqual,
toHex,
validateTypedData,
zeroAddress
} from "viem"
import {
type EntryPointVersion,
type SmartAccount,
type SmartAccountImplementation,
type UserOperation,
entryPoint06Abi,
entryPoint07Abi,
entryPoint07Address,
toSmartAccount
} from "viem/account-abstraction"
import type {
PrivateKeyAccount,
SignAuthorizationReturnType
} from "viem/accounts"
import {
getChainId,
getCode,
signAuthorization as signAuthorizationAction
} from "viem/actions"
import { getAction, verifyAuthorization } from "viem/utils"
import {
getAccountNonce,
getSenderAddress,
isPluginInstalled
} from "../../actions/public/index.js"
import {
KernelVersionToAddressesMap,
MAGIC_VALUE_SIG_REPLAYABLE
} from "../../constants.js"
import type {
CallType,
EntryPointType,
GetEntryPointAbi,
GetKernelVersion,
KernelPluginManager,
KernelPluginManagerParams,
KernelValidator,
PluginMigrationData
} from "../../types/kernel.js"
import type { Signer } from "../../types/utils.js"
import { KERNEL_FEATURES, hasKernelFeature } from "../../utils.js"
import { validateKernelVersionWithEntryPoint } from "../../utils.js"
import { toSigner } from "../../utils/toSigner.js"
import { addressToEmptyAccount } from "../addressToEmptyAccount.js"
import { signerTo7702Validator } from "../utils/signerTo7702Validator.js"
import {
isKernelPluginManager,
toKernelPluginManager
} from "../utils/toKernelPluginManager.js"
import { KernelInitAbi } from "./abi/KernelAccountAbi.js"
import { KernelV3InitAbi } from "./abi/kernel_v_3_0_0/KernelAccountAbi.js"
import { KernelV3FactoryAbi } from "./abi/kernel_v_3_0_0/KernelFactoryAbi.js"
import { KernelFactoryStakerAbi } from "./abi/kernel_v_3_0_0/KernelFactoryStakerAbi.js"
import { KernelV3_1AccountAbi } from "./abi/kernel_v_3_1/KernelAccountAbi.js"
import { encodeCallData as encodeCallDataEpV06 } from "./utils/account/ep0_6/encodeCallData.js"
import { encodeDeployCallData as encodeDeployCallDataV06 } from "./utils/account/ep0_6/encodeDeployCallData.js"
import { encodeCallData as encodeCallDataEpV07 } from "./utils/account/ep0_7/encodeCallData.js"
import { encodeDeployCallData as encodeDeployCallDataV07 } from "./utils/account/ep0_7/encodeDeployCallData.js"
import {
type AccountMetadata,
accountMetadata
} from "./utils/common/accountMetadata.js"
import { eip712WrapHash } from "./utils/common/eip712WrapHash.js"
import { getPluginInstallCallData } from "./utils/plugins/ep0_7/getPluginInstallCallData.js"
import type { CallArgs } from "./utils/types.js"
type SignMessageParameters = {
message: SignableMessage
useReplayableSignature?: boolean
}
export type KernelSmartAccountImplementation<
entryPointVersion extends EntryPointVersion = "0.7"
> = Assign<
SmartAccountImplementation<
GetEntryPointAbi<entryPointVersion>,
entryPointVersion
>,
{
sign: NonNullable<SmartAccountImplementation["sign"]>
encodeCalls: (
calls: Parameters<SmartAccountImplementation["encodeCalls"]>[0],
callType?: CallType | undefined
) => Promise<Hex>
kernelVersion: GetKernelVersion<entryPointVersion>
kernelPluginManager: KernelPluginManager<entryPointVersion>
factoryAddress: Address
accountImplementationAddress: Address
generateInitCode: () => Promise<Hex>
encodeModuleInstallCallData: () => Promise<Hex>
encodeDeployCallData: ({
abi,
args,
bytecode
}: EncodeDeployDataParameters) => Promise<Hex>
signMessage: (parameters: SignMessageParameters) => Promise<Hex>
eip7702Authorization?:
| (() => Promise<SignAuthorizationReturnType | undefined>)
| undefined
}
>
export type CreateKernelAccountReturnType<
entryPointVersion extends EntryPointVersion = "0.7"
> = SmartAccount<KernelSmartAccountImplementation<entryPointVersion>>
export type CreateKernelAccountParameters<
entryPointVersion extends EntryPointVersion,
KernelVerion extends GetKernelVersion<entryPointVersion>
> = {
entryPoint: EntryPointType<entryPointVersion>
index?: bigint
factoryAddress?: Address
accountImplementationAddress?: Address
metaFactoryAddress?: Address
address?: Address
kernelVersion: GetKernelVersion<entryPointVersion>
initConfig?: KernelVerion extends "0.3.1" ? Hex[] : never
useMetaFactory?: boolean
pluginMigrations?: PluginMigrationData[]
} & (
| {
eip7702Auth?: SignAuthorizationReturnType | undefined
eip7702Account: Signer
plugins?:
| Omit<
KernelPluginManagerParams<entryPointVersion>,
"entryPoint" | "kernelVersion"
>
| KernelPluginManager<entryPointVersion>
| undefined
}
| {
eip7702Auth?: SignAuthorizationReturnType | undefined
eip7702Account?: Signer | undefined
plugins:
| Omit<
KernelPluginManagerParams<entryPointVersion>,
"entryPoint" | "kernelVersion"
>
| KernelPluginManager<entryPointVersion>
}
)
/**
* 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
/**
* Default addresses for kernel smart account
*/
export const KERNEL_ADDRESSES: {
ACCOUNT_LOGIC_V0_6: Address
ACCOUNT_LOGIC_V0_7: Address
FACTORY_ADDRESS_V0_6: Address
FACTORY_ADDRESS_V0_7: Address
FACTORY_STAKER: Address
} = {
ACCOUNT_LOGIC_V0_6: "0xd3082872F8B06073A021b4602e022d5A070d7cfC",
ACCOUNT_LOGIC_V0_7: "0x94F097E1ebEB4ecA3AAE54cabb08905B239A7D27",
FACTORY_ADDRESS_V0_6: "0x5de4839a76cf55d0c90e2061ef4386d962E15ae3",
FACTORY_ADDRESS_V0_7: "0x6723b44Abeec4E71eBE3232BD5B455805baDD22f",
FACTORY_STAKER: "0xd703aaE79538628d27099B8c4f621bE4CCd142d5"
}
const getKernelInitData = async <entryPointVersion extends EntryPointVersion>({
entryPointVersion: _entryPointVersion,
kernelPluginManager,
initHook,
kernelVersion,
initConfig
}: {
entryPointVersion: entryPointVersion
kernelPluginManager: KernelPluginManager<entryPointVersion>
initHook: boolean
kernelVersion: GetKernelVersion<entryPointVersion>
initConfig?: GetKernelVersion<entryPointVersion> extends "0.3.1"
? Hex[]
: never
}) => {
const {
enableData,
identifier,
validatorAddress,
initConfig: initConfig_
} = await kernelPluginManager.getValidatorInitData()
if (_entryPointVersion === "0.6") {
return encodeFunctionData({
abi: KernelInitAbi,
functionName: "initialize",
args: [validatorAddress, enableData]
})
}
if (kernelVersion === "0.3.0") {
return encodeFunctionData({
abi: KernelV3InitAbi,
functionName: "initialize",
args: [
identifier,
initHook && kernelPluginManager.hook
? kernelPluginManager.hook?.getIdentifier()
: zeroAddress,
enableData,
initHook && kernelPluginManager.hook
? await kernelPluginManager.hook?.getEnableData()
: "0x"
]
})
}
return encodeFunctionData({
abi: KernelV3_1AccountAbi,
functionName: "initialize",
args: [
identifier,
initHook && kernelPluginManager.hook
? kernelPluginManager.hook?.getIdentifier()
: zeroAddress,
enableData,
initHook && kernelPluginManager.hook
? await kernelPluginManager.hook?.getEnableData()
: "0x",
initConfig ?? initConfig_ ?? []
]
})
}
/**
* Get the account initialization code for a kernel smart account
* @param index
* @param factoryAddress
* @param accountImplementationAddress
* @param ecdsaValidatorAddress
*/
const getAccountInitCode = async <entryPointVersion extends EntryPointVersion>({
index,
factoryAddress,
accountImplementationAddress,
entryPointVersion: _entryPointVersion,
kernelPluginManager,
initHook,
kernelVersion,
initConfig,
useMetaFactory
}: {
index: bigint
factoryAddress: Address
accountImplementationAddress: Address
entryPointVersion: entryPointVersion
kernelPluginManager: KernelPluginManager<entryPointVersion>
initHook: boolean
kernelVersion: GetKernelVersion<entryPointVersion>
initConfig?: GetKernelVersion<entryPointVersion> extends "0.3.1"
? Hex[]
: never
useMetaFactory: boolean
}): Promise<Hex> => {
// Build the account initialization data
const initialisationData = await getKernelInitData<entryPointVersion>({
entryPointVersion: _entryPointVersion,
kernelPluginManager,
initHook,
kernelVersion,
initConfig
})
// Build the account init code
if (_entryPointVersion === "0.6") {
return encodeFunctionData({
abi: createAccountAbi,
functionName: "createAccount",
args: [accountImplementationAddress, initialisationData, index]
})
}
if (!useMetaFactory) {
return encodeFunctionData({
abi: KernelV3FactoryAbi,
functionName: "createAccount",
args: [initialisationData, toHex(index, { size: 32 })]
})
}
return encodeFunctionData({
abi: KernelFactoryStakerAbi,
functionName: "deployWithFactory",
args: [factoryAddress, initialisationData, toHex(index, { size: 32 })]
})
}
const getDefaultAddresses = <entryPointVersion extends EntryPointVersion>(
entryPointVersion: entryPointVersion,
kernelVersion: GetKernelVersion<entryPointVersion>,
{
accountImplementationAddress,
factoryAddress,
metaFactoryAddress
}: {
accountImplementationAddress?: Address
factoryAddress?: Address
metaFactoryAddress?: Address
}
) => {
validateKernelVersionWithEntryPoint(entryPointVersion, kernelVersion)
const addresses = KernelVersionToAddressesMap[kernelVersion]
if (!addresses) {
throw new Error(
`No addresses found for kernel version ${kernelVersion}`
)
}
return {
accountImplementationAddress:
accountImplementationAddress ??
addresses.accountImplementationAddress,
factoryAddress: factoryAddress ?? addresses.factoryAddress,
metaFactoryAddress:
metaFactoryAddress ?? addresses.metaFactoryAddress ?? zeroAddress
}
}
type PluginInstallationCache = {
pendingPlugins: PluginMigrationData[]
allInstalled: boolean
}
/**
* 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 accountImplementationAddress
* @param ecdsaValidatorAddress
* @param address
*/
export async function createKernelAccount<
entryPointVersion extends EntryPointVersion,
KernelVersion extends GetKernelVersion<entryPointVersion>
>(
client: KernelSmartAccountImplementation["client"],
{
plugins,
entryPoint,
index = 0n,
factoryAddress: _factoryAddress,
accountImplementationAddress: _accountImplementationAddress,
metaFactoryAddress: _metaFactoryAddress,
address,
kernelVersion,
initConfig,
useMetaFactory: _useMetaFactory = true,
eip7702Auth,
eip7702Account,
pluginMigrations
}: CreateKernelAccountParameters<entryPointVersion, KernelVersion>
): Promise<CreateKernelAccountReturnType<entryPointVersion>> {
const isEip7702 = !!eip7702Account || !!eip7702Auth
if (isEip7702 && !satisfies(kernelVersion, ">=0.3.3")) {
throw new Error("EIP-7702 is recommended for kernel version >=0.3.3")
}
const localAccount = eip7702Account
? await toSigner({ signer: eip7702Account, address })
: undefined
let eip7702Validator: KernelValidator<"EIP7702Validator"> | undefined
if (localAccount) {
eip7702Validator = await signerTo7702Validator(client, {
signer: localAccount,
entryPoint,
kernelVersion
})
}
let useMetaFactory = _useMetaFactory
const { accountImplementationAddress, factoryAddress, metaFactoryAddress } =
getDefaultAddresses(entryPoint.version, kernelVersion, {
accountImplementationAddress: _accountImplementationAddress,
factoryAddress: _factoryAddress,
metaFactoryAddress: _metaFactoryAddress
})
let chainId: number
let cachedAccountMetadata: AccountMetadata | undefined
const getMemoizedChainId = async () => {
if (chainId) return chainId
chainId = client.chain
? client.chain.id
: await getAction(client, getChainId, "getChainId")({})
return chainId
}
const getMemoizedAccountMetadata = async () => {
if (cachedAccountMetadata) return cachedAccountMetadata
cachedAccountMetadata = await accountMetadata(
client,
accountAddress,
kernelVersion,
await getMemoizedChainId()
)
return cachedAccountMetadata
}
const kernelPluginManager = isKernelPluginManager<entryPointVersion>(
plugins
)
? plugins
: await toKernelPluginManager<entryPointVersion>(client, {
sudo: localAccount ? eip7702Validator : plugins?.sudo,
regular: plugins?.regular,
hook: plugins?.hook,
action: plugins?.action,
pluginEnableSignature: plugins?.pluginEnableSignature,
entryPoint,
kernelVersion,
chainId: await getMemoizedChainId()
})
// initHook flag is activated only if both the hook and sudo validator are given
// if the hook is given with regular plugins, then consider it as a hook for regular plugins
const initHook = Boolean(
isKernelPluginManager<entryPointVersion>(plugins)
? plugins.hook &&
plugins.getIdentifier() ===
plugins.sudoValidator?.getIdentifier()
: plugins?.hook && !plugins?.regular
)
// Helper to generate the init code for the smart account
const generateInitCode = async () => {
if (isEip7702) {
return "0x" as `0x${string}`
}
if (!accountImplementationAddress || !factoryAddress)
throw new Error("Missing account logic address or factory address")
return getAccountInitCode<entryPointVersion>({
index,
factoryAddress,
accountImplementationAddress,
entryPointVersion: entryPoint.version,
kernelPluginManager,
initHook,
kernelVersion,
initConfig,
useMetaFactory
})
}
const getFactoryArgs = async () => {
if (isEip7702) {
return {
factory: undefined,
factoryData: undefined
}
}
return {
factory:
entryPoint.version === "0.6" || useMetaFactory === false
? factoryAddress
: metaFactoryAddress,
factoryData: await generateInitCode()
}
}
// Fetch account address
let accountAddress =
address ??
(isEip7702
? (localAccount?.address ?? zeroAddress)
: await (async () => {
const { factory, factoryData } = await getFactoryArgs()
if (!factory || !factoryData) {
throw new Error("Missing factory address or factory data")
}
// Get the sender address based on the init code
return await getSenderAddress(client, {
factory,
factoryData,
entryPointAddress: entryPoint.address
})
})())
// If account is zeroAddress try without meta factory
if (isAddressEqual(accountAddress, zeroAddress) && useMetaFactory) {
useMetaFactory = false
accountAddress = await getSenderAddress(client, {
factory: factoryAddress,
factoryData: await generateInitCode(),
entryPointAddress: entryPoint.address
})
if (isAddressEqual(accountAddress, zeroAddress)) {
useMetaFactory = true
}
}
const _entryPoint = {
address: entryPoint?.address ?? entryPoint07Address,
abi: ((entryPoint?.version ?? "0.7") === "0.6"
? entryPoint06Abi
: entryPoint07Abi) as GetEntryPointAbi<entryPointVersion>,
version: entryPoint?.version ?? "0.7"
} as const
// Cache for plugin installation status
const pluginCache: PluginInstallationCache = {
pendingPlugins: pluginMigrations || [],
allInstalled: false
}
const checkPluginInstallationStatus = async () => {
// Skip if no plugins or all are installed
if (!pluginCache.pendingPlugins.length || pluginCache.allInstalled) {
pluginCache.allInstalled = true
return
}
// Check all pending plugins in parallel
const installationResults = await Promise.all(
pluginCache.pendingPlugins.map((plugin) =>
isPluginInstalled(client, {
address: accountAddress,
plugin
})
)
)
// Filter out installed plugins
pluginCache.pendingPlugins = pluginCache.pendingPlugins.filter(
(_, index) => !installationResults[index]
)
pluginCache.allInstalled = pluginCache.pendingPlugins.length === 0
}
const signAuthorization = async () => {
const code = await getCode(client, { address: accountAddress })
// check if account has not activated 7702 with implementation address
if (
!code ||
code.length === 0 ||
!code
.toLowerCase()
.startsWith(
`0xef0100${accountImplementationAddress.slice(2).toLowerCase()}`
)
) {
if (
eip7702Auth &&
!isAddressEqual(
eip7702Auth.address,
accountImplementationAddress
)
) {
throw new Error(
"EIP-7702 authorization delegate address does not match account implementation address"
)
}
const auth =
eip7702Auth ??
(await signAuthorizationAction(client, {
account: localAccount as LocalAccount,
address: accountImplementationAddress as `0x${string}`,
chainId: await getMemoizedChainId()
}))
const verified = await verifyAuthorization({
authorization: auth,
address: accountAddress
})
if (!verified) {
throw new Error("Authorization verification failed")
}
return auth
}
return undefined
}
await checkPluginInstallationStatus()
return toSmartAccount<KernelSmartAccountImplementation<entryPointVersion>>({
authorization: isEip7702
? {
account:
(localAccount as PrivateKeyAccount) ??
addressToEmptyAccount(accountAddress),
address: accountImplementationAddress
}
: undefined,
kernelVersion,
kernelPluginManager,
accountImplementationAddress,
factoryAddress: (await getFactoryArgs()).factory as Address,
generateInitCode,
encodeModuleInstallCallData: async () => {
return await kernelPluginManager.encodeModuleInstallCallData(
accountAddress
)
},
nonceKeyManager: createNonceManager({
source: { get: () => 0, set: () => {} }
}),
client,
entryPoint: _entryPoint,
getFactoryArgs,
async getAddress() {
if (accountAddress) return accountAddress
const { factory, factoryData } = await getFactoryArgs()
if (!factory || !factoryData) {
throw new Error("Missing factory address or factory data")
}
// Get the sender address based on the init code
accountAddress = await getSenderAddress(client, {
factory,
factoryData,
entryPointAddress: entryPoint.address
})
return accountAddress
},
// Encode the deploy call data
async encodeDeployCallData(_tx) {
if (entryPoint.version === "0.6") {
return encodeDeployCallDataV06(_tx)
}
return encodeDeployCallDataV07(_tx)
},
async encodeCalls(calls, callType) {
// Check plugin status only if we have pending plugins
await checkPluginInstallationStatus()
// Add plugin installation calls if needed
if (
pluginCache.pendingPlugins.length > 0 &&
entryPoint.version === "0.7" &&
kernelPluginManager.activeValidatorMode === "sudo"
) {
// convert map into for loop
const pluginInstallCalls: CallArgs[] = []
for (const plugin of pluginCache.pendingPlugins) {
pluginInstallCalls.push(
getPluginInstallCallData(accountAddress, plugin)
)
}
return encodeCallDataEpV07(
[...calls, ...pluginInstallCalls],
callType,
plugins?.hook ? true : undefined
)
}
if (
calls.length === 1 &&
(!callType || callType === "call") &&
calls[0].to.toLowerCase() === accountAddress.toLowerCase()
) {
return calls[0].data ?? "0x"
}
if (entryPoint.version === "0.6") {
return encodeCallDataEpV06(calls, callType)
}
if (plugins?.hook) {
return encodeCallDataEpV07(calls, callType, true)
}
return encodeCallDataEpV07(calls, callType)
},
eip7702Authorization: signAuthorization,
async sign({ hash }) {
return this.signMessage({ message: hash })
},
async signMessage({ message, useReplayableSignature }) {
const messageHash = hashMessage(message)
const {
name,
chainId: metadataChainId,
version
} = await getMemoizedAccountMetadata()
let signature: Hex
if (isEip7702) {
signature = await kernelPluginManager.signTypedData({
message: { hash: messageHash },
primaryType: "Kernel",
types: {
Kernel: [{ name: "hash", type: "bytes32" }]
},
domain: {
name,
version,
chainId: useReplayableSignature
? 0
: Number(metadataChainId),
verifyingContract: accountAddress
}
})
} else {
const wrappedMessageHash = await eip712WrapHash(
messageHash,
{
name,
chainId: Number(metadataChainId),
version,
verifyingContract: accountAddress
},
useReplayableSignature
)
signature = await kernelPluginManager.signMessage({
message: { raw: wrappedMessageHash }
})
}
if (
!hasKernelFeature(
KERNEL_FEATURES.ERC1271_WITH_VALIDATOR,
version
)
) {
return signature
}
if (
useReplayableSignature &&
hasKernelFeature(KERNEL_FEATURES.ERC1271_REPLAYABLE, version)
) {
signature = concatHex([MAGIC_VALUE_SIG_REPLAYABLE, signature])
}
return concatHex([kernelPluginManager.getIdentifier(), signature])
},
async signTypedData(typedData) {
const {
message,
primaryType,
types: _types,
domain
} = typedData as TypedDataDefinition
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: domain,
message: message,
primaryType: primaryType,
types: types
})
const typedHash = hashTypedData(typedData)
const {
name,
chainId: metadataChainId,
version
} = await getMemoizedAccountMetadata()
let signature: Hex
if (isEip7702) {
signature = await kernelPluginManager.signTypedData({
message: { hash: typedHash },
primaryType: "Kernel",
types: {
Kernel: [{ name: "hash", type: "bytes32" }]
},
domain: {
name,
version,
chainId: Number(metadataChainId),
verifyingContract: accountAddress
}
})
} else {
const wrappedMessageHash = await eip712WrapHash(typedHash, {
name,
chainId: Number(metadataChainId),
version,
verifyingContract: accountAddress
})
signature = await kernelPluginManager.signMessage({
message: { raw: wrappedMessageHash }
})
}
if (
!hasKernelFeature(
KERNEL_FEATURES.ERC1271_WITH_VALIDATOR,
version
)
) {
return signature
}
return concatHex([kernelPluginManager.getIdentifier(), signature])
},
// Get the nonce of the smart account
async getNonce(_args) {
const key = await kernelPluginManager.getNonceKey(
accountAddress,
_args?.key
)
return getAccountNonce(client, {
address: accountAddress,
entryPointAddress: entryPoint.address,
key
})
},
async getStubSignature(userOperation) {
if (!userOperation) {
throw new Error("No user operation provided")
}
return kernelPluginManager.getStubSignature(
userOperation as UserOperation
)
},
// Sign a user operation
async signUserOperation(parameters) {
const { chainId = await getMemoizedChainId(), ...userOperation } =
parameters
return kernelPluginManager.signUserOperation({
...userOperation,
sender: userOperation.sender ?? (await this.getAddress()),
chainId
})
}
}) as Promise<CreateKernelAccountReturnType<entryPointVersion>>
}