Account Abstraction on Base using Particle Network
Particle Network is a Smart Wallet-as-a-Service provider on Base, providing a modular Account Abstraction stack, allowing developers to use a variety of Paymasters, Bundlers, or smart accounts along with social logins.
This document will guide you through the process of using Particle Network within your Base application, building a simple React project using create-react-app
, Particle Auth Core, and Particle's AA SDK.
Objectives
By the end of this guide, you should be able to:
- Use Particle Auth Core to generate an Externally-Owned-Account (EOA) via a social login
- Assign a chosen smart account to the EOA generated by Particle Auth Core
- Set up a Bundler and Paymaster
- Construct and execute a gasless transaction
Prerequisites
Wallet funds
This guide requires you to have ETH on Base Sepolia, which will be used to showcase the execution of a gasless burn transaction.
- To fund your wallet with ETH on Base Sepolia, visit one of the faucets listed on the Base Faucets page.
Familiarity with modern, frontend web development
In this example, you'll be building a React-based application using create-react-app. It's recommended that you have some level of familiarity with the basics of working with React.
Understanding Particle Network
Wallet-as-a-Service
Particle Network provides a large suite of SDKs centered around the reduction of account-based friction.
In this case, "account-based friction" refers to barriers-to-entry that some Web3 users may face as they onboard into an application and begin managing a wallet.
This friction, in the context of this guide, can be placed within two distinct categories:
- The login process. Often, decentralized applications that tend to be more consumer-facing prefer login flows that aren't dependent upon a user downloading and managing a traditional wallet, as this can be a pain point for some.
- The rigidity of standard accounts. Externally Owned Accounts, or EOAs, are often quite rigid in how they operate. They're secured by one key and limited to a strict range of functions, thus developers (and therefore users) are confined to relatively low-level interaction with applications.
Wallet-as-a-Service (WaaS) aims to solve the first of these two points, the login process. WaaS solutions provide an alternative to standard wallets, typically allowing users to use applications through accounts generated by social logins (such as Google, email, or phone). The interfaces for interacting with these accounts are also often embedded within the applications, resulting in a consistent, application-specific experience.
WaaS providers other than Particle Network include Web3Auth, Privy, and Magic, among others.
Account Abstraction
Particle Network also aims to tackle the second friction point described above: account flexibility.
Account Abstraction refers to a transition away from standard account structures, EOAs, to smart accounts. Smart accounts are contracts that act as a wallet, providing users with an account that feels equivalent to an EOA but is intrinsically programmable (due to it being a smart contract) and thus more flexible.
The most popular modern implementation of Account Abstraction is ERC-4337, which enables Account Abstraction without any consensus-layer protocol changes. It does this through numerous components of supporting infrastructure, including a Bundler and Paymaster.
Particle Network describes its Account Abstraction stack as modular, referring to cross-compatibility with any provider of Bundlers, Paymasters, or smart accounts). Particle Network's Account Abstraction SDK runs and uses its own Bundler and Paymaster, with built-in support for Biconomy's Paymaster. However, Particle has made it simple to plug into external infrastructure and components, such as Paymasters or Bundlers from providers like Stackup or Pimlico.
Wallet-as-a-Service + Account Abstraction
Leveraging Account Abstraction directly with Wallet-as-a-Service allows users to onboard through social logins into embedded wallets that use smart accounts, not EOAs, allowing for a greater degree of flexibility.
Particle Network does this by allowing developers to use its Account Abstraction SDK alongside its Wallet-as-a-Service SDK (Particle Auth) to facilitate the intersection between both technologies, as we'll cover in this guide.
To learn more about Account Abstraction and the concepts outlined above, see ERC-4337, or Base’s Introduction to Account Abstraction guide.
Setting up the frontend
This guide will go through the process of creating a React-based application through the create-react-app
template, as shown below.
Creating a React project
To begin, we'll need to initialize a standard create-react-app
project.
Within your IDE of choice, run the following command, replacing {name}
with the name of your project.
npx create-react-app {name}
This will create a React project within a directory under the name you set within {name}
. This directory should contain the following structure by default:
{name}/
├── node_modules/
├── public/
├── src/
├── .gitignore
├── package.json
├── package-lock.json
└── README.md
Throughout this guide, we'll primarily be working with the src
folder and the files within it. src
will contain the following by default:
src/
├── App.css
├── App.js
├── App.test.js
├── index.css
├── index.js
├── logo.svg
└── reportWebVitals.js
This structure won't function properly out of the box as our example will be using JSX, a syntax extension that requires a special file type. Thus, you'll need to change the file extension of your App.js
and index.js
files to either .jsx
(JavaScript) or .tsx
(TypeScript).
We will use these two files, App.tsx
and index.tsx
, within this guide.
Configuring & Initializing Particle Network
Installing the dependencies
Before jumping into the application itself, it's important to walk through a few vital configurations required for Particle Network's SDKs to function properly.
Within this example, we'll be using three core libraries from Particle, these include:
@particle-network/auth-core-modal
, to directly initiate a social login and drive the usage of an embedded wallet.@particle-network/aa
, for configuring, assigning, and deploying a smart account.@particle-network/chain
, to allow Base to be used within this example.
To add these as dependencies within your project, run one of the two following commands at the root of your project.
yarn add @particle-network/auth-core-modal @particle-network/aa @particle-network/chains
# OR
npm install @particle-network/auth-core-modal @particle-network/aa @particle-network/chains
In addition to the above libraries from @particle-network
, we'll be using Ethers for core functions, such as retrieving the user's balance, sending a gasless transaction, and so on.
For the sake of simplicity, we'll be using Ethers v5.7.2, the last release before the Ethers v6 upgrade. Although, if you'd like to use a newer version (v6), comments noting the new syntax of v6 will be left on the code snippets throughout this guide.
Thus, you'll need to install Ethers through either of the following commands:
yarn add [email protected]
# OR
npm install [email protected]
Setting up the Particle Network dashboard
As you'll find in a moment, every library from Particle Network requires three key values for authentication. These are:
- Your
projectId
, assigned to a project created through the Particle dashboard. - Your
clientKey
, similarly assigned to a project created through the dashboard, but serving a different purpose. - Your
appId
, retrieved through the creation of an application (within a project) on the dashboard.
Configuring both @particle-network/auth-core-modal
and @particle-network/aa
will require the retrieval and utilization of these three values.
To create a project and an application through the Particle dashboard:
- Navigate to https://dashboard.particle.network.
- Create a new account (with your email).
- Click "Add New Project" and enter the name of your project.
- Copy and save the "Project ID" and "Client Key."
- Create a new application. For this example, we’ll select "Web."
- Once again, enter the name of your project, alongside the domain where you intend to host this application. If you don't have a domain in mind, feel free to use any filler domain (such as 'base.org'), as it won't affect any underlying functionalities.
- Copy the "App ID" shown after creating an application.
With these values retrieved, it's recommended that you assign them to environment variables within your application, such as REACT_APP_PROJECT_ID
, REACT_APP_CLIENT_KEY
, and REACT_APP_APP_ID
.
Configuring Particle Auth Core
Now that you've installed @particle-network/auth-core-modal
(among other dependencies) and retrieved your project ID, client key, and app ID, you're ready to configure and therefore initialize the Particle Auth Core SDK.
As mentioned, we'll be working out of two files within this guide:
App.jsx/tsx
, containing our core application logic (such as initiating social login and executing the transaction).index.jsx/tsx
, used in this example for configuringAuthCoreContextProvider
from@particle-network/auth-core-modal
, the core configuration object for Particle Auth Core.
AuthCoreContextProvider
is a React component used to define these three aforementioned values, customize the embedded wallet modal and enable account abstraction within it. This will wrap our primary application component (App
from App.jsx/tsx
), therefore allowing Particle Auth Core to be used through various hooks within App
.
To achieve this, AuthCoreContextProvider
will take the following parameters:
projectId
,clientKey
, andappId
. These are the required values previously retrieved from the Particle dashboard.erc4337
, used to define the type of smart account you intend to use, ensuring it's displayed and reflected within the embedded wallet interface.wallet
, for customizing the embedded wallet interface through the restriction of supported chains, color options, etc.
With this in mind, an example of what your index.jsx/tsx
file may look like given the usage of AuthCoreContextProvider
has been included below.
import React from 'react';
import ReactDOM from 'react-dom/client';
import { AuthType } from '@particle-network/auth-core';
import { BaseSepolia } from '@particle-network/chains';
import { AuthCoreContextProvider } from '@particle-network/auth-core-modal';
import App from './App';
// Optional, needed for some environments
import('buffer').then(({ Buffer }) => {
window.Buffer = Buffer;
});
// -----
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<AuthCoreContextProvider
options={{
projectId: process.env.REACT_APP_PROJECT_ID,
clientKey: process.env.REACT_APP_CLIENT_KEY,
appId: process.env.REACT_APP_APP_ID,
erc4337: {
// The name of the smart account you'd like to use
// SIMPLE, BICONOMY, LIGHT, or CYBERCONNECT
name: 'SIMPLE',
// The version of the smart account you're using
// 1.0.0 for everything except Biconomy, which can be either 1.0.0 or 2.0.0
version: '1.0.0',
},
wallet: {
// Set to false to remove the embedded wallet modal
visible: true,
customStyle: {
// Locks the chain selector to Base Sepolia
supportChains: [BaseSepolia],
},
},
}}
>
<App />
</AuthCoreContextProvider>
</React.StrictMode>,
);
Setting Up Your Application
Importing and configuring hooks
The second core component of this application is App.jsx/tsx
, which will contain logic achieving the following:
- Configuration and assignment of a smart account (SimpleAccount in this example).
- Construction of a custom Ethers provider, using a custom AA-enabled EIP-1193 provider object.
- Initiation of social login.
- Execution of a gasless transaction.
To achieve this, we'll be using a combination of hooks (from @particle-network/auth-core-modal
) and base functions on Ethers, which will be automatically powered by AA through the custom EIP-1193 provider object.
These hooks include useEthereum
for the retrieval of the standard EOA-based provider object, useConnect
for managing social logins, and useAuthCore
to retrieve the user’s information (after social login).
To begin building App.jsx/tsx
, you'll need to define the relevant functions from these hooks through a process similar to the example below:
import React, { useState, useEffect } from 'react';
import { useEthereum, useConnect, useAuthCore } from '@particle-network/auth-core-modal';
import { BaseSepolia } from '@particle-network/chains';
import { AAWrapProvider, SendTransactionMode, SmartAccount } from '@particle-network/aa';
import { ethers } from 'ethers';
import './App.css';
const App = () => {
// Standard, EOA-based 1193 provider
const { provider } = useEthereum();
// Used for initiating social login and disconnecting users (post-login)
const { connect, disconnect } = useConnect();
// Automatically loaded with relevant user information after logging in
const { userInfo } = useAuthCore();
};
Configuring the Smart Account
Choosing and defining a smart account
The EOA generated through the social login process will be used as the Signer for the smart account specified within this configuration, this will then be reflected through the embedded wallet modal (through its former selection within AuthCoreContextProvider
).
As mentioned, Particle Network supports a variety of smart accounts directly through its AA SDK, these include:
- Light Account (by Alchemy).
- Biconomy V1 and V2.
- SimpleAccount (eth-infinitism).
- CyberConnect.
We'll be using SimpleAccount in this case. Although, note that you can change which smart account you use at any time through one line of code. This line of code exists on the SmartAccount
object (imported from @particle-network/aa
). SmartAccount
acts as the central point for initializing a smart account.
To configure SmartAccount
, you'll be using the following parameters:
projectId
,clientKey
, andappId
. These were used withinAuthCoreContextProvider
and can be retrieved from the Particle dashboard through the same procedure.aaOptions
, which containsaccountContracts
. WithinaccountContracts
, you'll need to define a property corresponding with the smart account you'd like to use, i.e.BICONOMY
,LIGHT
,CYBERCONNECT
, orSIMPLE
.- This property contains
chainIds
andversion
.
- This property contains
See the snippet below for an example of a constructed SmartAccount
object:
import React, { useState, useEffect } from 'react';
import { useEthereum, useConnect, useAuthCore } from '@particle-network/auth-core-modal';
import { BaseSepolia } from '@particle-network/chains';
import { AAWrapProvider, SendTransactionMode, SmartAccount } from '@particle-network/aa';
import { ethers } from 'ethers';
import './App.css';
const App = () => {
...
const smartAccount = new SmartAccount(provider, {
projectId: process.env.REACT_APP_PROJECT_ID,
clientKey: process.env.REACT_APP_CLIENT_KEY,
appId: process.env.REACT_APP_APP_ID,
aaOptions: {
accountContracts: {
SIMPLE: [{ chainIds: [BaseSepolia.id], version: '1.0.0' }]
// BICONOMY: [{ chainIds: [BaseSepolia.id], version: '1.0.0' }]
// BICONOMY: [{ chainIds: [BaseSepolia.id], version: '2.0.0' }]
// LIGHT: [{ chainIds: [BaseSepolia.id], version: '1.0.0' }]
// CYBERCONNECT: [{ chainIds: [BaseSepolia.id], version: '1.0.0' }]
}
}
});
};
Constructing a custom Ethers object
There are two primary mechanisms to interact with the smart account defined previously. These are:
- Directly through the constructed
SmartAccount
object, such as with{your object}.sendTransaction
. - Through a Web3 library, such as Ethers or Web3.js. These libraries can facilitate interaction through a standardized provider object, allowing for a consistent syntax and setup through any wallet or account structure.
As mentioned, we'll be using Ethers within this guide. You'll be able to use any typical Ethers method, such as sendTransaction
or getBalance
through the AA-enabled provider we construct with AAWrapProvider
(imported from @particle-network/aa
).
Essentially, we'll construct an instance of AAWrapProvider
, passing in the previously defined SmartAccount
object alongside an object representing the method selected to pay gas fees. This will allow Ethers to directly load the smart account and drive transactions/signatures through Particle's embedded wallet.
You can achieve this in one line of code, e.g.:
// For Ethers V6, use ethers.BrowserProvider instead; the syntax below won't work.
const customProvider = new ethers.providers.Web3Provider(
new AAWrapProvider(smartAccount, SendTransactionMode.Gasless),
'any',
);
SendTransactionMode
has three options. They are:
SendTransactionMode.Gasless
, which will request gas sponsorship on every transaction sent through Ethers. By default, this will be through Particle's Omnichain Paymaster. If you don't have enough USDT deposited in the Paymaster to cover the gas fees, or if the transaction fails to meet your sponsorship conditions (set on the Particle dashboard), the user will pay the gas fees themselves.SendTransactionMode.UserPaidNative
, the default method used ifSendTransactionMode
is missing fromAAWrapProvider
. This forces the user to pay gas fees themselves.SendTransactionMode.UserSelect
, which allows a user to select which gas fee payment mechanism they use (ERC-20 token, native token, or request sponsorship).
In this example, we're using SendTransactionMode.Gasless
. Because this example uses Base Sepolia, all transactions will automatically be sponsored.
Initiating Social Login
Calling the connect function
At this point, you've configured Particle Auth Core, initialized a smart account of choice, and constructed a custom Ethers provider to drive on-chain interaction.
As covered earlier, one of the core benefits of Wallet-as-a-Service is the ability to onboard users through their social accounts. In this case, users’ social login will directly create or log them into a smart account (if they have already created one through Particle Auth), allowing for the immediate usage of account abstraction.
To initiate the social login process programmatically, you'll need to use the connect
function, defined from the useConnect
hook imported earlier (from @particle-network/auth-core-modal
).
Upon calling connect
, a user will be brought through the social login process, after which an EOA will be generated (through MPC-TSS) and used as a signer for the smart account.
connect
takes the following parameters:
socialType
, the specific social login mechanism you'd like users to go through. If left as an empty string, a generalized social login modal will be shown. Otherwise, use strings such as 'google', 'twitter', 'email', etc.chain
, an object (imported from@particle-network/chains
) corresponding with the chain you'd like to use. In this example, it'll beBaseSepolia
.
Ideally, some logic should be set in place to ensure connect
isn't called if a user has already logged in. This can be done by only calling connect
on the condition that userInfo
(from useAuthCore
) is undefined, indicating that the user isn't logged in.
Below is an example of calling connect
(within the conditional described above).
const handleLogin = async (authType) => {
if (!userInfo) {
await connect({
socialType: authType,
chain: BaseSepolia,
});
}
};
In most applications, connect
(or handleLogin
in this example), will be bound to a "Login" or "Connect" button, as will be done here.
Executing a Gasless Transaction
Constructing a transaction
Because we're using Ethers in this guide, constructing and executing a gasless transaction (intrinsically gasless through the previously defined SendTransactionMode
) is identical to the flow you're likely already familiar with. However, it's important to note that transactions sent through ERC-4337 account abstraction do not follow standard transaction structures, these are called UserOperations.
Typically, UserOperations follow lower-level, alternative structures. Although, through the usage of AAWrapProvider
, the conversion between a simple transaction object (with to
, value
, data
, etc.) to a complete UserOperation is handled automatically, allowing you to send transactions as you would normally.
Thus, we'll be constructing a simple transaction (tx
) adhering to the following structure:
to
, the recipient address. For this example, we can burn a small amount of ETH on Base Sepolia, which means the recipient will be0x000000000000000000000000000000000000dEaD
.value
, the value being sent in wei. Because of the default denomination in wei, this will be set asethers.utils.parseEther("0.001")
.
If you intend on interacting with a contract, data
can also be filled out (or within Ethers, a Contract
object can be built).
Therefore, your tx
object should look like this:
const executeUserOp = async () => {
...
const tx = {
to: "0x000000000000000000000000000000000000dEaD",
value: ethers.utils.parseEther("0.001"),
};
...
};
Executing a transaction
Now that you've defined a standard transaction object, you'll need to execute it. Once again, due to the usage of Ethers, this is quite straightforward.
We'll be using a signer
object retrieved from {your provider}.getSigner()
to call the sendTransaction
method, which simply takes the tx
object we constructed a moment ago.
Upon calling signer.sendTransaction(tx)
, the user will be prompted to confirm the transaction (sign a UserOperation hash) through an application-embedded popup. After doing so, the transaction will immediately be sent on Base Sepolia.
To reflect the transaction hash after its confirmation on-chain, you can call the wait
method on the variable you saved signer.sendTransaction(tx)
to. The resulting object will contain a transactionHash
value.
See the example below for a visualization of this process:
const executeUserOp = async () => {
const signer = customProvider.getSigner();
const tx = {
to: '0x000000000000000000000000000000000000dEaD',
value: ethers.utils.parseEther('0.001'),
};
const txResponse = await signer.sendTransaction(tx);
const txReceipt = await txResponse.wait();
return txReceipt.transactionHash;
};
Mapping to JSX
You've now initiated social login (through handleLogin
), assigned a smart account (through SmartAccount
), and executed a gasless transaction (through executeUserOp
).
To present all of this to the user and allow them to interact with these functions for themselves, you'll need to map handleLogin
and executeUserOp
to the JSX of your App
component. This will format the frontend that a user interacts with to test this application.
Essentially, this displays either "Sign in with Google" or "Sign in with Twitter" through custom buttons that are only shown if the user hasn't logged in (determined through the state of userInfo
). Upon logging in, the user can either call executeUserOp
or disconnect
(which was defined from useConnect
).
Below is an example of what your App.jsx/tsx
file may look at this point. At the bottom of this snippet you'll find the JSX:
import React, { useState, useEffect } from 'react';
import { useEthereum, useConnect, useAuthCore } from '@particle-network/auth-core-modal';
import { BaseSepolia } from '@particle-network/chains';
import { AAWrapProvider, SmartAccount, SendTransactionMode } from '@particle-network/aa';
import { ethers } from 'ethers';
import './App.css';
const App = () => {
const { provider } = useEthereum();
const { connect, disconnect } = useConnect();
const { userInfo } = useAuthCore();
// Initializes and assigns a smart account to the EOA resulting from social login
const smartAccount = new SmartAccount(provider, {
projectId: process.env.REACT_APP_PROJECT_ID,
clientKey: process.env.REACT_APP_CLIENT_KEY,
appId: process.env.REACT_APP_APP_ID,
aaOptions: {
accountContracts: {
SIMPLE: [{ chainIds: [BaseSepolia.id], version: '1.0.0' }],
},
},
});
// Enables interaction with the smart account through Ethers
const customProvider = new ethers.providers.Web3Provider(
new AAWrapProvider(smartAccount, SendTransactionMode.Gasless),
'any',
);
// Initiates social login according to authType
const handleLogin = async (authType) => {
if (!userInfo) {
await connect({
socialType: authType,
chain: BaseSepolia,
});
}
};
// Executes a gasless burn of 0.001 ETH
const executeUserOp = async () => {
const signer = customProvider.getSigner();
const tx = {
to: '0x000000000000000000000000000000000000dEaD',
value: ethers.utils.parseEther('0.001'),
};
const txResponse = await signer.sendTransaction(tx);
const txReceipt = await txResponse.wait();
return txReceipt.transactionHash;
};
// The JSX
return (
<div className="App">
<div className="logo-section">
<img src="https://i.imgur.com/EerK7MS.png" alt="Logo 1" className="logo logo-big" />
<img src="https://i.imgur.com/1RV3pMV.png" alt="Logo 2" className="logo" />
</div>
{!userInfo ? (
<div className="login-section">
<button className="sign-button" onClick={() => handleLogin('google')}>
Sign in with Google
</button>
<button className="sign-button" onClick={() => handleLogin('twitter')}>
Sign in with Twitter
</button>
</div>
) : (
<div className="profile-card">
<h2>{userInfo.name}</h2>
<div className="button-section">
<button className="sign-message-button" onClick={executeUserOp}>
Execute User Operation
</button>
<button className="disconnect-button" onClick={() => disconnect()}>
Logout
</button>
</div>
</div>
)}
</div>
);
};
export default App;
With everything complete, you're ready to run your application to test it. To do so, go through the following process:
- Navigate to the root of your project.
- Run either
npm run start
oryarn start
. - Once running, log in with either your Google or Twitter.
- Fund the address displayed on the wallet modal in the bottom right.
- Click "Execute User Operation".
Conclusion
Congratulations! You've just built an application from scratch, onboarding users into smart accounts through social logins using Particle Network.
To learn more about using Particle Network on Base, take a look at the following resources:
- Biconomy Guide (which uses Particle Network)
- Account Abstraction on Base
- Particle Network Documentation
- Particle Network 101: Developer Experience