How to create and use a Safe account with permissionless.js
Safe is the most battle-tested Ethereum smart account provider. With their recent release of their ERC-4337 module, it is now possible to plug in Safe accounts to ERC-4337 bundlers and paymasters. This guide will walk you through how to create and use a Safe account with permissionless.js.
Steps
Import the required packages
import { ENTRYPOINT_ADDRESS_V07, createSmartAccountClient } from "permissionless"
import { signerToSafeSmartAccount } from "permissionless/accounts"
import {
createPimlicoBundlerClient,
createPimlicoPaymasterClient,
} from "permissionless/clients/pimlico"
import { createPublicClient, getContract, http, parseEther } from "viem"
import { sepolia } from "viem/chains"
Create the clients
First we must create the public, bundler, and (optionally) paymaster clients that will be used to interact with the Safe account.
export const publicClient = createPublicClient({
transport: http("https://rpc.ankr.com/eth_sepolia"),
})
export const paymasterClient = createPimlicoPaymasterClient({
transport: http("https://api.pimlico.io/v2/sepolia/rpc?apikey=API_KEY"),
entryPoint: ENTRYPOINT_ADDRESS_V07,
})
export const pimlicoBundlerClient = createPimlicoBundlerClient({
transport: http("https://api.pimlico.io/v2/sepolia/rpc?apikey=API_KEY"),
entryPoint: ENTRYPOINT_ADDRESS_V07,
})
Create the signer
The Safe account will need to have a signer to sign user operations. In permissionless.js, the default Safe account validates ECDSA signatures. Any permissionless.js-compatible signer can be used for the Safe account.
For example, to create a signer based on a private key:
import { privateKeyToAccount } from "viem/accounts"
const signer = privateKeyToAccount("0xPRIVATE_KEY")
Create the Safe account
With a signer, you can create a Safe account as such:
const safeAccount = await signerToSafeSmartAccount(publicClient, {
entryPoint: ENTRYPOINT_ADDRESS_V07,
signer: signer,
saltNonce: 0n, // optional
safeVersion: "1.4.1",
address: "0x...", // optional, only if you are using an already created account
})
Create the smart account client
The smart account client is a permissionless.js client that is meant to serve as an almost drop-in replacement for viem's walletClient.
const smartAccountClient = createSmartAccountClient({
account: safeAccount,
entryPoint: ENTRYPOINT_ADDRESS_V07,
chain: sepolia,
bundlerTransport: http("https://api.pimlico.io/v2/sepolia/rpc?apikey=API_KEY"),
middleware: {
sponsorUserOperation: paymasterClient.sponsorUserOperation, // optional
gasPrice: async () => (await pimlicoBundlerClient.getUserOperationGasPrice()).fast, // if using pimlico bundler
},
})
Send a transaction
Transactions using permissionless.js simply wrap around user operations. This means you can switch to permissionless.js from your existing viem EOA codebase with minimal-to-no changes.
const txHash = await smartAccountClient.sendTransaction({
to: "0xd8da6bf26964af9d7eed9e03e53415d37aa96045",
value: parseEther("0.1"),
})
This also means you can also use viem Contract instances to transact without any modifications.
const nftContract = getContract({
address: "0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2",
abi: nftAbi,
client: {
public: publicClient,
wallet: smartAccountClient,
},
})
const txHash = await nftContract.write.mint()
You can also send an array of transactions in a single batch.
const txHash = await smartAccountClient.sendTransactions({
transactions: [
{
to: "0xd8da6bf26964af9d7eed9e03e53415d37aa96045",
value: parseEther("0.1"),
data: "0x",
},
{
to: "0x1440ec793aE50fA046B95bFeCa5aF475b6003f9e",
value: parseEther("0.1"),
data: "0x1234",
},
],
})
Understanding the errors
If you're getting an error that starts with GS
, it probably means that something went off with the Safe account. Checkout the Safe error codes here.