How to use the ERC-20 Paymaster
When using Pimlico's ERC-20 Paymaster, the paymaster needs approval to spend funds on the payer's behalf. The amount to approve must be atleast equal to the userOperation's maxCost.
You can use the helper function below to get the required approval amount.
Steps
Define imports and create the clients
import { toSimpleSmartAccount } from "permissionless/accounts"
import { createPimlicoClient } from "permissionless/clients/pimlico"
import { createPublicClient, getAddress, type Hex, http, parseAbi } from "viem"
import {
createBundlerClient,
entryPoint07Address,
UserOperation,
type EntryPointVersion,
} from "viem/account-abstraction"
import { privateKeyToAccount } from "viem/accounts"
import { baseSepolia } from "viem/chains"
const pimlicoUrl = `https://api.pimlico.io/v2/${baseSepolia.id}/rpc?apikey=${process.env.PIMLICO_API_KEY}`
const publicClient = createPublicClient({
chain: baseSepolia,
transport: http("https://sepolia.base.org"),
})
const pimlicoClient = createPimlicoClient({
chain: baseSepolia,
transport: http(pimlicoUrl),
entryPoint: {
address: entryPoint07Address,
version: "0.7" as EntryPointVersion,
},
})
const bundlerClient = createBundlerClient({
account: await toSimpleSmartAccount({
client: publicClient,
owner: privateKeyToAccount(process.env.PRIVATE_KEY as Hex),
}),
chain: baseSepolia,
transport: http(pimlicoUrl),
paymaster: pimlicoClient,
userOperation: {
estimateFeesPerGas: async () => {
return (await pimlicoClient.getUserOperationGasPrice()).fast
},
},
})
Fetch your users' token holdings
For this example, we will assume that our user only holds USDC on Base Sepolia.
const usdc = "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
const smartAccountAddress = bundlerClient.account.address
const senderUsdcBalance = await publicClient.readContract({
abi: parseAbi(["function balanceOf(address account) returns (uint256)"]),
address: usdc,
functionName: "balanceOf",
args: [smartAccountAddress],
})
if (senderUsdcBalance < 1_000_000n) {
throw new Error("insufficient USDC balance, required at least 1 USDC.")
}
Call pimlico_getTokenQuotes
and calculate the approval amount needed for your userOperation
After calling pimlico_getTokenQuotes
, use the returned values to calculate the approval amount for a specific userOperation by calling using formula from the paymaster's getCostInToken
function.
const quotes = await pimlicoClient.getTokenQuotes({
tokens: [usdc],
})
const { postOpGas, exchangeRate, paymaster } = quotes[0]
const userOperation: UserOperation<"0.7"> = await bundlerClient.prepareUserOperation({
calls: [
{
to: getAddress("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"),
data: "0x1234" as Hex,
},
],
})
const userOperationMaxGas =
userOperation.preVerificationGas +
userOperation.callGasLimit +
userOperation.verificationGasLimit +
(userOperation.paymasterPostOpGasLimit || 0n) +
(userOperation.paymasterVerificationGasLimit || 0n)
// using formula here https://github.com/pimlicolabs/singleton-paymaster/blob/main/src/base/BaseSingletonPaymaster.sol#L334-L341
const maxCostInToken =
((userOperationMaxGas + postOpGas * userOperation.maxFeePerGas) * exchangeRate) / BigInt(1e18)
Reconstruct and send the userOperation
After finding the amount that we need to approve, we reconstruct the userOperation to include the token approval before sending it.
Ensure that the paymaster context includes a token
field to inform the Pimlico endpoint to sponsor the userOperation using ERC-20 mode.
const hash = await bundlerClient.sendUserOperation({
paymasterContext: {
token: usdc,
},
calls: [
{
abi: parseAbi(["function approve(address,uint)"]),
functionName: "approve",
args: [paymaster, maxCostInToken],
to: usdc,
},
{
to: getAddress("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"),
data: "0x1234" as Hex,
},
],
})
const opReceipt = await bundlerClient.waitForUserOperationReceipt({
hash,
})
console.log(`transactionHash: ${opReceipt.receipt.transactionHash}`)