ZeroDev Account Abstraction
ZeroDev Account Abstraction
ZeroDev’s Account Abstraction tooling makes it possible to quickly spin up Smart Contract Accounts tied to your Magic wallet. The guide below walks through adding account abstraction to a simple Magic project using ZeroDev. We’ll leverage a project pointed at the Ethereum Sepolia test network, but you can use any network supported by both Magic and ZeroDev. The code snippets provided are based on a Next.js web app but can be modified to work with virtually any JavaScript framework.
To learn more about ZeroDev, visit their documentation.
#Project prerequisites
To follow along with this guide, you’ll need three things:
- A Magic Publishable API Key
- A ZeroDev Project ID
- A web client
You can get your Publishable API Key from your Magic Dashboard.
You can get your ZeroDev Project ID (for Ethereum Sepolia) from your ZeroDev Dashboard.
We’ll use the make-scoped-magic-app
CLI tool to bootstrap a Next.js app with Magic authentication already baked into the client. You’re welcome to use your own client, but this tutorial and its accompanying code snippets assume the output of the make-scoped-magic-app
CLI as the starting point.
The make-scoped-magic-app
CLI tool is an easy way to bootstrap new projects with Magic. To get started, simply run the command below in the shell of your choice. Be sure to replace <YOUR_PUBLISHABLE_API_KEY>
with the Publishable API Key from your Magic Dashboard.
01npx make-scoped-magic-app \\
02 --template nextjs-dedicated-wallet \\
03 --network ethereum-sepolia \\
04 --login-methods EmailOTP \\
05 --publishable-api-key <YOUR_PUBLISHABLE_API_KEY>
This will bootstrap the starting point of the tutorial for you. In the scaffolded project, be sure to add your Magic Publishable API Key and ZeroDev project ID to the .env
as NEXT_PUBLIC_MAGIC_API_KEY
and NEXT_PUBLIC_ZERODEV_SEPOLIA_PROJECT_ID
, respectively.
01// Publishable API Key found in the Magic Dashboard
02NEXT_PUBLIC_MAGIC_API_KEY=pk_live_FF619AE0AEC9D473
03
04// The RPC URL for the blockchain network
05NEXT_PUBLIC_BLOCKCHAIN_NETWORK=ethereum-sepolia
06
07// The ZeroDev project id for the Sepolia blockchain network
08NEXT_PUBLIC_ZERODEV_SEPOLIA_PROJECT_ID=<ZERODEV_PROJECT_ID>
#Install additional project dependencies
In addition to the packages included in the scaffold produced by the make-scoped-magic-app
CLI, you’ll need a number of packages related to ZeroDev and their account abstraction tools. You’ll also need to install viem
for EVM-related types and transaction convenience methods.
Run the following command to install the required dependencies:
01npm install @zerodev/sdk @zerodev/presets @zerodev/ecdsa-validator viem permissionless
#Initialize ZeroDev smart contract accounts
To establish a connection between Magic and ZeroDev smart accounts, we create a ZeroDev kernel client. Kernel is ZeroDev’s ERC-4337-compatible smart contract account. It’s customizable, modular, and comes equipped with a number of plugin capabilities. We won’t go into the specifics of how it works, but feel free to look at the GitHub repo for more information.
Inside of src/components
, create a directory named zeroDev
. Inside that directory create a file named useZeroDevKernelClient.tsx
.
This file will contain a hook that will surface the ZeroDev kernel client to the rest of the app. It’ll also observe when users log in or out and connect and disconnect to the corresponding smart contract account accordingly. We’ll go through each of these three separately, then show the code for the entire file.
#Initialize useZeroDevKernelClient
When a user logs in with Magic, we need to associate their Magic account with a smart contract account through ZeroDev. Just as Magic handles the creation of user wallets, ZeroDev handles the creation of smart contract accounts associated with the wallet.
To initialize the kernel client, first we must declare the magic provider and pass it to the providerToSmartAccountSigner
function provided by ZeroDev. This will associate the Magic account as the signer of the smart contract account.
Next, we create the kernel client by calling createEcdsaKernelAccountClient
and passing the following values as arguments:
chain
: The chain to point to. We’ll be using Sepolia.projectId
: This comes from the ZeroDev project created earlier.signer
: ThesmartAccountSigner
we create using the Magic provider andproviderToSmartAccountSigner
function.paymaster?
: Sponsoring gas paymaster. This is optional but allows you to pay transaction fees on behalf of your users.
01const magicProvider = await magic?.wallet.getProvider();
02const smartAccountSigner = await providerToSmartAccountSigner(magicProvider);
03
04const client = await createEcdsaKernelAccountClient({
05 chain: sepolia,
06 projectId: process.env.NEXT_PUBLIC_ZERODEV_SEPOLIA_PROJECT_ID!,
07 signer: smartAccountSigner,
08 paymaster: "SPONSOR" // defaults to "SPONSOR". Use "NONE" if no policy is required.
09});
#Paymaster
Account abstraction enables you to cover gas fees on behalf of users, eliminating the need for them to obtain native tokens to engage with your DApp. ZeroDev makes sponsoring gas straight-forward in a few steps.
For this guide we are using testnet tokens. If you're on planning on using mainnet you will need to make a payment for your gas policy to be applied.
Head to your ZeroDev dashboard and navigate to the Sepolia project created earlier. On the left hand navigation, select "Gas Policies" and click the "New" button.
A display will pop up allowing you to set the type, amount and interval amount for your gas policy.
Once the gas policy has been added to your ZeroDev project, the user's gas will be subsidized so long as the value set has not been exceeded within the interval.
Now when a user logs in with Magic, their account is associated with the ZeroDev kernel as the smart account signer and their gas has been sponsored.
#Completed useZeroDevKernelClient
code
01import { sepolia } from "viem/chains"
02import { useCallback, useEffect, useMemo, useState } from "react"
03import { useMagic } from "../magic/MagicProvider"
04import { createEcdsaKernelAccountClient } from '@zerodev/presets/zerodev';
05import { providerToSmartAccountSigner } from 'permissionless';
06
07export const useZeroDevKernelClient = () => {
08 const { magic } = useMagic();
09 const [kernelClient, setKernelClient] = useState<any>();
10 const [scaAddress, setScaAddress] = useState<any>();
11
12 useEffect(() => {
13 const fetchAccount = async () => {
14 const magicProvider = await magic?.wallet.getProvider();
15 if (!magicProvider) return;
16
17 const smartAccountSigner = await providerToSmartAccountSigner(magicProvider);
18
19 const client = await createEcdsaKernelAccountClient({
20 chain: sepolia,
21 projectId: process.env.NEXT_PUBLIC_ZERODEV_SEPOLIA_PROJECT_ID!,
22 signer: smartAccountSigner,
23 paymaster: "NONE"
24 });
25 setKernelClient(client)
26
27 setScaAddress(client.account.address);
28 }
29
30 fetchAccount()
31 }, [magic])
32
33 return {
34 kernelClient,
35 scaAddress,
36 }
37}
Notice that our hook returns both the client and the smart contract account address. This will allow us to use destructuring to access either or both from the rest of our code.
#Update UI Components
Now that the project successfully creates and connects to users’ smart contract accounts with ZeroDev, we can update the UI to show the smart account address, its balance, and enable sending transactions from the smart contract account. These changes take place in the UserInfoCard
and the SendTransactionCard
.
#Update UserInfoCard
#Update state items
First things first. Open src/components/magic/cards/UserInfoCard.tsx
and make the following changes:
- Update the state declaration of
balance
,setBalance
, andpublicAddress
tomagicBalance
,setMagicBalance
, andmagicAddress
- Import our
useZeroDevKernelClient
hook and call it to get access toscaAddress
- Add a state declaration for
scaBalance
andsetScaBalance
to store the smart contract account balance.
01// Change this
02const [balance, setBalance] = useState("...")
03const [publicAddress] = useState(
04 localStorage.getItem("user")
05)
06
07// To this
08const [magicBalance, setMagicBalance] = useState<string>("...")
09const [scaBalance, setScaBalance] = useState<string>("...")
10const [magicAddress] = useState(
11 localStorage.getItem("user")
12)
13const { scaAddress } = useZeroDevKernelClient();
#Update getBalance
Next, update the getBalance
function to set both balances:
01const getBalance = useCallback(async () => {
02 if (magicAddress && web3) {
03 const magicBalance = await web3.eth.getBalance(magicAddress)
04 if (magicBalance == BigInt(0)) {
05 setMagicBalance("0")
06 } else {
07 setMagicBalance(web3.utils.fromWei(magicBalance, "ether"))
08 }
09 }
10 if (scaAddress && web3) {
11 const aaBalance = await web3.eth.getBalance(scaAddress)
12 if (aaBalance == BigInt(0)) {
13 setScaBalance("0")
14 } else {
15 setScaBalance(web3.utils.fromWei(aaBalance, "ether"))
16 }
17 }
18}, [web3, magicAddress, scaAddress])
#Update balance display
Next, update the TSX for displaying the balance to show both balances:
01<div className="flex flex-col gap-2">
02 <div className="code">
03 Magic: {magicBalance.substring(0, 7)} {getNetworkToken()}
04 </div>
05 <div className="code">
06 AA: {scaBalance.substring(0, 7)} {getNetworkToken()}
07 </div>
08</div>
#Update initial balances
The only remaining balance reference is to set the initial balance while loading to "..."
. This is in a short useEffect
that calls setBalance
. Update this useEffect
to set both balances:
01// Change this
02useEffect(() => {
03 setBalance('...');
04}, [magic]);
05
06// To this
07useEffect(() => {
08 setMagicBalance("...")
09 setScaBalance("...")
10}, [magic])
#Update address display
Now find the CardLabel
and div that displays the address and modify it to use the new naming for magicAddress
and also display the scaAddress
.
01<CardLabel
02 leftHeader="Addresses"
03 rightAction={
04 !magicAddress ? <Spinner /> : <div onClick={copy}>{copied}</div>
05 }
06/>
07<div className="flex flex-col gap-2">
08 <div className="code">
09 Magic:{" "}
10 {magicAddress?.length == 0 ? "Fetching address..." : magicAddress}
11 </div>
12 <div className="code">
13 Smart Contract Account:{" "}
14 {scaAddress?.length == 0 ? "Fetching address..." : scaAddress}
15 </div>
16</div>
#Update copy
function
Lastly, update the copy
function to reference magicAddress
instead of publicAddress
, otherwise you’ll get an error.
01const copy = useCallback(() => {
02 if (magicAddress && copied === "Copy") {
03 setCopied("Copied!")
04 navigator.clipboard.writeText(magicAddress)
05 setTimeout(() => {
06 setCopied("Copy")
07 }, 1000)
08 }
09}, [copied, magicAddress])
Now when a user logs in using Magic, both their Magic and smart contract account address and balances will be displayed!
#Update SendTransactionCard
To send a transaction from your smart contract account, you will need to initiate a transaction by calling the sendTransaction
method on the ZeroDev kernelClient
object. This transaction requires the following arguments:
target
- The recipient’s wallet addressdata
- Data associated with the transaction. Since we’re just transferring tokens, there is no data and you should put"0x"
value
- the amount of tokens to send inwei
.
In src/components/magic/cards/SendTransactionCard.tsx
, import the the kernelClient
from useZeroDevKernelClient
hook and replace the code for sendTransaction
with the code below.
To transfer funds from your smart contract account, ensure you have enough test tokens to send. You can get some test Sepolia tokens here.
01const sendTransaction = useCallback(async () => {
02 if (!web3?.utils.isAddress(toAddress)) {
03 return setToAddressError(true);
04 }
05 if (isNaN(Number(amount))) {
06 return setAmountError(true);
07 }
08 setDisabled(true);
09
10 try {
11 const result = await kernelClient.sendTransaction({
12 to: toAddress as `0x${string}`,
13 data: "0x",
14 value: web3.utils.toWei(amount, 'ether'),
15 });
16
17 showToast({
18 message: `Transaction Successful. TX Hash: ${result}`,
19 type: 'success',
20 });
21 setHash(result.hash);
22 setToAddress('');
23 setAmount('');
24 } catch (err) {
25 console.log(err)
26 }
27
28 setDisabled(false);
29 }, [web3, amount, publicAddress, toAddress]);
Thats it! Go ahead and run the project to test it out. Then you can test out transferring tokens from your smart contract account.
Note: If you set up a paymaster earlier, then this transaction won't cost the user any gas. To be sure, try changing the value
to 0
and send a transaction - you will notice that your ZeroDev account's balance didn't change at all.
#Next Steps
You now know how to integrate Magic with a smart contract account and include the following features:
- Simple authentication with Email OTP
- Automatic smart contract account creation for first-time users
- Ability to have Magic users interact with their smart contract accounts
- Transfer funds from your smart contract account
Feel free to take a look at our final solution code or tinker with it directly in Codesandbox. Take a look at the ZeroDev smart account docs for more information on what is possible with Magic and smart accounts.