guides
Guide

Alchemy Account Abstraction

Magic Staff · February 6, 2024

Alchemy’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 Alchemy. We’ll leverage a project pointed at the Ethereum Sepolia test network, but you can use any network supported by both Magic and Alchemy. The code snippets provided are based on a Next.js web app but can be modified to work with virtually any JavaScript framework.

You can view the full example on github or codesandbox.

#Project prerequisites

To follow along with this guide, you’ll need two things:

  1. A Magic Publishable API Key
  2. Alchemy RPC URL
  3. A web client

You can get your Publishable API Key from your Magic Dashboard.

You can get your Alchemy RPC URL (for Ethereum Sepolia) from your Alchemy Dashboard.

We’ll use the make-magic 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-magic CLI as the starting point. At the time of writing we are using make-magic version 4.10.0.

The make-magic 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 <PUBLISHABLE_API_KEY> with the Publishable API Key from your Magic Dashboard.

Bash
01npx make-magic \\
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 Alchemy RPC URL to the .env as NEXT_PUBLIC_MAGIC_API_KEY and NEXT_PUBLIC_SEPOLIA_RPC, respectively.

Plaintext
01// Publishable API Key found in the Magic Dashboard
02NEXT_PUBLIC_MAGIC_API_KEY=pk_live_1234567890
03
04// The RPC URL for the blockchain network
05NEXT_PUBLIC_BLOCKCHAIN_NETWORK=ethereum-sepolia
06
07// The Alchemy RPC URL for the blockchain network
08NEXT_PUBLIC_SEPOLIA_RPC=https://eth-sepolia.g.alchemy.com/v2/{ALCHEMY_API_KEY}

#Install additional project dependencies

In addition to the packages included in the scaffold produced by the make-magic CLI, you’ll need a number of packages related to Alchemy and their account abstraction tools. You’ll also need to install viem for EVM-related types and transaction convenience methods. You may need a specific version of viem to work properly with the Alchemy packages. At the time of writing, we’re using 1.16.0.

Run the following command to install the required dependencies:

Bash;npm
Bash;yarn
01npm install @alchemy/aa-accounts @alchemy/aa-alchemy @alchemy/aa-core viem@\^1.16.0

#Initialize Alchemy smart contract accounts

Inside of src/components, create a directory named alchemy. Inside that directory create a file named useAlchemyProvider.tsx.

This file will contain a hook that will surface the Alchemy Provider 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 AlchemyProvider

To initialize the AlchemyProvider, call the constructor with the following arguments:

  1. chain - The chain to point to. We’ll be using Sepolia
  2. entryPointAddress - The entry point address is the ERC-4337 contract that enables account abstraction. In our case, we’ll be using the one provided by Alchemy, which you can hardcode as 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789.
  3. rpcUrl - The RPC URL from your Alchemy Sepolia project

#Connect to smart contract account

When a user logs in with Magic, we need to associate their Magic account with a smart contract account through Alchemy. Just as Magic handles the creation of user wallets, Alchemy handles the creation of smart contract accounts associated with the wallet. You do this with the LightSmartContractAccount from the @alchemy/aa-accounts package. You’ll need to pass it the following:

  1. rpcClient - This should be the provider initialized previously
  2. owner - The account owner; in this case you initialize a WalletClientSigner using the RPC provider from magic
  3. chain - The chain to use. It should be the same one used to initialize the Alchemy Provider.
  4. entryPointAddress - The ERC-4337 contract that enables account abstraction. It should be the same one used to initialize the Alchemy Provider.
  5. factoryAddress - The address that facilitates the creation of new wallet contracts. You can get this with the getDefaultLightAccountFactoryAddress helper function from @alchemy/aa-accounts.

Below is an example of how to connect to a Magic user’s smart contract account:

Typescript
01const lightAccountFactoryAddress = getDefaultLightAccountFactoryAddress(chain)
02
03const magicSigner: SmartAccountSigner | undefined = useMemo(() => {
04  if (!magic) return
05
06  const client = createWalletClient({
07    transport: custom(magic.rpcProvider),
08  })
09
10  return new WalletClientSigner(client as any, "magic")
11}, [magic])
12
13provider.connect((provider) => {
14  return new LightSmartContractAccount({
15    rpcClient: provider,
16    owner: magicSigner,
17    chain,
18    entryPointAddress,
19    factoryAddress: lightAccountFactoryAddress,
20  })
21})

#Disconnect from smart contract account

When a user logs out, you’ll need to disconnect from their smart contract account. This is as simple as calling provider.disconnect() and handling necessary state changes.

#Completed useAlchemyProvider code

When we put all of this together, we get the following:

Typescript
01import {
02  getDefaultLightAccountFactoryAddress,
03  LightSmartContractAccount,
04} from "@alchemy/aa-accounts"
05import { SmartAccountSigner, WalletClientSigner } from "@alchemy/aa-core"
06import { AlchemyProvider } from "@alchemy/aa-alchemy"
07import { sepolia } from "viem/chains"
08import { createWalletClient, custom, WalletClient } from "viem"
09import { useCallback, useEffect, useMemo, useState } from "react"
10import { useMagic } from "../magic/MagicProvider"
11
12// Initializes the useAlchemyProvider hook for managing AlchemyProvider in a React component.
13export const useAlchemyProvider = () => {
14  const chain = sepolia
15  const lightAccountFactoryAddress = getDefaultLightAccountFactoryAddress(chain)
16  const entryPointAddress = useMemo(
17    () => "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789",
18    []
19  )
20  const { magic } = useMagic()
21  const [provider, setProvider] = useState<AlchemyProvider>(
22    new AlchemyProvider({
23      chain,
24      entryPointAddress,
25      rpcUrl: process.env.NEXT_PUBLIC_SEPOLIA_RPC!,
26    })
27  )
28
29  const magicSigner: SmartAccountSigner | undefined = useMemo(() => {
30    if (!magic) return
31
32    const client = createWalletClient({
33      transport: custom(magic.rpcProvider),
34    })
35
36    return new WalletClientSigner(client as any, "magic")
37  }, [magic])
38
39  useEffect(() => {
40    if (magic?.user.isLoggedIn) {
41      connectToSmartContractAccount()
42    } else {
43      disconnectFromSmartContractAccount()
44    }
45  }, [magic?.user.isLoggedIn])
46
47  // Connects the AlchemyProvider to a Smart Account using the LightSmartContractAccount class.
48  // Sets the owner as the Magic account wallet
49  const connectToSmartContractAccount = useCallback(() => {
50    if (!magicSigner) return
51
52    // This is where Magic is associated as the owner of the smart contract account
53    const connectedProvider = provider.connect((provider) => {
54      return new LightSmartContractAccount({
55        rpcClient: provider,
56        owner: magicSigner,
57        chain,
58        entryPointAddress,
59        factoryAddress: lightAccountFactoryAddress,
60      })
61    })
62
63    setProvider(connectedProvider)
64    return connectedProvider
65  }, [entryPointAddress, provider])
66
67  // Disconnects the AlchemyProvider from the current account.
68  const disconnectFromSmartContractAccount = useCallback(() => {
69    const disconnectedProvider = provider.disconnect()
70    setProvider(disconnectedProvider)
71    return disconnectedProvider
72  }, [provider])
73
74  // Returns the AlchemyProvider for use in components.
75  return {
76    provider,
77  }
78}

#Update UI Components

Now that the project successfully creates and connects to users’ smart contract accounts with Alchemy, we can update the UI to show the smart contract 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 change the state declaration of balance, setBalance, and publicAddress to magicBalance, setMagicBalance, and magicAddress. While you’re at it, add a state declaration for scaBalance, setScaBalance, scaAddress, and setScaAddress to store the smart contract account balance and address.

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, setScaAddress] = useState<string>("")

#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])

#Set scaAddress

Typescript
01const { provider } = useAlchemyProvider()
02
03const getSmartContractAccount = useCallback(async () => {
04  const aaAccount = await provider.account?.getAddress()
05  setScaAddress(aaAccount as `0x${string}`)
06}, [provider])
07
08useEffect(() => {
09  getSmartContractAccount()
10}, [provider, provider.account, getSmartContractAccount])

#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>

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 sendUserOperation method on the Alchemy provider 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.

The hash returned from sendUserOperation is not the User Operation Receipt, rather a proof of submission. We call the waitForUserOperationTransaction function, which will return the User Operation Receipt once the transaction has been bundled, included in a block and executed on-chain.

In src/components/magic/cards/SendTransactionCard.tsx, import the the provider from useAlchemyProvider 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    const result = await provider.sendUserOperation({
11      target: toAddress as `0x${string}`,
12      data: "0x",
13      value: web3.utils.toWei(amount, 'ether'),
14    });
15
16    const txHash = await provider.waitForUserOperationTransaction(result.hash)
17      .then((receipt) => {
18        showToast({
19          message: `Transaction Successful. TX Hash: ${receipt}`,
20          type: 'success',
21        });
22        setHash(receipt);
23        setToAddress('');
24        setAmount('');
25        console.log('Transaction receipt:', receipt);
26      })
27
28    console.log(txHash);
29    setDisabled(false);
30  }, [web3, amount, publicAddress, toAddress]);

Thats it! You’ve just transferred tokens from your newly created smart contract account!

#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 Alchemy smart account docs for more information on what is possible with Magic and smart accounts.

Let's make some magic!