@zerodev/sdk
Version:
A utility library for working with ERC-4337
539 lines • 22.7 kB
JavaScript
import { satisfies } from "semver";
import { concatHex, createNonceManager, encodeFunctionData, getTypesForEIP712Domain, hashMessage, hashTypedData, isAddressEqual, toHex, validateTypedData, zeroAddress } from "viem";
import { entryPoint06Abi, entryPoint07Abi, entryPoint07Address, toSmartAccount } from "viem/account-abstraction";
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 { 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 { accountMetadata } from "./utils/common/accountMetadata.js";
import { eip712WrapHash } from "./utils/common/eip712WrapHash.js";
import { getPluginInstallCallData } from "./utils/plugins/ep0_7/getPluginInstallCallData.js";
/**
* 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"
}
];
/**
* Default addresses for kernel smart account
*/
export const KERNEL_ADDRESSES = {
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: _entryPointVersion, kernelPluginManager, initHook, kernelVersion, initConfig }) => {
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 ({ index, factoryAddress, accountImplementationAddress, entryPointVersion: _entryPointVersion, kernelPluginManager, initHook, kernelVersion, initConfig, useMetaFactory }) => {
// Build the account initialization data
const initialisationData = await getKernelInitData({
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, kernelVersion, { accountImplementationAddress, factoryAddress, metaFactoryAddress }) => {
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
};
};
/**
* 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(client, { plugins, entryPoint, index = 0n, factoryAddress: _factoryAddress, accountImplementationAddress: _accountImplementationAddress, metaFactoryAddress: _metaFactoryAddress, address, kernelVersion, initConfig, useMetaFactory: _useMetaFactory = true, eip7702Auth, eip7702Account, pluginMigrations }) {
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;
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;
let cachedAccountMetadata;
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(plugins)
? plugins
: await toKernelPluginManager(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(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";
}
if (!accountImplementationAddress || !factoryAddress)
throw new Error("Missing account logic address or factory address");
return getAccountInitCode({
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),
version: entryPoint?.version ?? "0.7"
};
// Cache for plugin installation status
const pluginCache = {
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,
address: accountImplementationAddress,
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({
authorization: isEip7702
? {
account: localAccount ??
addressToEmptyAccount(accountAddress),
address: accountImplementationAddress
}
: undefined,
kernelVersion,
kernelPluginManager,
accountImplementationAddress,
factoryAddress: (await getFactoryArgs()).factory,
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 = [];
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;
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;
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;
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);
},
// Sign a user operation
async signUserOperation(parameters) {
const { chainId = await getMemoizedChainId(), ...userOperation } = parameters;
return kernelPluginManager.signUserOperation({
...userOperation,
sender: userOperation.sender ?? (await this.getAddress()),
chainId
});
}
});
}
//# sourceMappingURL=createKernelAccount.js.map