Appearance
Account abstraction with Biconomy
Biconomy is a toolkit for creating ERC-4337-compatible smart accounts for your users, using the user's EOA as the smart account's signer. This allows you to easily add Account Abstraction features into your app.
You can easily integrate Biconomy alongside Privy to create smart wallets from your user's embedded or external wallets, allowing you to enhance your app with gas sponsorship and more!
Read below to learn how to configure your app to create smart wallets for all your users!
TIP
Want to see an end-to-end integration of Privy with Biconomy? Check out our example app and repo!
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. Biconomy's smart wallet is a contract account. 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 Biconomy). The smart wallet (Biconomy) 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 Biconomy
In your app's repository, install the @privy-io/react-auth
SDK from Privy and the @biconomy/{account, bundler, common, core-types, paymaster}
SDKs from Biconomy:
sh
npm i @privy-io/react-auth @biconomy/account @biconomy/bundler @biconomy/common @biconomy/core-types @biconomy/paymaster
2. Configure your app's PrivyProvider
First, follow the instructions in the Privy Quickstart to get your app set up with Privy.
Next, set Wallet UIs 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
}
}}
>
{children}
</PrivyProvider>
3. Configure your Biconomy bundler and paymaster
Go to the Biconomy Dashboard and configure a Paymaster and a Bundler for your app. Make sure these correspond to the desired network for your user's smart accounts.
Once you've configured a Paymaster, you can also deposit funds into your app's gas tank and configure specific policies for gas sponsorship.
4. Initialize your users' smart accounts
When users log into your app, Privy provisions each user an embedded wallet, which is an EOA. In order to leverage the features of Biconomy's account abstraction, each user also needs a Biconomy smart account. You can provision Biconomy smart accounts for each user by assigning their embedded wallet as a signer for their smart account.
To start, after a user logs in, find the user's embedded wallet from Privy's useWallets
hook, and switch its network to your app's target network. 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';
...
// Find the embedded wallet
const {wallets} = useWallets();
const embeddedWallet = wallets.find((wallet) => (wallet.walletClientType === 'privy'));
// Switch the embedded wallet to your target network
// Replace '80001' with your desired chain ID.
await embeddedWallet.switchChain(80001);
Next, initialize instances of a Biconomy bundler
and paymaster
for the user, following the code snippet below. You'll need to pass in your bundler and paymaster URLs that you previously configured in the Biconomy Dashboard.
tsx
import { IBundler, Bundler } from '@biconomy/bundler';
import { IPaymaster, BiconomyPaymaster } from '@biconomy/paymaster';
import { ChainId } from "@biconomy/core-types";
...
// Initialize your bundler
const bundler: IBundler = new Bundler({
bundlerUrl: 'your-bundler-url-from-the-biconomy-dashboard',
chainId: ChainId.POLYGON_MUMBAI, // Replace this with your desired network
entryPointAddress: DEFAULT_ENTRYPOINT_ADDRESS, // This is a Biconomy constant
});
// Initialize your paymaster
const paymaster: IPaymaster = new BiconomyPaymaster({
paymasterUrl: 'your-paymaster-url-from-the-biconomy-dashboard',
});
Then, initialize a validation module for the user's smart account, by passing an ethers signer from the user's embedded wallet to Biconomy's ECDSAOwnershipValidationModule
. This allows the user to authorize actions from their Biconomy smart account by signing messages with their Privy embedded wallet.
tsx
import { ECDSAOwnershipValidationModule, DEFAULT_ECDSA_OWNERSHIP_MODULE } from "@biconomy/modules";
...
// Get an ethers provider and signer for the user's embedded wallet
const provider = await embeddedWallet.getEthersProvider();
const signer = provider.getSigner();
// Initialize Biconomy's validation module with the ethers signer
const validationModule = await ECDSAOwnershipValidationModule.create({
signer: signer,
moduleAddress: DEFAULT_ECDSA_OWNERSHIP_MODULE // This is a Biconomy constant
});
Lastly, using the user's paymaster, bundler, and validation module instances from above, initialize the user's smart account using Biconomy's BiconomySmartAccountV2.create
method:
tsx
import { BiconomySmartAccountV2, DEFAULT_ENTRYPOINT_ADDRESS } from "@biconomy/account";
...
const smartAccount = await BiconomySmartAccountV2.create({
provider: provider, // This can be any ethers JsonRpcProvider connected to your app's network
chainId: ChainId.POLYGON_MUMBAI, // Replace this with your target network
bundler: bundler, // Use the `bundler` we initialized above
paymaster: paymaster, // Use the `paymaster` we initialized above
entryPointAddress: DEFAULT_ENTRYPOINT_ADDRESS, // This is a Biconomy constant
defaultValidationModule: validationModule, // Use the `validationModule` we initialized above
activeValidationModule: validationModule // Use the `validationModule` we initialized above
});
Want to see this code end-to-end?
You can find the code snippets above pasted in an end-to-end example below.tsx
import { useWallets } from '@privy-io/react-auth';
import { IBundler, Bundler } from '@biconomy/bundler';
import { IPaymaster, BiconomyPaymaster } from '@biconomy/paymaster';
import { ChainId } from "@biconomy/core-types";
import { ECDSAOwnershipValidationModule, DEFAULT_ECDSA_OWNERSHIP_MODULE } from "@biconomy/modules";
import { BiconomySmartAccountV2, DEFAULT_ENTRYPOINT_ADDRESS } from "@biconomy/account";
...
// Find the embedded wallet and switch it to your target network
const {wallets} = useWallets();
const embeddedWallet = wallets.find((wallet) => (wallet.walletClientType === 'privy'));
await embeddedWallet.switchChain(80001);
// Initialize your bundler and paymaster
const bundler: IBundler = new Bundler({
bundlerUrl: 'your-bundler-url-from-the-biconomy-dashboard',
chainId: ChainId.POLYGON_MUMBAI, // Replace this with your desired network
entryPointAddress: DEFAULT_ENTRYPOINT_ADDRESS, // This is a Biconomy constant
});
const paymaster: IPaymaster = new BiconomyPaymaster({
paymasterUrl: 'your-paymaster-url-from-the-biconomy-dashboard',
});
// Initialize your validation module
const provider = await embeddedWallet.getEthersProvider();
const signer = provider.getSigner();
const validationModule = await ECDSAOwnershipValidationModule.create({
signer: signer,
moduleAddress: DEFAULT_ECDSA_OWNERSHIP_MODULE // This is a Biconomy constant
});
// Initialize your smart account
const smartAccount = await BiconomySmartAccountV2.create({
provider: provider,
chainId: ChainId.POLYGON_MUMBAI,
bundler: bundler,
paymaster: paymaster,
entryPointAddress: DEFAULT_ENTRYPOINT_ADDRESS,
defaultValidationModule: validationModule,
activeValidationModule: validationModule
});
Note: if your app uses React, we suggest that you store the user's Biconomy smartAccount
in a React context that wraps your application. This allows you to easily access the smart account from your app's pages and components. You can see an example of this in our example app.
5. Send UserOperations from the smart account
Now that your users have Biconomy smart accounts, they can now send UserOperations from their smart account. This is the AA analog to sending a transaction.
To send a user operation from a user's smart account, use Biconomy's sendUserOp
method. An example of sending a user operation to mint an NFT on Polygon Mumbai, gaslessly, is below:
tsx
import { PaymasterMode, type IHybridPaymaster, type SponsorUserOperationDto } from "@biconomy/paymaster";
...
// Initialize an ethers JsonRpcProvider for your network
const provider = new ethers.providers.JsonRpcProvider(`insert-rpc-url-for-your-network`);
// Initialize an ethers contract instance for your NFT
const nft = new ethers.Contract('insert-your-NFT-address', insertYourNftAbi, provider);
// Construct a UserOperation for the minting transaction
const mintTransaction = await nft.populateTransaction.mint!('insert-the-smart-account-address');
// `smartAccount` is the Biconomy smart account we initialized above
const mintUserOp = await smartAccount.buildUserOp([{
to: 'insert-your-NFT-address',
data: mintTransaction.data
}]);
// Configure your paymaster for this UserOperation
const biconomyPaymaster = smartAccount.paymaster as IHybridPaymaster<SponsorUserOperationDto>;
const paymasterServiceData: SponsorUserOperationDto = {
mode: PaymasterMode.SPONSORED,
smartAccountInfo: {
name: 'BICONOMY',
version: '2.0.0'
},
};
const paymasterAndDataResponse = await biconomyPaymaster.getPaymasterAndData(
mintUserOp,
paymasterServiceData
);
mintUserOp.paymasterAndData = paymasterAndDataResponse.paymasterAndData;
// Send UserOperation to mempool, to mint NFT gaslessly
const userOpResponse = await smartAccount.sendUserOp(mintUserOp);
const { receipt } = await userOpResponse.wait(1);
// Get the transaction hash of the mint
console.log(receipt.transactionHash);
That's it! You've configured your app to create smart wallets for all of your users, and can seamlessly add in AA features like gas sponsorship and more. 🎉