@zerodev/sdk
Version:
A utility library for working with ERC-4337
374 lines (348 loc) • 12 kB
text/typescript
import {
type Address,
type EncodeDeployDataParameters,
type Hex,
type TypedDataDefinition,
concatHex,
createNonceManager,
encodeDeployData,
encodeFunctionData,
getTypesForEIP712Domain,
hashTypedData,
parseAbi,
validateTypedData
} from "viem"
import {
type UserOperation,
entryPoint06Abi,
entryPoint06Address,
toSmartAccount
} from "viem/account-abstraction"
import { toAccount } from "viem/accounts"
import { getChainId } from "viem/actions"
import { getAction } from "viem/utils"
import {
getAccountNonce,
getSenderAddress
} from "../../../actions/public/index.js"
import { KernelVersionToAddressesMap } from "../../../constants.js"
import type {
GetKernelVersion,
KernelPluginManager,
KernelPluginManagerParams
} from "../../../types/kernel.js"
import {
MULTISEND_ADDRESS,
encodeMultiSend,
multiSendAbi
} from "../../utils/multisend.js"
import {
isKernelPluginManager,
toKernelPluginManager
} from "../../utils/toKernelPluginManager.js"
import type {
CreateKernelAccountReturnType,
KernelSmartAccountImplementation
} from "../createKernelAccount.js"
import { KernelAccountV2Abi } from "./abi/KernelAccountV2Abi.js"
import { KernelFactoryV2Abi } from "./abi/KernelFactoryV2Abi.js"
// Safe's library for create and create2: https://github.com/safe-global/safe-contracts/blob/0acdd35a203299585438f53885df630f9d486a86/contracts/libraries/CreateCall.sol
// Address was found here: https://github.com/safe-global/safe-deployments/blob/926ec6bbe2ebcac3aa2c2c6c0aff74aa590cbc6a/src/assets/v1.4.1/create_call.json
const createCallAddress = "0x9b35Af71d77eaf8d7e40252370304687390A1A52"
const createCallAbi = parseAbi([
"function performCreate(uint256 value, bytes memory deploymentData) public returns (address newContract)",
"function performCreate2(uint256 value, bytes memory deploymentData, bytes32 salt) public returns (address newContract)"
])
/**
* Default addresses for kernel smart account
*/
export const KERNEL_ADDRESSES: {
FACTORY_ADDRESS: Address
ENTRYPOINT_V0_6: Address
} = {
FACTORY_ADDRESS: "0xaee9762ce625e0a8f7b184670fb57c37bfe1d0f1",
ENTRYPOINT_V0_6: "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"
}
/**
* Get the account initialization code for a kernel smart account
* @param index
* @param factoryAddress
* @param ecdsaValidatorAddress
*/
const getAccountInitCode = async ({
index,
factoryAddress,
validatorAddress,
enableData
}: {
index: bigint
factoryAddress: Address
validatorAddress: Address
enableData: Hex
}): Promise<Hex> => {
// Build the account init code
return concatHex([
factoryAddress,
encodeFunctionData({
abi: KernelFactoryV2Abi,
functionName: "createAccount",
args: [validatorAddress, enableData, index]
}) as Hex
])
}
/**
* 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 ecdsaValidatorAddress
* @param deployedAccountAddress
*/
export async function createKernelAccountV0_2(
client: KernelSmartAccountImplementation["client"],
{
plugins,
entryPoint,
index = 0n,
factoryAddress = KERNEL_ADDRESSES.FACTORY_ADDRESS,
address
}: {
plugins:
| Omit<
KernelPluginManagerParams<"0.6">,
"entryPoint" | "kernelVersion"
>
| KernelPluginManager<"0.6">
entryPoint: {
address: Address
version: "0.6"
}
index?: bigint
factoryAddress?: Address
address?: Address
}
): Promise<CreateKernelAccountReturnType<"0.6">> {
const kernelPluginManager = isKernelPluginManager<"0.6">(plugins)
? plugins
: await toKernelPluginManager(client, {
sudo: plugins.sudo,
regular: plugins.regular,
action: plugins.action,
pluginEnableSignature: plugins.pluginEnableSignature,
kernelVersion: "0.0.2",
entryPoint
})
// Helper to generate the init code for the smart account
const generateInitCode = async () => {
const validatorInitData =
await kernelPluginManager.getValidatorInitData()
return getAccountInitCode({
index,
factoryAddress,
validatorAddress: validatorInitData.validatorAddress,
enableData: validatorInitData.enableData
})
}
const getFactoryArgs = async () => {
return {
factory: factoryAddress,
factoryData: await generateInitCode()
}
}
// Fetch account address
let accountAddress =
address ??
(await (async () => {
const { factory, factoryData } = await getFactoryArgs()
// Get the sender address based on the init code
return await getSenderAddress(client, {
factory,
factoryData,
entryPointAddress: entryPoint.address
})
})())
// Build the EOA Signer
const account = toAccount({
address: accountAddress,
async signMessage({ message }) {
return kernelPluginManager.signMessage({
message
})
},
async signTransaction(_, __) {
throw new Error(
"Smart account signer doesn't need to sign transactions"
)
},
async signTypedData(typedData) {
const _typedData = typedData as TypedDataDefinition
const types = {
EIP712Domain: getTypesForEIP712Domain({
domain: _typedData.domain
}),
..._typedData.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: _typedData.domain,
message: _typedData.message,
primaryType: _typedData.primaryType,
types: types
})
const typedHash = hashTypedData(typedData)
return kernelPluginManager.signMessage({
message: {
raw: typedHash
}
})
}
})
const _entryPoint = {
address: entryPoint?.address ?? entryPoint06Address,
abi: entryPoint06Abi,
version: entryPoint?.version ?? "0.6"
}
let chainId: number
const getMemoizedChainId = async () => {
if (chainId) return chainId
chainId = client.chain
? client.chain.id
: await getAction(client, getChainId, "getChainId")({})
return chainId
}
return toSmartAccount<KernelSmartAccountImplementation<"0.6">>({
kernelVersion: "0.0.2" as GetKernelVersion<"0.6">,
client,
entryPoint: _entryPoint,
kernelPluginManager,
factoryAddress,
accountImplementationAddress:
KernelVersionToAddressesMap["0.0.2"].accountImplementationAddress,
generateInitCode,
encodeModuleInstallCallData: async () => {
return await kernelPluginManager.encodeModuleInstallCallData(
accountAddress
)
},
nonceKeyManager: createNonceManager({
source: { get: () => 0, set: () => {} }
}),
async sign({ hash }) {
return account.signMessage({ message: hash })
},
async signMessage(message) {
return account.signMessage(message)
},
async signTypedData(typedData) {
return account.signTypedData(typedData)
},
getFactoryArgs,
async getAddress() {
if (accountAddress) return accountAddress
const { factory, factoryData } = await getFactoryArgs()
// Get the sender address based on the init code
accountAddress = await getSenderAddress(client, {
factory,
factoryData,
entryPointAddress: entryPoint.address
})
return accountAddress
},
// 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
})
},
// Sign a user operation
async signUserOperation(parameters) {
const { chainId = await getMemoizedChainId(), ...userOperation } =
parameters
return kernelPluginManager.signUserOperation({
...userOperation,
sender: userOperation.sender ?? (await this.getAddress()),
chainId
})
},
// Encode the deploy call data
async encodeDeployCallData(_tx) {
return encodeFunctionData({
abi: KernelAccountV2Abi,
functionName: "execute",
args: [
createCallAddress,
0n,
encodeFunctionData({
abi: createCallAbi,
functionName: "performCreate",
args: [
0n,
encodeDeployData({
abi: _tx.abi,
bytecode: _tx.bytecode,
args: _tx.args
} as EncodeDeployDataParameters)
]
}),
1 // Delegate call
]
})
},
// Encode a call
async encodeCalls(calls, callType): Promise<Hex> {
if (calls.length > 1) {
const multiSendCallData = encodeFunctionData({
abi: multiSendAbi,
functionName: "multiSend",
args: [encodeMultiSend(calls)]
})
return encodeFunctionData({
abi: KernelAccountV2Abi,
functionName: "execute",
args: [MULTISEND_ADDRESS, 0n, multiSendCallData, 1]
})
}
const call = calls.length === 0 ? undefined : calls[0]
if (!call) {
throw new Error("No calls to encode")
}
// Default to `call`
if (!callType || callType === "call") {
if (call.to.toLowerCase() === accountAddress.toLowerCase()) {
return call.data || "0x"
}
return encodeFunctionData({
abi: KernelAccountV2Abi,
functionName: "execute",
args: [call.to, call.value || 0n, call.data || "0x", 0]
})
}
if (callType === "delegatecall") {
return encodeFunctionData({
abi: KernelAccountV2Abi,
functionName: "execute",
args: [call.to, 0n, call.data || "0x", 1]
})
}
throw new Error("Invalid call type")
},
// Get simple dummy signature
async getStubSignature(userOperation) {
if (!userOperation) {
throw new Error("No user operation provided")
}
return kernelPluginManager.getStubSignature(
userOperation as UserOperation
)
}
})
}