Particle Network Demo
This demo application illustrates how to integrate self-custodial social authentication with Particle Network
View it live
This tutorial will use Particle Network to sign ERC-4337 transactions. It will use both the Particle Network SDK and the Userop.js SDK.
At its core, the integration of a signer like Particle Network happens through the EIP-1193 interface. You can see a summary of using this interface here: Particle Network.
Getting Started
Clone the Repository
git clone https://github.com/TABASCOatw/particle-stackup-demo.git
You can view the repository here: https://github.com/TABASCOatw/particle-stackup-demo
Install Dependencies
npm install
yarn install
Set Environment Variables
This project requires a number of keys from Particle Network, WalletConnect, and Stackup to be defined in .env. The following should be defined:
- REACT_APP_APP_ID, the ID of the corresponding application in your Particle Network dashboard.
- REACT_APP_PROJECT_ID, the ID of the corresponding project in your Particle Network dashboard.
- REACT_APP_CLIENT_KEY, the client key of the corresponding project in your Particle Network dashboard.
- REACT_APP_STACKUP_PAYMASTER, the Stackup paymaster URL retrieved from the Stackup dashboard.
- REACT_APP_RPC_URL, an Ethereum Goerli RPC URL.
Run the Project
npm run dev
yarn dev
How does it work?
Particle Network's Wallet-as-a-Service has native ERC-4337 support, so you can use it directly to manage authentication of ERC-4337 accounts.
You can view the full code on GitHub.
import React, { useState, useEffect } from 'react';
import { ParticleNetwork } from '@particle-network/auth';
import { SmartAccount } from '@particle-network/aa';
import { ParticleProvider } from '@particle-network/provider';
import { ethers } from 'ethers';
import { Presets, Client } from 'userop';
import { notification } from 'antd';
import './App.css';
const config = {
projectId: process.env.REACT_APP_PROJECT_ID,
clientKey: process.env.REACT_APP_CLIENT_KEY,
appId: process.env.REACT_APP_APP_ID,
};
const particle = new ParticleNetwork({
...config,
chainName: 'ethereum',
chainId: 5,
wallet: { displayWalletEntry: true },
});
const provider = new ethers.providers.Web3Provider(new ParticleProvider(particle.auth));
const smartAccount = new SmartAccount(new ParticleProvider(particle.auth), {
...config,
aaOptions: {
simple: [{ chainId: 5, version: '1.0.0' }],
},
});
particle.setERC4337({
name: 'SIMPLE',
version: '1.0.0'
})
const initializePaymaster = () => {
const paymasterContext = { type: 'payg' };
return Presets.Middleware.verifyingPaymaster(
process.env.REACT_APP_STACKUP_PAYMASTER,
paymasterContext
);
};
const App = () => {
const [userInfo, setUserInfo] = useState(null);
const [ethBalance, setEthBalance] = useState(null);
const [isDeployed, setIsDeployed] = useState(null);
useEffect(() => {
if (userInfo) fetchAccountInfo();
}, [userInfo]);
const fetchAccountInfo = async () => {
const address = await smartAccount.getAddress();
const balance = ethers.utils.formatEther(await provider.getBalance(address));
const isDeployed = await smartAccount.isDeployed();
setEthBalance(balance);
setIsDeployed(isDeployed);
};
const deployAccount = async () => {
if (!isDeployed) {
await smartAccount.deployWalletContract();
}
};
const handleLogin = async (preferredAuthType) => {
const user = await particle.auth.login({ preferredAuthType });
setUserInfo(user);
};
const executeUserOp = async () => {
const paymaster = initializePaymaster();
const signer = provider.getSigner();
const builder = await Presets.Builder.SimpleAccount.init(signer, process.env.REACT_APP_RPC_URL, { paymasterMiddleware: paymaster });
const client = await Client.init(process.env.REACT_APP_RPC_URL);
const address = builder.getSender();
console.log(`Account address: ${address}`);
const res = await client.sendUserOperation(builder.execute("0x000000000000000000000000000000000000dEaD", ethers.utils.parseUnits('0.001', 'ether'), "0x"), {
onBuild: (op) => console.log("Signed UserOperation:", op),
});
notification.success({
message: "User operation successful",
description: `userOpHash: ${res.userOpHash}`
});
};
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/9gGvvtO.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="balance-section">
<small>{ethBalance} ETH</small>
{isDeployed ? (
<button className="sign-message-button" onClick={executeUserOp}>Execute User Operation</button>
) : (
<button className="sign-message-button" onClick={deployAccount}>Deploy Account</button>
)}
</div>
</div>
)}
</div>
);
};
export default App;
Updated 10 months ago