ZeroDev Tutorial
In this tutorial, we will mint an NFT without paying gas. We assume that you have a high-level understanding of AA concepts such as bundlers, paymasters, and UserOp; if not, read the introduction first.
Create a ZeroDev Project
For this tutorial, we will use ZeroDev's AA infra, but you can use ZeroDev with any AA infra provider.
Go to the ZeroDev dashboard and create a project for Sepolia.
We will be using the "Project ID" in the next steps.
Set up a gas policy
With ZeroDev, by default you are not sponsoring UserOps. To sponsor UserOps, you need to set up a gas policy.
Go to the "Gas Policies" section of your dashboard and create a new "Project Policy":
From now on, when you use the paymaster RPC from the previous step, the paymaster will sponsor UserOps according to the policy you just set up, which in this case is up to 100 UserOps per minute.
Write the code
Clone the ZeroDev examples repo. Then, inside the directory, install all dependencies:
npm install
Create a .env
file with the following line:
ZERODEV_PROJECT_ID=<YOUR_PROJECT_ID>
Replacing <YOUR_PROJECT_ID>
with your actual project ID from the dashboard, and make sure you are using a project ID for Sepolia.
If all goes well, you should be able to run:
npx ts-node tutorial/completed.ts
Now open the tutorial/template.ts
file in your editor. This will be the template where you will write your code. You can always refer to tutorial/completed.ts
to see the completed tutorial code.
Create a signer
Kernel accounts support many different signing methods, including ECDSA keys and passkeys. In this tutorial, we will use ECDSA keys which are the same type of keys that MetaMask and other Ethereum wallets use.
Let's start by generating a random key. Add the following code to the main
function:
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"
const main = async () => {
const privateKey = generatePrivateKey()
const signer = privateKeyToAccount(privateKey)
}
Create a validator
Each Kernel account handles validation through a smart contract known as a "validator." In this case, we will be using the ECDSA validator.
Add the following code to create the ECDSA validator:
import { signerToEcdsaValidator } from "@zerodev/ecdsa-validator"
const main = async () => {
// other code...
const ecdsaValidator = await signerToEcdsaValidator(publicClient, {
signer,
entryPoint,
})
}
Create an account
We are now ready to create an account. Add the following code:
import { createKernelAccount } from "@zerodev/sdk"
const main = async () => {
// other code...
const account = await createKernelAccount(publicClient, {
plugins: {
sudo: ecdsaValidator,
},
entryPoint,
})
}
Creating a Kernel client
Finally, we are going to create an "account client" which serves as the connection between your account and some AA infra (i.e. bundlers and paymasters). The connection is necessary for you to actually send UserOps.
Add the following code:
const main = async () => {
// ... other code
const zerodevPaymaster = createZeroDevPaymasterClient({
chain,
transport: http(PAYMASTER_RPC),
})
const kernelClient = createKernelAccountClient({
account,
chain,
bundlerTransport: http(BUNDLER_RPC),
client: publicClient,
paymaster: {
getPaymasterData(userOperation) {
return zerodevPaymaster.sponsorUserOperation({userOperation})
}
},
userOperation: {
estimateFeesPerGas: async ({bundlerClient}) => {
return getUserOperationGasPrice(bundlerClient)
}
}
})
const accountAddress = kernelClient.account.address
console.log("My account:", accountAddress)
}
Run this script with npx ts-node tutorial/template.ts
and confirm that it prints an address.
Send a UserOp
Now that you have an account client, it's time to send your first UserOp! For this tutorial, we will mint an NFT from a contract deployed on Sepolia.
Add the following import and code:
import { encodeFunctionData } from "viem"
const main = async () => {
// ... other code
const userOpHash = await kernelClient.sendUserOperation({
callData: await kernelClient.account.encodeCalls([{
to: contractAddress,
value: BigInt(0),
data: encodeFunctionData({
abi: contractABI,
functionName: "mint",
args: [accountAddress],
})
}])
})
console.log("Submitted UserOp:", userOpHash)
}
There's quite a bit of code going on, so let's go through it.
- We start by calling
kernelClient.sendUserOperation
, which takes auserOperation
argument. - Inside
userOperation
, we specify acallData
field. This is the equivalent of the calldata field for a normal Ethereum transaction. - Since we want to call the
mint(address)
function on the NFT contract, we use Viem's helper functionencodeFunctionData
and give it the ABI, function name, and function argument. kernelClient.sendUserOperation
returns a "UserOperation hash." This is the equivalent of a transaction hash but for a UserOp.
Run the script again with npx ts-node tutorial/template.ts
and confirm that it prints the UserOp hash. At this point, you can go to a UserOp explorer such as JiffyScan and find your UserOp with the hash!
Waiting for the UserOp
When you call sendUserOperation
, the call returns as soon as the UserOp has been submitted to the bundler, but it doesn't wait for the UserOp to be "confirmed" on-chain. To wait for the UserOp to be confirmed, add the following import and code:
const main = async () => {
// ... other code
const receipt = await kernelClient.waitForUserOperationReceipt({
hash: userOpHash,
})
console.log("UserOp confirmed:", receipt.userOpHash)
}
Let's break down the code:
waitForUserOperationReceipt
is a bundler action. If you are unfamiliar with the concept of "actions," you can read more about it on Viem's documentation.- This function returns a "receipt" object. If you are curious, you can print the full object and see what it contains.
Read contract state
Now let's confirm that we actually minted an NFT. Add the following import and code:
import { publicActions } from "viem"
const main = async () => {
// ... other code
const nftBalance = await publicClient.readContract({
address: contractAddress,
abi: contractABI,
functionName: 'balanceOf',
args: [accountAddress],
})
console.log(`NFT balance: ${nftBalance}`)
}
Run the script again. You should see that it prints NFT balance: 1
, confirming that you have minted an NFT!
Next steps
In this tutorial, we were able to mint an NFT without paying gas, thanks to gas sponsorship.
For next steps:
- Check out the core API to learn more about the SDK
- Read some code examples of using ZeroDev