Alchemy Account Abstraction
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:
- A Magic Publishable API Key
- Alchemy RPC URL
- 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-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 generate your application, 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 Alchemy RPC URL to the .env
as NEXT_PUBLIC_MAGIC_API_KEY
and NEXT_PUBLIC_SEPOLIA_RPC
, respectively.
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-scoped-magic-app
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:
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:
chain
- The chain to point to. We’ll be using SepoliaentryPointAddress
- 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 as0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789
.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:
rpcClient
- This should be the provider initialized previouslyowner
- The account owner; in this case you initialize aWalletClientSigner
using the RPC provider frommagic
chain
- The chain to use. It should be the same one used to initialize the Alchemy Provider.entryPointAddress
- The ERC-4337 contract that enables account abstraction. It should be the same one used to initialize the Alchemy Provider.factoryAddress
- The address that facilitates the creation of new wallet contracts. You can get this with thegetDefaultLightAccountFactoryAddress
helper function from@alchemy/aa-accounts
.
Below is an example of how to connect to a Magic user’s smart contract account:
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:
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.
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:
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])
#Set scaAddress
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
.
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:
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
.
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 provider
from useAlchemyProvider
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 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:
- 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 code solution. Take a look at the Alchemy smart account docs for more information on what is possible with Magic and smart accounts.