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:

  1. A Magic Publishable API Key
  2. A ZeroDev Project ID
  3. 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.

Bash
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.

Plaintext
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:

NPM
Yarn
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:

  1. chain: The chain to point to. We’ll be using Sepolia.
  2. projectId: This comes from the ZeroDev project created earlier.
  3. signer: The smartAccountSigner we create using the Magic provider and providerToSmartAccountSigner function.
  4. paymaster?: Sponsoring gas paymaster. This is optional but allows you to pay transaction fees on behalf of your users.
Typescript
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.

note

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

Typescript
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:

  1. Update the state declaration of balance, setBalance, and publicAddress to magicBalance, setMagicBalance, and magicAddress
  2. Import our useZeroDevKernelClient hook and call it to get access to scaAddress
  3. Add a state declaration for scaBalance and setScaBalance to store the smart contract account balance.
Typescript
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:

Typescript
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:

Typescript
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:

Typescript
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.

Typescript
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.

Typescript
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:

  1. target - The recipient’s wallet address
  2. data - Data associated with the transaction. Since we’re just transferring tokens, there is no data and you should put "0x"
  3. value - the amount of tokens to send in wei.

In src/components/magic/cards/SendTransactionCard.tsx, import the the kernelClient from useZeroDevKernelClient hook and replace the code for sendTransaction with the code below.

note

To transfer funds from your smart contract account, ensure you have enough test tokens to send. You can get some test Sepolia tokens here.

Typescript
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:

  1. Simple authentication with Email OTP
  2. Automatic smart contract account creation for first-time users
  3. Ability to have Magic users interact with their smart contract accounts
  4. 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.