Creating a Smart Account
At the core of account abstraction is the smart account -- an account powered by a smart contract. ZeroDev is built on Kernel, a modular smart account that can be customized with plugins.
When you create a Kernel account, you set it up with a validator, which is a type of plugin that handles how the account validates UserOps. In this tutorial, we will be using the ECDSA validator, which works like a normal EOA by validating signatures from a ECDSA private key. ZeroDev supports other validators such as passkeys and multisig.
We will be using a local private key, but the ECDSA validator also works with third-party auth providers.
Installation
npm i @zerodev/sdk @zerodev/ecdsa-validator
API
Picking an EntryPoint
Currently there are two versions of ERC-4337 that are used in production. They are referred to as "EntryPoint 0.6" and "EntryPoint 0.7", where "EntryPoint" refers to the singleton ERC-4337 contract.
If you are building a new application, we recommend using EntryPoint 0.7 (Kernel v3), which gives you the latest and greatest features and optimizations. If you already have an application using EntryPoint 0.6 (Kernel v2), just stick with it -- it will be supported indefinitely.
In this tutorial, we will use EntryPoint 0.7. Start by selecting an EntryPoint:
const entryPoint = getEntryPoint("0.7")
Picking a Kernel version
Kernel is the smart account that ZeroDev builds on. ZeroDev SDK used to implicitly use the latest version of Kernel, which has caused some compatibility issues when people upgrade the SDK. Therefore, starting from ZeroDev SDK v5.3, we require that you explicitly specify the Kernel version. This is how you generally should choose:
- If you had already been in production with ZeroDev SDK v4 or lower, use Kernel version 2.4 with EntryPoint 0.6.
- If you had already been in production with ZeroDev SDK v5, use Kernel version 3.0 with EntryPoint 0.7.
- If you are still in development or starting a new project, use Kernel version 3.1 with EntryPoint 0.7.
import { KERNEL_V3_1 } from "@zerodev/sdk/constants"
const kernelVersion = KERNEL_V3_1
Creating a public client
In Viem, a public client is an interface to a JSON-RPC API such as Infura or Alchemy.
import { createPublicClient, http } from "viem"
import { base } from 'viem/chains'
const publicClient = createPublicClient({
// Use your own RPC provider (e.g. Infura/Alchemy).
transport: http("RPC_URL"),
chain: base,
})
Creating a signer
As aforementioned, a Kernel account using a ECDSA validator is "owned" by a signer, which is anything that can sign messages with a private key.
Since Kernel is built on top of Viem, we can use any Viem account as the signer. In this example, we create a signer from a private key:
import { Hex } from "viem"
import { privateKeyToAccount } from "viem/accounts"
const signer = privateKeyToAccount("PRIVATE_KEY" as Hex)
Replace PRIVATE_KEY
with an actual private key. You can generate a random one here.
Creating a ECDSA validator
Then create a ECDSA validator from the signer:
import { signerToEcdsaValidator } from "@zerodev/ecdsa-validator"
const ecdsaValidator = await signerToEcdsaValidator(publicClient, {
signer,
entryPoint,
kernelVersion
})
Create a Kernel account
Next, create a Kernel account with the ECDSA validator:
import { createKernelAccount } from "@zerodev/sdk"
const account = await createKernelAccount(publicClient, {
plugins: {
sudo: ecdsaValidator,
},
entryPoint,
kernelVersion
})
Create an account client
Now that we have an account, we can finally construct an "account client," which is the equivalent of a wallet client in Viem that allows you to send UserOps to bundlers.
import { createKernelAccountClient } from "@zerodev/sdk"
import { http } from "viem"
import { base } from 'viem/chains'
const kernelClient = createKernelAccountClient({
account,
// Replace with your chain
chain: base,
// Replace with your bundler RPC.
// For ZeroDev, you can find the RPC on your dashboard.
bundlerTransport: http('BUNDLER_RPC'),
// Optional -- only if you want to use a paymaster
paymaster: {
getPaymasterData(userOperation) {
return paymasterClient.sponsorUserOperation({userOperation})
}
},
})
Note that:
- You need to replace the
BUNDLER_RPC
with an actual bundler RPC.- For ZeroDev, you can find the RPC on your dashboard.
- You need to make sure to set the right
chain
. paymaster
only needs to be specified if you want to use a paymaster.
Now you are ready to do things with your smart account, like sending UserOps!
FAQs
When I create an account, is it deployed on-chain?
No. If your account hasn't been deployed yet, we simply use CREATE2
to compute the address that the account would be deployed to. Your account is deployed automatically with the first UserOp it sends.
In other words, "creating" accounts with the SDK is free -- you can create an infinite number of such account objects without paying any gas. It's only when you send the first UserOp that the account is deployed automatically.
Can I create multiple accounts from the same EOA signer?
Yes, you can do so by providing an index
when you create the account object.
import { createKernelAccount } from "@zerodev/sdk"
const account = createKernelAccount(publicClient, {
// other options...
// optionally specify the index; different indexes will yield different accounts
index: 1,
})
How do I compute the smart account address from the EOA signer address?
Sometimes you only know the address of the EOA signer but you don't have the signer itself. In that case, you can still compute the address of the smart account with this helper function:
import { getKernelAddressFromECDSA } from "@zerodev/ecdsa-validator"
// index is 0 by default
const smartAccountAddress = await getKernelAddressFromECDSA(publicClient, eoaAddress, index)
How do I create a Kernel account object with a specific address?
Normally, you don't need to manually specify an address because the smart account address is computed from your signer data. However, if you have changed the signer, then you may need to manually specify the smart account address.
You can do it like this:
const account = await createKernelAccount(publicClient, {
address: "address",
// ...other args
})