A tutorial that teaches how to implement Account Abstraction into a Base project using Privy and the Base paymaster.
UserOperation
s to mitigate these limitationsPrivyProvider
context and usePrivy
hook to implement basic authentication via an email address, SMS, EOA, and/or social authallow users to use smart contract wallets containing arbitrary verification logic instead of EOAs as their primary account. Completely remove any need at all for users to also have EOAs (as status quo SC wallets and EIP-3074 both require)In other words, the proposal seeks to allow users to use smart contract wallets instead of EOAs to transact onchain.
UserOperations
s with a function called, validateUserOp
. validateUserOp
will check each UserOperation
’s signature, increment the nonce, and handle the operation’s fees.
UserOperations
s are created. These are not yet transactions, but rather represent intents from the user. These intents can represent any onchain user operation.
UserOperation
includes the details of the transaction such as sender, nonce, gas limit, max fee per gas, paymaster data (if applicable), and a signature.
UserOperation
is then signed using the private key associated with the initiating account. This signature serves to authenticate the transaction and validate that it was indeed initiated by the owner of the smart contract account.
UserOperation
s and consolidating them into a single transaction, called a bundle transaction. These bundle transactions are then directed towards a universal smart contract, called the EntryPoint.
The submission of the signed UserOperation
to the EntryPoint contract can be done directly or through a paymaster, which is a contract that agrees to cover the cost of operations for certain users.
handleOps
on the EntryPoint smart contract, which receives the bundle transaction. The EntryPoint then calls validateUserOp
for each account within this bundle transaction.
Each smart contract wallet is then required to implement an additional function and execute the actual operation sent by the EntryPoint contract.
user
object for each session. Before authentication, this user
object is null
, but as the user interacts with your application it will progressively associate more user information with this object. For example, users can start by authenticating with their email address and later add their wallet address or any other user information as the application requires.
Embedded Wallets: Embedded wallets are “self-custodial Ethereum wallets that are embedded into your app. This allows your users to take wallet-based actions without ever leaving your site. Embedded wallets are the easiest way to unlock your full product experience for users who don’t have, or don’t want to connect, their own wallet.” Developers can simply configure Privy to automatically created an Embedded Wallet on login
or they can be pregenerated on the backend.
msg.sender
is not the address you were expecting.cd create-next-app
and install dependencies with yarn
.
.env.local
file in your project’s root. This is where you’ll add your Privy App ID:
base@privy.io
with:yarn dev
and navigate to [http://localhost:3000] to see the starter application.
/dashboard
page, where the demo app will allow you to connect a number of other accounts to your user
object:
PrivyProvider
inside of _app.jsx
:
PrivyProvider
uses React Context and wraps any components that will use the usePrivy
hook.
Additionally, it’s here that you can pass an optional config
property to enable more authentication methods.
Add a config
property to the <PrivyProvider />
in _app.jsx
with 'github'
and 'sms'
as the login options:
loginMethods
in the docs for PrivyClientConfig
.
usePrivy
HookusePrivy
. Open pages/dashboard.tsx
to see the methods decomposed from usePrivy
in the starter, and how they are used.
A full list of the fields and methods returned from usePrivy
are documented here.
useWallets
HookuseWallets
hook:
Embedded wallets are self-custodial Ethereum wallets that are embedded into your app. This allows your users to take wallet-based actions without ever leaving your site. Embedded wallets are the easiest way to unlock your full product experience for users who don’t have, or don’t want to connect, their own wallet.When configuring your app to create embedded wallets on login, you have 2 options:
users-without-wallets
: This will create embedded wallets for all use who did not login with an external walletall-users
: This will create an additional embedded wallet for all users, regardless if they have linked an external wallet_app.tsx
, update your PrivyProvider
:
linkedAccount
which is the Privy Embedded Wallet:
about
section of the Github page links to a deployed version of the app. It’s the same app you get from Privy’s Quick Start, with the addition of a mint button (the versions may be a little older).
The app is limited to social auth, so log in with either your Google account or email. You’ll see the dashboard, with the addition of a Mint NFT
button at the top.
Click the button and you’ll see a toast notification informing you of updates to the transaction status. Note that this happens without you needing to approve a transaction or fund a wallet!
Click to see the transaction when it’s done to open BaseScan. If you missed it, mint another NFT, it’s not like you’re paying gas!
ERC-721 Tokens Transferred
section, click the link to NFT Name (NFT)
to open up the overview page for the token. You’ll see a list of transfers, with yours likely on the top. Click the address for the contract to open up the view for the contract itself.
You may be surprised to see that there are very few transactions listed for this contract, despite the list of transfers you can see on the token page, or the Events
tab. Currently, Etherscan and BaseScan won’t display transactions done via the paymaster.
From:
field. It will contain 0x1e6754b227c6ae4b0ca61d82f79d60660737554a
. What is this address? It’s not your smart wallet address or signer address. If you mint another NFT from a different login, you’ll get the same sender.
This address is the bundler, which is a special node that bundles user operations from the alt mempool into a single transaction and sends them to the single EntryPoint contract.
0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789
. Strangely, in your transaction receipt you’ll see that the transaction includes a transfer of ETH from the EntryPoint to the bundler. This transaction is how the bundler gets compensated for performing the service of bundling user ops and turning them into transactions — the EntryPoint calculates the gas used by user ops and multiplies that by the fee percentage and send it to the bundler.
SmartAccountContext.tsx
hooks/SmartAccountContext.tsx
uses a React Context provider to create a SmartAccountProvider`` and pass it into your app. You can see it in use in
_app.tsx, with the regular
PrivyProvider` around it. Review the file in detail.
Starting on line 63, the exported SmartAccountProvider
does the following:
PrivyProvider
lib/constants.ts
signer
is your EOA embedded wallet created by Privy and fetched in the first stepYOUR SIGNER ADDRESS
YOUR SMART WALLET ADDRESS
sendSponsoredUserOperation
function takes a traditional transaction, turns it into a user operation, adds the data for the paymaster to pay the gas, signs it, and sends it. Whew!
If you want a deeper dive into the inner workings of this process, review the helper functions in user-operations.ts
.
pages/dashboard.tsx
and take a look at the onMint
function on line 32. This function is used as the oncClick
handler for the Mint
button at the top of the dashboard.
If you’re used to working with wagmi, you’ll find the process of sending and awaiting for confirmation of a transaction a little on the manual side. Most of this will be familiar if you’ve used viem directly, or have worked with Ethers.
When a user clicks, the app first creates a viem RpcTransactionRequest
for the mint
function on the smart contract. The smartContractAddress
is supplied by the SmartAccountProvider
, and the ABI
and contract NFT_ADDRESS
are loaded from lib/constants.ts
:
await
s first the userOpHash
, then the transactionHash
, indicating that transaction has completed successfully. It then updates the link in the toast to send the user to that transaction on Goerli BaseScan.
create-next-app
template, and complete the setup instructions in the readme.
Add an environment variable for NEXT_PUBLIC_ALCHEMY_API_KEY
and paste in the an API key for a Base Goerli app. If you need a key, go to add an app and create a new one.
hooks
and lib
folders into your new project. You’ll need to install some more dependencies. Use npm
or yarn
to add:
SmartAccountContext.tsx
in your project. You’ll see an error for getDefaultLightAccountFactory
. The name of this function has been updated to getDefaultLightAccountFactoryAddress
. Change it in the import, and where it is used in the file in the call to LightSmartContractAccount
.
SmartAccountProvider
. Instead of find
ing the user’s Privy wallet:
useEffect
to createSmartWallet
if there is an embeddedWallet
to instead create it if there is a wallet
, using that wallet
. You’ll also need to update the dependency in the dependency array.
PrivyProvider
allows logging in with a wallet or email address. To limit it to only the wallet, update the config. You can also set the default chain here. You’ll need to import baseGoerli
to do so.
You also need to import and wrap the app with SmartAccountProvider
, imported from hooks/SmartAccountContext.tsx
.
@alchemy/aa-core
package also exports SmartAccountProvider
and this export takes precedence when VSCode attempts to help you by automatically adding the import. You’ll know you’ve got the wrong one if SmartAccountProvider
generates an error that:dashboard.tsx
in the new project:
<p>
for the User Object
window.
You’ll need to import BASE_GOERLI_SCAN_URL
from constants.ts
. The useSmartAccount
hook returns smartAccountProvider
and eoa
. Import it and add it under the usePrivy
hook. You don’t need them just yet, but go ahead and decompose smartAccountProvider
and sendSponsoredUserOperation
as well:
YOUR SIGNER ADDRESS
!
config
. If you see the Log In
button but clicking it does nothing, try manually navigating to localhost:3000/dashboard
or clearing the cache.mint
function in the original example. In the DashboardPage
component, add a state variable holding an empty element:
onMint
function that sets this variable and has the code related to the toast removed.
Note: make sure you change your wallet address in args
to make sure the NFT is sent to your EOA wallet address!
RpcTransactionRequest
in sendSponsoredUserOperation
to match the address, abi, function name, and arguments of your function on your smart contract.
For example, to call the claim
function in the Weighted Voting contract we’ve used in other tutorials, you’d simply need to import the Hardhat-style artifact for the contract and use it to call the function: