Skip to content

Tutorial 2 — Submit a user operation with an ERC-20 Paymaster

In this tutorial, you will deploy an ERC-4337 smart contract wallet on Base Sepolia, and submit a user operation that pays for its gas fees with USDC using an ERC-20 Paymaster.

Steps

Get a Pimlico API key

To get started, please go to our dashboard and generate a Pimlico API key.

Clone the Pimlico tutorial template repository

We have created a Pimlico tutorial template repository that you can use to get started. It comes set up with Typescript, viem, and permissionless.js.

git clone https://github.com/pimlicolabs/tutorial-template.git pimlico-tutorial-2
cd pimlico-tutorial-2

Now, let's install the dependencies:

npm install

The main file we will be working with is index.ts. Let's run it to make sure everything is working:

npm start

If everything has been set up correctly, you should see Hello world! printed to the console.

Create the public and bundler clients, and generate a private key

The public client will be responsible for querying the blockchain, while the bundler client will be responsible for submitting user operations for relaying.

Make sure to replace YOUR_PIMLICO_API_KEY in the code below with your actual Pimlico API key.

Let's open up index.ts, and add the following to the bottom:

const usdc = "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
const paymaster = "0x0000000000000039cd5e8ae05257ce51c473ddd1"
 
const privateKey =
	(process.env.PRIVATE_KEY as Hex) ??
	(() => {
		const pk = generatePrivateKey()
		writeFileSync(".env", `PRIVATE_KEY=${pk}`)
		return pk
	})()
 
const publicClient = createPublicClient({
	chain: baseSepolia,
	transport: http("https://sepolia.base.org"),
})
 
const apiKey = process.env.PIMLICO_API_KEY
const pimlicoUrl = `https://api.pimlico.io/v2/${baseSepolia.id}/rpc?apikey=${apiKey}`
 
const pimlicoClient = createPimlicoClient({
	chain: baseSepolia,
	transport: http(pimlicoUrl),
	entryPoint: {
		address: entryPoint07Address,
		version: "0.7" as EntryPointVersion,
	},
})

Create the SmartAccount instance

For the purposes of this guide, we will be using Safe accounts. This account is an ERC-4337 wallet controlled by a single EOA signer.

To create the Safe account, we will use the toSafeSmartAccount utility function from permissionless.js.

Add the following to the bottom of index.ts:

const account = await toSafeSmartAccount({
	client: publicClient,
	owners: [privateKeyToAccount(privateKey)],
	version: "1.4.1",
})
 
const smartAccountClient = createSmartAccountClient({
	account,
	chain: baseSepolia,
	bundlerTransport: http(pimlicoUrl),
	paymaster: pimlicoClient,
	userOperation: {
		estimateFeesPerGas: async () => {
			return (await pimlicoClient.getUserOperationGasPrice()).fast
		},
	},
})
 
console.log(`Smart account address: https://sepolia.basescan.io/address/${account.address}`)

Since we will be looking to fund our account with USDC (which is what we will use to pay gas fees with), we need to know the address where our smart wallet will be deployed.

Let's run this code with npm start. You should see something like this:

Smart account address: https://sepolia.basescan.org/address/0xbAd38BdCf884ED92ab370f69C0CD0B7b8a1459A1

Get Testnet USDC on Base Sepolia

Let's get some USDC on the Base Sepolia testnet to the counterfactual address of the wallet we will be deploying. This will be used to pay for the gas fees of the user operation we will be submitting.

The recommended way to do this is to use the USDC faucet, select 'Base Sepolia' and enter the counterfactual sender address you generated in the previous step.

Verify you have USDC on the counterfactual sender address

To make sure you have USDC on the counterfactual sender address, let's add a check to the bottom of index.ts:

const senderUsdcBalance = await publicClient.readContract({
	abi: parseAbi(["function balanceOf(address account) returns (uint256)"]),
	address: usdc,
	functionName: "balanceOf",
	args: [account.address],
})
 
if (senderUsdcBalance < 1_000_000n) {
	throw new Error(
		`insufficient USDC balance for counterfactual wallet address ${account.address}: ${
			Number(senderUsdcBalance) / 1_000_000
		} USDC, required at least 1 USDC. Load up balance at https://faucet.circle.com/`,
	)
}
 
console.log("Smart account USDC balance: ", Number(senderUsdcBalance) / 1_000_000)

If you run this code with npm start, you should not see any errors, and you should see the USDC balance of the counterfactual sender address printed to the console.

Smart account USDC balance: 10 USDC

Send a transaction from the smart account, paying only with USDC for gas fees.

Finally, let's submit a transaction from the smart account. We will send a transaction to the 0xd8da6bf26964af9d7eed9e03e53415d37aa96045 (vitalik.eth) address with 0x1234 as example callData. We will also specify the gas price we want to use, which we fetched from the bundler in the previous step.

Underneath the hood, the SmartAccountClient will build a user operation with the designated ERC-20 Paymaster, sign it with the smart account's private key, and then submit it to the bundler. The bundler will then query for receipts until it sees the user operation included on-chain.

The paymasterContext will be passed to the paymaster endpoint, instructing it to sponsor the userOperation using ERC-20 tokens.

Add the following to the bottom of index.ts:

const txHash = await smartAccountClient.sendTransaction({
	calls: [
		{
			to: getAddress(usdc),
			abi: parseAbi(["function approve(address,uint)"]),
			functionName: "approve",
			args: [paymaster, maxUint256],
		},
		{
			to: getAddress("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"),
			data: "0x1234" as Hex,
		},
	],
	paymasterContext: {
		token: usdc,
	},
})
 
console.log(`transactionHash: ${txHash}`)

Let's run this code again with npm start. You should see the transaction hash bundling the user operation on-chain printed to the console.

User operation included: https://sepolia.basescan.org/tx/0xf8e4fc41a134fc9530a0c019167f9dc0981874b90187717605355bdcce8b2fb7

You can now view the transaction on the Base Sepolia testnet explorer. By sending this user operation, you have:

  • Deployed the counterfactual smart account contract
  • Had your smart account approve the ERC-20 Paymaster to spend USDC
  • Had this newly-deployed smart account verify the private key's signature
  • Made an ERC-20 Paymaster sponsor the user operation's gas fees by taking USDC from the smart account
  • Executed a simple transaction to vitalik.eth's address

If you visit the address of the sender account on the Base Sepolia explorer, you should also see that some of your USDC balance has been deducted!

That's it! Congratulations!

Combined code

If you want to see the complete code that combines all of the previous steps, we uploaded it to a separate repository. If you're looking to run it, remember to replace the API key with your own!