permissionless.js Guide
permissionless.js is a TypeScript library built on top of viem for building with ERC-4337 smart accounts. permissionless.js supports Safe, Kernel, Biconomy, SimpleAccount, TrustWallet and LightAccount and more.
You can see the full implementation used in this guide here.
Prerequisites
Setting Up the Environment
First, install the necessary packages by running:
npm install permissionless viem wagmi axios circomlibjs
Import the required modules in your script:
import { createSmartAccountClient } from "permissionless";
import { entryPoint07Address } from "viem/account-abstraction";
import { toSafeSmartAccount } from "permissionless/accounts";
import { erc7579Actions } from "permissionless/actions/erc7579";
import {
createPublicClient,
encodeAbiParameters,
encodeFunctionData,
http,
keccak256,
parseAbiParameters,
bytesToHex,
toHex,
} from "viem";
import { baseSepolia } from "viem/chains";
import { readContract } from "wagmi/actions";
import axios from "axios";
import { buildPoseidon } from "circomlibjs";
Configuring the Clients
Set up your API keys and URLs:
const owner = "YOUR_OWNER_ADDRESS";
const apiKey = "YOUR_PIMLICO_API_KEY";
const rpcApiKey = "YOUR_ALCHEMY_RPC_API_KEY";
const bundlerUrl = `https://api.pimlico.io/v2/basesepolia/rpc?apikey=${apiKey}`;
const rpcUrl = `https://base-sepolia.g.alchemy.com/v2/${rpcApiKey}`;
Create the public client and the Pimlico client:
const publicClient = createPublicClient({
transport: http(rpcUrl),
});
const pimlicoClient = createSmartAccountClient({
transport: http(bundlerUrl),
entryPoint: {
address: entryPoint07Address,
version: "0.7",
},
});
Setting Up the Safe Account
Initialize your Safe account with the necessary parameters:
const safeAccount = await toSafeSmartAccount({
client: publicClient,
owners: [owner],
version: "1.4.1",
entryPoint: {
address: entryPoint07Address,
version: "0.7",
},
safe4337ModuleAddress: "0x7579EE8307284F293B1927136486880611F20002",
erc7579LaunchpadAddress: "0x7579011aB74c46090561ea277Ba79D510c6C00ff",
attesters: ["0x000000333034E9f539ce08819E12c1b8Cb29084d"], // Rhinestone's attester address
attestersThreshold: 1,
});
const safeWalletAddress = "YOUR_SAFE_WALLET_ADDRESS";
Create the smart account client and extend it with ERC-7579 actions:
const smartAccountClient = createSmartAccountClient({
account: safeAccount,
chain: baseSepolia,
bundlerTransport: http(bundlerUrl),
paymaster: pimlicoClient,
userOperation: {
estimateFeesPerGas: async () => {
return (await pimlicoClient.getUserOperationGasPrice()).fast;
},
},
}).extend(erc7579Actions());
Installing the Module and Configuring Recovery
Set up the Universal Email Recovery Module:
const universalEmailRecoveryModuleAddress = "0x636632FA22052d2a4Fb6e3Bab84551B620b9C1F9";
const guardianEmail = "guardian@gmail.com";
Generate a random account code using Poseidon:
const poseidon = await buildPoseidon();
const accountCodeBytes: Uint8Array = poseidon.F.random();
const accountCode = bytesToHex(accountCodeBytes.reverse());
Fetch the guardian salt by sending a POST request:
const { guardianSalt } = await axios.post(`${relayerApiUrl}/getAccountSalt`, {
account_code: accountCode.slice(2),
email_addr: guardianEmail,
});
Compute the guardian address:
const guardianAddr = await readContract({
abi: universalEmailRecoveryModuleAbi,
address: universalEmailRecoveryModuleAddress,
functionName: "computeEmailAuthAddress",
args: [safeWalletAddress, guardianSalt],
});
Prepare the module data for installation:
const account = safeWalletAddress;
const isInstalledContext = toHex(0);
const functionSelector = toFunctionSelector(
"swapOwner(address,address,address)"
);
const guardians = [guardianAddr];
const guardianWeights = [1n];
const threshold = 1n;
const delay = 6n * 60n * 60n; // 6 hours
const expiry = 2n * 7n * 24n * 60n * 60n; // 2 weeks in seconds
const moduleData = encodeAbiParameters(
parseAbiParameters(
"address, bytes, bytes4, address[], uint256[], uint256, uint256, uint256"
),
[
account,
isInstalledContext,
functionSelector,
guardians,
guardianWeights,
threshold,
delay,
expiry,
]
);
Install the module:
const userOpHash = await smartAccountClient.installModule({
type: "executor",
address: universalEmailRecoveryModuleAddress,
context: moduleData,
});
const receipt = await pimlicoClient.waitForUserOperationReceipt({
hash: userOpHash,
});
Handling Acceptance
Fetch the acceptance command template:
const subject = await readContract({
abi: universalEmailRecoveryModuleAbi,
address: universalEmailRecoveryModuleAddress,
functionName: "acceptanceCommandTemplates",
args: [],
});
const templateIdx = 0;
const handleAcceptanceCommand = subject[0]
.join(" ")
.replace("{ethAddr}", safeWalletAddress);
Send the acceptance request:
const { data: handleAcceptanceData } = await axios.post(
`${relayerApiUrl}/acceptanceRequest`,
{
controller_eth_addr: universalEmailRecoveryModuleAddress,
guardian_email_addr: guardianEmail,
account_code: accountCode,
template_idx: templateIdx,
command: handleAcceptanceCommand,
}
);
const { request_id: requestId } = handleAcceptanceData;
Handling Recovery
Fetch the recovery command template:
const processRecoveryCommand = await readContract({
abi: universalEmailRecoveryModuleAbi,
address: universalEmailRecoveryModuleAddress,
functionName: "recoveryCommandTemplates",
args: [],
});
Send the recovery request:
See the following example for how to construct the recovery command.
const { data: processRecoveryData } = await axios.post(
`${relayerApiUrl}/recoveryRequest`,
{
controller_eth_addr: universalEmailRecoveryModuleAddress,
guardian_email_addr: guardianEmail,
template_idx: templateIdx,
command: processRecoveryCommand,
}
);
const { request_id: processRecoveryDataRequestId } = processRecoveryData;
Completing the Recovery
See the following example for how to retrieve the previousOwnerInLinkedList
, oldOwner
and newOwner
.
Set up the parameters for owner swapping:
const previousOwnerInLinkedList = "PREVIOUS_OWNER_IN_LINKED_LIST";
const oldOwner = "OLD_OWNER_ADDRESS";
const newOwner = "NEW_OWNER_ADDRESS";
const recoveryCallData = encodeFunctionData({
abi: safeAbi,
functionName: "swapOwner",
args: [previousOwnerInLinkedList, oldOwner, newOwner],
});
const recoveryData = encodeAbiParameters(
parseAbiParameters("address, bytes"),
[safeWalletAddress, recoveryCallData]
);
Complete the recovery process:
const { data } = await axios.post(`${relayerApiUrl}/completeRequest`, {
controller_eth_addr: universalEmailRecoveryModuleAddress,
account_eth_addr: safeWalletAddress,
complete_calldata: recoveryData,
});