Appearance
Account Abstraction with permissionless.js and Pimlico
TIP
Privy now allows you to natively use smart wallet for a better developer experience. Check out the docs here.
permissionless.js
is a modular and extensible TypeScript library originally created by Pimlico for deploying and managing ERC-4337 smart accounts. You can use this library for all major smart account implementations, including Safe, Kernel, Biconomy, SimpleAccount, and more.
You can easily integrate permissionless.js
alongside Privy to create smart wallets from your user's embedded or external wallets, allowing you to enhance your app with gas sponsorship, batched transactions, and more.
Just follow the steps below!
TIP
Want to see an end-to-end integration of Privy with permissionless.js
? Check out our example app!
What is an EOA?
An EOA, or externally-owned account, is any Ethereum account that is controlled by a private key. Privy's embedded wallets and most external wallets (MetaMask, Coinbase Wallet, Rainbow Wallet, etc.) are EOAs.
EOAs differ from contract accounts, which are instead controlled by smart contract code and do not have their own private key. Smart wallets are contract accounts. Contract accounts have enhanced capabilities, such as gas sponsorship and batched transactions.
Since they do not have their own private key, contract accounts cannot directly produce signatures and initiate transaction flows. Instead, each contract account is generally "managed" by an EOA, which authorizes actions taken by the contract account via a signature; this EOA is called a signer.
In this integration, the user's EOA (from Privy) serves as the signer for their smart wallet (from permissionless). The smart wallet holds all assets and submits all transactions to the network, but the signer (Privy) is responsible for producing signatures and "kicking off" transaction flows.
1. Install Privy and permissionless.js
In your project, install the necessary dependencies from Privy, Pimlico, and viem
:
bash
npm i @privy-io/react-auth permissionless viem
2. Sign up for a Pimlico account and create an API key.
To send transactions from smart accounts, you will need access to a bundler. We also recommend using paymaster to sponsor your user's transactions.
To get a bundler and paymaster for your application, sign up for a Pimlico account and copy down your API key for the rest of this guide!
3. Configure your app's PrivyProvider
First, follow the instructions in the Privy Quickstart to get your app set up with Privy.
Next, set Add confirmation modals to "off" in your app's Embedded wallets page in the Privy dashboard. This will configure Privy to not show its default UIs when your user must sign messages or send transactions. Instead, we recommend you use your own custom UIs for showing users the UserOperation
s they sign.
Then, update the config.embeddedWallets.createOnLogin
property of your PrivyProvider
to 'users-without-wallets'
.This will configure Privy to create an embedded wallet for users logging in via a web2 method (email, phone, socials), ensuring that all of your users have a wallet that can be used as an EOA.
Your PrivyProvider
should then look like:
tsx
<PrivyProvider
appId='insert-your-privy-app-id'
config={{
/* Replace this with your desired login methods */
loginMethods: ['email', 'wallet'],
/* Replace this with your desired appearance configuration */
appearance: {
theme: 'light',
accentColor: '#676FFF',
logo: 'your-logo-url'
}
embeddedWallets: {
createOnLogin: 'users-without-wallets',
noPromptOnSignature: true
}
}}
>
{/* Your app's components */}
</PrivyProvider>
4. Create a smart account for your user
You'll now create a smart account for your user, using the Privy embedded wallet (an EOA) as the signer.
To do so, when the user logs in, find the user's embedded wallet from Privy's useWallets
hook, and create a viem WalletClient
for it. You can find embedded wallet by finding the only entry in the useWallets
array with a walletClientType
of 'privy'
.
tsx
import {useWallets} from '@privy-io/react-auth';
import {sepolia} from 'viem/chains'; // Replace this with the chain used by your application
import {createWalletClient, custom} from 'viem';
...
// Find the embedded wallet and get its EIP1193 provider
const {wallets} = useWallets();
const embeddedWallet = wallets.find((wallet) => (wallet.walletClientType === 'privy'));
const eip1193provider = await embeddedWallet.getEthereumProvider();
// Create a viem WalletClient from the embedded wallet's EIP1193 provider
// This will be used as the signer for the user's smart account
const privyClient = createWalletClient({
account: embeddedWallet.address,
chain: sepolia, // Replace this with the chain used by your application
transport: custom(eip1193provider)
});
Next, using the privyClient
from above, create a SmartAccountClient
which represents the user's smart account. In creating the smart account, you can also specify which smart account implementation you'd like to use. Possible options include: Safe, Kernel, Biconomy, and SimpleAccount (the original smart account implementation).
If your app also uses a paymaster to sponsor gas on behalf of users, you can also specify which paymaster to use by calling the createPimlicoPaymasterClient
method from permissionless
with the RPC URL in your Pimlico Dashboard.
ts
import {createSmartAccountClient, walletClientToCustomSigner} from 'permissionless';
import {signerToSimpleSmartAccount} from 'permissionless/accounts';
import {createPimlicoPaymasterClient} from 'permissionless/clients/pimlico';
import {createPublicClient, http} from 'viem';
// The `privyClient` comes from the snippet above
const customSigner = walletClientToCustomSigner(privyClient);
// Create a viem public client for RPC calls
const publicClient = createPublicClient({
chain: sepolia, // Replace this with the chain of your app
transport: http(),
});
// Initialize the smart account for the user
const simpleSmartAccount = await signerToSimpleSmartAccount(publicClient, {
entryPoint: '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789',
signer: customSigner,
factoryAddress: '0x9406Cc6185a346906296840746125a0E44976454',
});
// Create the Paymaster for gas sponsorship using the API key from your Pimlico dashboard
const pimlicoPaymaster = createPimlicoPaymasterClient({
transport: http('https://api.pimlico.io/v2/sepolia/rpc?apikey=YOUR_PIMLICO_API_KEY'),
});
// Create the SmartAccountClient for requesting signatures and transactions (RPCs)
const smartAccountClient = createSmartAccountClient({
account: simpleSmartAccount,
chain: sepolia, // Replace this with the chain for your app
transport: http('https://api.pimlico.io/v1/sepolia/rpc?apikey=YOUR_PIMLICO_API_KEY'),
sponsorUserOperation: pimlicoPaymaster.sponsorUserOperation, // If your app uses a paymaster for gas sponsorship
});
When using the snippets above, make sure replace YOUR_PIMLICO_API_KEY
with your Pimlico API key that you created in step 2!
TIP
You can also store the user's smart account address on Privy's user object. See this guide for more.
Want to see this code end-to-end?
You can find the code snippets above pasted in an end-to-end example below.tsx
/**
* This example assumes your app is wrapped with the `PrivyProvider` and
* is configured to create embedded wallets for users upon login. Aside from
* the imports, all of the code in this snippet must be used within a React component
* or context.
*/
import {useWallets} from '@privy-io/react-auth';
import {sepolia} from 'viem/chains'; // Replace this with the chain used by your application
import {createWalletClient, createPublicClient, custom, http} from 'viem';
import {createSmartAccountClient, walletClientToCustomSigner} from "permissionless";
import {createPimlicoPaymasterClient} from "permissionless/clients/pimlico";
import {signerToSimpleSmartAccount} from "permissionless/accounts";
...
// Find the embedded wallet and get its EIP1193 provider
const {wallets} = useWallets();
const embeddedWallet = wallets.find((wallet) => (wallet.walletClientType === 'privy'));
const eip1193provider = await embeddedWallet.getEthereumProvider();
// Create a viem WalletClient from the embedded wallet's EIP1193 provider
const privyClient = createWalletClient({
account: embeddedWallet.address,
chain: sepolia, // Replace this with the chain used by your application
transport: custom(eip1193provider)
});
// Create a viem public client for RPC calls
const publicClient = createPublicClient({
chain: sepolia, // Replace this with the chain of your app
transport: http()
})
// Initialize the smart account for the user using the embedded wallet as the signer
const customSigner = walletClientToCustomSigner(privyClient);
const simpleSmartAccount = await signerToSimpleSmartAccount(publicClient, {
entryPoint: "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789",
signer: customSigner,
factoryAddress: "0x9406Cc6185a346906296840746125a0E44976454"
})
// Create the Paymaster for gas sponsorship using the API key from your Pimlico dashboard
const pimlicoPaymaster = createPimlicoPaymasterClient({
transport: http(
"https://api.pimlico.io/v2/sepolia/rpc?apikey=YOUR_PIMLICO_API_KEY",
),
})
// Create the SmartAccountClient for requesting signatures and transactions
const smartAccountClient = createSmartAccountClient({
account: simpleSmartAccount,
chain: sepolia, // Replace this with the chain for your app
transport: http("https://api.pimlico.io/v1/sepolia/rpc?apikey=YOUR_PIMLICO_API_KEY"),
sponsorUserOperation: pimlicoPaymaster.sponsorUserOperation // If your app uses a paymaster for gas sponsorship
})
Note: if your app uses React, we suggest that you store the user's SmartAccountClient
in a React context that wraps your application. This allows you to easily access the smart account from your app's pages and components.
5. Send transactions from the smart account
You can now send transactions using the sendTransaction
method on the SmartAccountClient
object, like so:
ts
const txHash = await smartAccountClient.sendTransaction({
account: smartAccountClient.account,
to: zeroAddress,
data: '0x',
value: BigInt(0),
});
You can also request signatures, typed data signatures, and more from the smart account! The SmartAccountClient
functions as a drop-in replacement for viem
's wallet client - you can use the same interfaces with the SmartAccountClient
object!
That's it! Once you've created smart accounts for your users, you can easily add AA features into your application like gas sponsorship, batched transactions, and more. 🎉