@zerodev/sdk
Version:
A utility library for working with ERC-4337
313 lines (285 loc) • 9.84 kB
text/typescript
import {
type Account,
type Address,
type Assign,
type Chain,
type EIP1193Provider,
type Hex,
type LocalAccount,
type OneOf,
type Transport,
type TypedData,
type TypedDataDefinition,
type WalletClient,
createNonceManager,
encodeFunctionData
} from "viem"
import {
type SmartAccount,
type SmartAccountImplementation,
entryPoint06Abi,
entryPoint06Address,
getUserOperationHash,
toSmartAccount
} from "viem/account-abstraction"
import { toAccount } from "viem/accounts"
import { getChainId, signMessage } from "viem/actions"
import {
getAccountNonce,
getSenderAddress
} from "../../../actions/public/index.js"
import type { CallType, GetEntryPointAbi } from "../../../types/kernel.js"
import {
MULTISEND_ADDRESS,
encodeMultiSend,
multiSendAbi
} from "../../utils/multisend.js"
export type KernelSmartAccountV1Implementation = Assign<
SmartAccountImplementation<GetEntryPointAbi<"0.6">, "0.6">,
{
sign: NonNullable<SmartAccountImplementation["sign"]>
encodeCalls: (
calls: Parameters<SmartAccountImplementation["encodeCalls"]>[0],
callType?: CallType | undefined
) => Promise<Hex>
generateInitCode: () => Promise<Hex>
encodeModuleInstallCallData: () => Promise<Hex>
}
>
export type CreateKernelAccountV1ReturnType =
SmartAccount<KernelSmartAccountV1Implementation>
const createAccountAbi = [
{
inputs: [
{ internalType: "address", name: "_owner", type: "address" },
{ internalType: "uint256", name: "_index", type: "uint256" }
],
name: "createAccount",
outputs: [
{
internalType: "contract EIP1967Proxy",
name: "proxy",
type: "address"
}
],
stateMutability: "nonpayable",
type: "function"
}
]
const executeAndRevertAbi = [
{
inputs: [
{ internalType: "address", name: "to", type: "address" },
{ internalType: "uint256", name: "value", type: "uint256" },
{ internalType: "bytes", name: "data", type: "bytes" },
{ internalType: "enum Operation", name: "operation", type: "uint8" }
],
name: "executeAndRevert",
outputs: [],
stateMutability: "nonpayable",
type: "function"
}
]
const KERNEL_V1_ADDRESSES: {
FACTORY_ADDRESS: Address
ENTRYPOINT_V0_6: Address
} = {
FACTORY_ADDRESS: "0x4E4946298614FC299B50c947289F4aD0572CB9ce",
ENTRYPOINT_V0_6: "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"
}
export async function createKernelAccountV1(
client: KernelSmartAccountV1Implementation["client"],
{
signer,
address,
entryPoint,
index = 0n
}: {
signer: OneOf<
| EIP1193Provider
| WalletClient<Transport, Chain | undefined, Account>
| LocalAccount
>
entryPoint: {
address: Address
version: "0.6"
}
address?: Address
index?: bigint
}
): Promise<CreateKernelAccountV1ReturnType> {
const viemSigner: LocalAccount = {
...signer,
signTransaction: (_, __) => {
throw new Error(
"Smart account signer doesn't need to sign transactions"
)
}
} as LocalAccount
// Fetch chain id
const chainId = await getChainId(client)
const generateInitCode = async (): Promise<Hex> => {
return encodeFunctionData({
abi: createAccountAbi,
functionName: "createAccount",
args: [signer.address, index]
})
}
const getFactoryArgs = async () => {
return {
factory: KERNEL_V1_ADDRESSES.FACTORY_ADDRESS,
factoryData: await generateInitCode()
}
}
// Fetch account address and chain id
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
})
})())
if (!accountAddress) throw new Error("Account address not found")
const account = toAccount({
address: accountAddress,
async signMessage({ message }) {
return viemSigner.signMessage({ message })
},
async signTransaction(_, __) {
throw new Error(
"Smart account signer doesn't need to sign transactions"
)
},
async signTypedData<
const TTypedData extends TypedData | Record<string, unknown>,
TPrimaryType extends
| keyof TTypedData
| "EIP712Domain" = keyof TTypedData
>(typedData: TypedDataDefinition<TTypedData, TPrimaryType>) {
return viemSigner.signTypedData(typedData)
}
})
const _entryPoint = {
address: entryPoint?.address ?? entryPoint06Address,
abi: entryPoint06Abi,
version: entryPoint?.version ?? "0.6"
}
return toSmartAccount<KernelSmartAccountV1Implementation>({
...account,
generateInitCode,
encodeModuleInstallCallData: async () => {
throw new Error("Not implemented")
},
nonceKeyManager: createNonceManager({
source: { get: () => 0, set: () => {} }
}),
async sign({ hash }) {
return account.signMessage({ message: hash })
},
async signMessage({ message }) {
return account.signMessage({ message })
},
async signTypedData<
const TTypedData extends TypedData | Record<string, unknown>,
TPrimaryType extends
| keyof TTypedData
| "EIP712Domain" = keyof TTypedData
>(typedData: TypedDataDefinition<TTypedData, TPrimaryType>) {
return viemSigner.signTypedData(typedData)
},
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
},
getFactoryArgs,
client,
entryPoint: _entryPoint,
async getNonce() {
return getAccountNonce(client, {
address: accountAddress,
entryPointAddress: entryPoint.address
})
},
async signUserOperation(userOperation) {
const hash = getUserOperationHash({
userOperation: {
...userOperation,
sender: userOperation.sender ?? (await this.getAddress()),
signature: "0x"
},
entryPointAddress: entryPoint.address,
entryPointVersion: entryPoint.version,
chainId: chainId
})
const signature = await signMessage(client, {
account: viemSigner,
message: { raw: hash }
})
return signature
},
// async getInitCode() {
// if (smartAccountDeployed) return "0x"
// smartAccountDeployed = await isSmartAccountDeployed(
// client,
// accountAddress
// )
// if (smartAccountDeployed) return "0x"
// return generateInitCode()
// },
async encodeCalls(calls, callType): Promise<Hex> {
if (calls.length > 1) {
if (callType === "delegatecall") {
throw new Error("Cannot batch delegatecall")
}
// Encode a batched call using multiSend
const multiSendCallData = encodeFunctionData({
abi: multiSendAbi,
functionName: "multiSend",
args: [encodeMultiSend(calls)]
})
return encodeFunctionData({
abi: executeAndRevertAbi,
functionName: "executeAndRevert",
args: [MULTISEND_ADDRESS, 0n, multiSendCallData, 1n]
})
}
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: executeAndRevertAbi,
functionName: "executeAndRevert",
args: [call.to, call.value || 0n, call.data, 0n]
})
}
if (callType === "delegatecall") {
return encodeFunctionData({
abi: executeAndRevertAbi,
functionName: "executeAndRevert",
args: [call.to, call.value || 0n, call.data, 1n]
})
}
throw new Error("Invalid call type")
},
async getStubSignature() {
return "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c"
}
})
}