One of the biggest challenges in Web3 today is improving user experience. Traditionally, users are required to have a crypto wallet, manage gas fees, and sign their own transactions to interact with decentralized applications (dapps). This complex process often creates friction for users unfamiliar with blockchain. However, ERC-4337 account abstraction changes the game by making Web3 feel more like Web2. With it, users no longer need to create wallets, handle gas fees, or sign transactions — these steps can be abstracted away. In this article, we’ll explore how to implement account abstraction in your dapp using Privy, simplifying the Web3 experience.
https://medium.com/media/90e6277b224370f45b09056b4ec4261b/href
Table of Contents
· Table of Contents
· Intro to Account Abstraction
· Introduction to Privy
· Hands On Demonstration
∘ 1- Creating the Privy App
∘ 2- Creating the Dapp
∘ 3 Test the smart account
· Conclusions
· Code Repository
· References
Intro to Account Abstraction
The user experience for newcomers to crypto can be challenging due to several factors:
Private keys are difficult to manage: Users must securely handle long, complex private keys, which are easy to lose or mishandle.Every action incurs gas fees: Simple transactions require users to spend cryptocurrency on gas, which can be confusing and costly.Maintaining wallet privacy is hard: Ensuring anonymity and privacy while using a wallet can be complex, especially for beginners.Signing every transaction: Every on-chain action must be signed by the user’s wallet, adding another layer of complexity, as it requires users to interact with cryptographic tools that aren’t always user-friendly.
Account abstraction aims to solve many of the issues related to current private key and wallet management. Simply put, it offers a new approach to transaction signing.
In traditional crypto transactions, you need to use your private key to sign the data and send the transaction, which is how tools like MetaMask work:
With account abstraction, however, you can sign transactions using alternative methods such as Google, GitHub, X (formerly Twitter), LinkedIn, Instagram, Telegram, and others, making the process more flexible and user-friendly.
Additionally, account abstraction introduces the ability to create custom transaction rules, such as:
Requiring multiple users to sign a transaction (multi-signature transactions).Setting time-based restrictions (e.g., only allowing transactions during the day).Implementing spending limits that prevent transactions exceeding a certain amount.
These features provide more control and flexibility over how wallets and transactions operate, creating a smoother and more secure user experience.
With account abstraction, a new level of customization is introduced that allows others to pay for your transaction fees. Before this innovation, performing any on-chain operation required having enough gas fees in your own wallet. This typically meant buying the gas (ETH, MATIC, etc.) from an exchange and transferring it to your wallet to cover transaction costs.
However, with account abstraction, users are no longer required to hold gas in their wallets. Instead, they can create the transaction, and a third party (like a sponsor or dApp) can cover the gas fees on their behalf. This allows for a much smoother onboarding process where the user doesn’t need to worry about acquiring gas tokens, reducing friction significantly.
The user doesn’t even need a traditional wallet. They simply generate the transaction, and someone else — such as a service provider — can pay the gas fees for them. This feature helps to simplify the user experience while making decentralized applications more accessible to a broader audience.
Let’s understand how account abstraction works.
In a traditional user experience, the user has a wallet that allows them to manage an externally owned account (EOA). The user holds a private key, which they use to sign transactions before submitting them to a blockchain node.
In the context of account abstraction defined by ERC-4337, there are five key actors:
User: Initiates a user operation, which functions as a meta transaction.Bundler: Collects, verifies, and signs user operations from various users. It bundles these operations into a single transaction, acting similarly to an Externally Owned Account (EOA) in traditional transactions. The bundled transaction is then sent to the Entry Point.Entry Point: Receives the bundled transaction from the Bundler. It verifies the validity of each user operation and communicates with the Paymaster to ensure that there are sufficient funds in the Entry Point’s account to cover the gas fees for the operations. Once validated, it calls the Smart Contract Account to execute the transaction.Paymaster: Responsible for sponsoring the gas fees associated with the user operations.Smart Contract Account: Executes all user operations contained in the transaction that the Entry Point has forwarded.https://blog.thirdweb.com/account-abstraction-erc4337/
Introduction to Privy
Implementing account abstraction in a dApp can be quite challenging due to its inherent complexity. I recently discovered Privy, which greatly simplifies the process of generating a smart account for our users by abstracting much of the complexity.
The Privy React SDK is an authentication library that provides one of the easiest ways to onboard users to web3 in a React app providing:
Multiple login options: Users can sign in via email, phone, external wallets, or popular social platforms, offering flexible login experiences.Customizable onboarding UIs: You can tailor the user journey, progressively introducing web3 concepts to your users at their own pace.Self-custodial embedded wallets: Privy offers powerful embedded wallet solutions while maintaining self-custody, and it also includes robust connectors for external wallets.
This is an example of how a login looks with Privy:
Privy offers multiple login methods to simplify the user onboarding experience. Users can choose from various options, including:
Email: A straightforward method for users who prefer traditional email authentication.Google: Quick login with an existing Google account.X (formerly Twitter): Users can sign in using their X (Twitter) account.Other Socials: Privy also supports a wide range of other social platforms, including Discord, GitHub, TikTok, LinkedIn, Farcaster, Telegram, and more, offering flexibility for different user preferences.
This flexibility in login methods ensures users can easily connect to your dApp using the platform or method they’re most comfortable with, creating a seamless web3 experience.
Hands On Demonstration
This is the Dapp that we are going to build with Privy:
The code repository is available here.
1- Creating the Privy App
To begin with let’s connect to privy.io and create our Privy app.
In the “Login Methods” section, we can configure user authentication. We can allow users to log in via email, SMS, or external wallets.
We can also allow our user to log in with socials, by configuring the social of interest in the socials panel:
In the section “Embedded wallets” – “Smart wallets” we can configure the settings related to the smart account.
In the section “Enable smart wallets for your app” I have chosen LightAccount but you can choose any of the options presented:
In the section “Configure chain” we can configure the chain, the bundler and paymaster URLs:
I have chosen Base Sepolia as the blockchain, and for the bundler and paymaster URLs, I used the API key generated with Pimlico. Pimlico will sponsor the gas fees for our transactions since we’re using its paymaster.
2- Creating the Dapp
Let’s start by creating a new NextJS application with:
npx create-next-app@latest
Let’s also add the library NextUI :
npm install @nextui-org/react framer-motion
And modify the file tailwind.config.ts:
import { nextui } from “@nextui-org/react”;
import type { Config } from “tailwindcss”;
const config: Config = {
content: [
“./pages/**/*.{js,ts,jsx,tsx,mdx}”,
“./components/**/*.{js,ts,jsx,tsx,mdx}”,
“./app/**/*.{js,ts,jsx,tsx,mdx}”,
“./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}”,
],
theme: {
extend: {},
},
plugins: [nextui()],
};
export default config;
Then we install the Privy library:
npm install @privy-io/react-auth@latest
In the root directory, create a new file called .env.local. Inside this file, add your Privy app ID, which you can retrieve from the Privy dashboard:
NEXT_PUBLIC_PRIVY_APP_ID=<your_privy_app_id>
Create a new file called providers.tsx, where we will configure Privy:
export default function Providers({ children }: { children: React.ReactNode }) {
return (
<NextUIProvider>
<PrivyProvider
appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID as string}
config={{
appearance: {
theme: “light”,
accentColor: “#676FFF”,
},
embeddedWallets: {
createOnLogin: “all-users”,
},
defaultChain: baseSepolia,
supportedChains: [baseSepolia],
}}
>
<SmartWalletsProvider>
<main className=”h-full”>{children}</main>
</SmartWalletsProvider>
</PrivyProvider>
</NextUIProvider>
);
}
The embeddedWallet setting refers to the wallet that Privy automatically generates for the user. This is not a smart account. For instance, when a user logs in via Google, Facebook, email, or other methods, Privy creates an embedded wallet for them.
Wrap your application inside the template.tsx file with the Providers component:
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang=”en”>
<body className={`${raleway.className} antialiased`}>
<Providers>{children}</Providers>
</body>
</html>
);
}
Let’s create a very simple page:
export default function Home() {
const { ready, authenticated, logout, user } = usePrivy();
const { login } = useLogin();
const { client } = useSmartWallets();
const [isLoadingNft, setIsLoadingNft] = useState(false);
const [recipientNftAddress, setRecipientNftAddress] = useState(“”);
const [errorMessageNft, setErrorMessageNft] = useState(“”);
const [nftTx, setNftTx] = useState(“”);
const mintNftTransaction = async () => {
setIsLoadingNft(true);
setNftTx(“”);
if (!client) {
console.error(“No smart account client found”);
return;
}
setErrorMessageNft(“”);
try {
const tx = await client.sendTransaction({
chain: baseSepolia,
to: ERC721_ADDRESS,
value: BigInt(0),
data: encodeFunctionData({
abi: erc721Abi,
functionName: “safeMint”,
args: [recipientNftAddress as `0x${string}`],
}),
});
console.log(“tx”, tx);
setNftTx(tx);
} catch (error) {
console.error(“Transaction failed:”, error);
setErrorMessageNft(“Transaction failed. Please try again.”);
}
setIsLoadingNft(false);
};
const handleLogout = () => {
// Reset all input fields
setIsLoadingNft(false);
setRecipientNftAddress(“”);
setErrorMessageNft(“”);
setNftTx(“”);
logout();
};
return (
<div className=”min-h-screen min-w-screen”>
<div className=”grid grid-cols-1 lg:grid-cols-4 h-screen text-black”>
<div className=” col-span-2 bg-gray-50 p-12 h-full flex flex-col lg:flex-row items-center justify-center space-y-2″>
<div className=”flex flex-col justify-evenly h-full”>
<div className=”flex flex-col gap-4″>
<div className=”flex flex-col gap-2″></div>
<div className=”text-3xl lg:text-6xl font-black”>
Privy Tutorial
</div>
<div className=”text-md lg:text-lg”>Your Privy Tutorial App</div>
{ready && !authenticated && (
<Button
radius=”sm”
color=”secondary”
className=”w-4 text-white”
onClick={() => login()}
>
Login
</Button>
)}
{ready && authenticated && (
<Button
radius=”sm”
color=”danger”
className=”w-4″
onClick={handleLogout}
>
Logout
</Button>
)}
</div>
</div>
</div>
<div className=”col-span-2 bg-white h-full p-12 lg:p-48 flex flex-col lg:flex-row items-center justify-center w-full space-y-4″>
{!user && <div className=”lg:w-1/2″></div>}
{user && (
<div className=”lg:flex lg:flex-row justify-center w-full”>
<div className=”flex flex-col gap-4 w-full”>
<div className=”flex flex-col gap-2 w-full”>
<div className=”flex flex-col gap-2″>
<div className=”text-base font-semibold”>Wallets</div>
<div className=”flex items-center”>
<Input
size=”sm”
value={user.wallet?.address}
label=”Embedded Wallet”
isReadOnly
className=”flex-grow”
/>
</div>
<div className=”flex items-center”>
<Input
size=”sm”
value={client?.account.address}
label=”Smart Wallet”
isReadOnly
className=”flex-grow”
/>
</div>
<Divider />
<div className=”flex flex-col gap-2″>
<div className=”text-base font-semibold”>Mint NFT</div>
<div className=”flex items-center”>
<Input
size=”sm”
value={recipientNftAddress}
onChange={(e) =>
setRecipientNftAddress(e.target.value)
}
placeholder=”Enter recipient address”
label=”Recipient Address”
/>
</div>
<Button
radius=”sm”
color=”secondary”
className=”w-1/5″
onClick={() => mintNftTransaction()}
isLoading={isLoadingNft}
isDisabled={!recipientNftAddress}
>
Mint NFT
</Button>
{errorMessageNft && (
<div className=”text-red-500 text-xs text-center mt-1″>
{errorMessageNft}
</div>
)}
{nftTx && (
<div className=”flex flex-col”>
<div className=”flex items-center”>
<Input
size=”sm”
value={nftTx}
label=”NFT Minting Transaction”
isReadOnly
className=”flex-grow”
/>
</div>
</div>
)}
</div>
</div>
</div>
</div>
</div>
)}
</div>
</div>
</div>
);
}
At the start the applications is very simple with just a Login Button to log in the user by means of Privy:
As the user has logged in the applican looks like this:
In particular in the right hand side we have:
We have different input fields:
Embedded Wallet: This is the wallet generated by Privy during login.Smart Wallet: This refers to the smart account created using account abstraction, with gas fees sponsored by the Pimlico paymaster.
Next, we have a “Mint NFT” section, which we’ll use to test the smart account.
3 Test the smart account
In the previous panel we have a button “Mint NFT” that sends a transaction from the smart account to an ERC 721 deployed on Base Sepolia at the address 0x515F9a95c2d21fDe738CE65f7025C3BC79c7239A:
// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.0.0
pragma solidity ^0.8.26;
import “@openzeppelin/contracts/token/ERC721/ERC721.sol”;
import “@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol”;
contract MyNFT is ERC721, ERC721Enumerable {
uint256 private _nextTokenId;
constructor()
ERC721(“MyNFT”, “NFT”)
{}
function safeMint(address to) public {
uint256 tokenId = _nextTokenId++;
_safeMint(to, tokenId);
}
// The following functions are overrides required by Solidity.
function _update(address to, uint256 tokenId, address auth)
internal
override(ERC721, ERC721Enumerable)
returns (address)
{
return super._update(to, tokenId, auth);
}
function _increaseBalance(address account, uint128 value)
internal
override(ERC721, ERC721Enumerable)
{
super._increaseBalance(account, value);
}
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721, ERC721Enumerable)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}
If we add the recipient address and press the “Mint NFT” button, it will be created an user operation from the smart account that will allow us to mint a new NFT from the ERC721 contract and the Pimlico paymaster will sponsor the gas fees.
As we press the button “Mint NFT” a new transaction will be created and the transaction hash will be showed on the screen:
We can also check the transaction on Sepolia Basescan here.
Conclusions
In this article, we explored how Privy enables our dapp to implement account abstraction, simplifying the complexities behind it. Privy proves to be a valuable tool for easing user onboarding and bridging the gap between Web3 and Web2 applications. Adding this library to your dapp can significantly enhance the user experience.
Code Repository
https://github.com/RosarioB/privy_tutorial
References
What is Account Abstraction? ERC-4337A Deep Dive into ERC 4337Privy Doc
Streamline Dapp Access: Implementing Account Abstraction ERC-4337 with Privy was originally published in Coinmonks on Medium, where people are continuing the conversation by highlighting and responding to this story.