Skip to content

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 prefund value.

You can use the helper function below to get the required approval amount.

Steps

Define imports and create the clients

import { getRequiredPrefund } from "permissionless"
import { toSimpleSmartAccount } from "permissionless/accounts"
import { createPimlicoClient } from "permissionless/clients/pimlico"
import { createPublicClient, getAddress, getContract, type Hex, http, parseAbi } from "viem"
import {
	createBundlerClient,
	entryPoint07Address,
	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 the paymaster's getCostInToken.

const quotes = await pimlicoClient.getTokenQuotes({
	tokens: [usdc],
})
const { postOpGas, exchangeRate, paymaster } = quotes[0]
 
const userOperation = await bundlerClient.prepareUserOperation({
	calls: [
		{
			to: getAddress("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"),
			data: "0x1234" as Hex,
		},
	],
})
 
const paymasterContract = getContract({
	address: paymaster,
	abi: parseAbi([
		"function getCostInToken(uint256 actualGasCost, uint256 postOpGas, uint256 actualUserOpFeePerGas, uint256 exchangeRate) public pure returns (uint256)",
	]),
	client: publicClient,
})
 
const maxCostInToken = await paymasterContract.read.getCostInToken([
	getRequiredPrefund({ userOperation, entryPointVersion: "0.7" }),
	postOpGas,
	userOperation.maxFeePerGas,
	exchangeRate,
])

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}`)