Skip to content

Tutorial 2 — Submit a user operation with a Verifying Paymaster

In this tutorial, you will generate a user operation, ask Pimlico's verifying paymaster to sponsor it, and then submit the sponsored user operation on-chain with Pimlico's Alto bundler.

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 viem clients

We will be using three different clients for this example.

  1. Standard publicClient for normal Ethereum RPC calls — https://rpc.sepolia.org
  2. Pimlico v1 api for the Bundler methods — https://api.pimlico.io/v2/sepolia/rpc?apikey=YOUR_PIMLICO_API_KEY
  3. Pimlico v2 api for the Paymaster methods — https://api.pimlico.io/v2/sepolia/rpc?apikey=YOUR_PIMLICO_API_KEY

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:

export const publicClient = createPublicClient({
	transport: http("https://rpc.ankr.com/eth_sepolia"),
	chain: sepolia,
})
 
const apiKey = "YOUR_PIMLICO_API_KEY" // REPLACE THIS
const endpointUrl = `https://api.pimlico.io/v2/sepolia/rpc?apikey=${apiKey}`
 
const bundlerClient = createClient({
	transport: http(endpointUrl),
	chain: sepolia,
})
	.extend(bundlerActions(ENTRYPOINT_ADDRESS_V07))
	.extend(pimlicoBundlerActions(ENTRYPOINT_ADDRESS_V07))
 
const paymasterClient = createClient({
	transport: http(endpointUrl),
	chain: sepolia,
}).extend(pimlicoPaymasterActions(ENTRYPOINT_ADDRESS_V07))

Generate the factory and factoryData

For the purposes of this guide, we will be using the SimpleAccount.sol wallet found in the eth-infinitism repository. This Wallet is a simple ERC-4337 wallet controlled by a single EOA signer.

At 0x91E60e0613810449d098b0b5Ec8b51A0FE8c8985, most chain already have deployed a SimpleAccountFactory.sol contract, that is able to easily deploy new SimpleAccount instances via the createAccount function. We will be leveraging this contract to help us generate the factory and factoryData.

Requesting a smart contract deployment is done through the factory and factoryData field, where the factory field specifies the address the EntryPoint will call, and the factoryData corresponds to the data that will be called on that factory.

Add the following to the bottom of index.ts:

const SIMPLE_ACCOUNT_FACTORY_ADDRESS = "0x91E60e0613810449d098b0b5Ec8b51A0FE8c8985"
 
const ownerPrivateKey = generatePrivateKey()
const owner = privateKeyToAccount(ownerPrivateKey)
 
console.log("Generated wallet with private key:", ownerPrivateKey)
 
const factory = SIMPLE_ACCOUNT_FACTORY_ADDRESS
const factoryData = encodeFunctionData({
	abi: [
		{
			inputs: [
				{ name: "owner", type: "address" },
				{ name: "salt", type: "uint256" },
			],
			name: "createAccount",
			outputs: [{ name: "ret", type: "address" }],
			stateMutability: "nonpayable",
			type: "function",
		},
	],
	args: [owner.address, 0n],
})
 
console.log("Generated factoryData:", factoryData)

Let's run this code with npm start. You should see the generated initCode printed to the console.

Generated factoryData: 0x9406cc6185a346906296840746125a0e449764545fbfb9cf000000000000000000000000508a7005f997a21cd662d6fb41ad69f945c129c10000000000000000000000000000000000000000000000000000000000000000

Calculate the sender address

Now that we have the factory and factoryData, we have to calculate the corresponding sender address, which is the address the SimpleAccount will be deployed to, and thereby the address which will handle the verification and execution steps of the UserOperation.

We do this by calling the getSenderAddress utility function on the EntryPoint. Upon success, it will revert with a special error type that contains the counterfactual address of the smart contract wallet that will be deployed

Add the following to the bottom of index.ts:

const senderAddress = await getSenderAddress(publicClient, {
	factory,
	factoryData,
	entryPoint: ENTRYPOINT_ADDRESS_V07,
})
console.log("Calculated sender address:", senderAddress)

Let's run this code with npm start. You should see the address printed to the console.

Calculated sender address: 0xbAd38BdCf884ED92ab370f69C0CD0B7b8a1459A1

Generate the callData

Now, let's decide on the callData that we want the wallet to actually execute once the UserOperation passes verification.

Add the following to the bottom of index.ts:

const to = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" // vitalik
const value = 0n
const data = "0x68656c6c6f" // "hello" encoded to utf-8 bytes
 
const callData = encodeFunctionData({
	abi: [
		{
			inputs: [
				{ name: "dest", type: "address" },
				{ name: "value", type: "uint256" },
				{ name: "func", type: "bytes" },
			],
			name: "execute",
			outputs: [],
			stateMutability: "nonpayable",
			type: "function",
		},
	],
	args: [to, value, data],
})
 
console.log("Generated callData:", callData)

Above, we are leveraging the execute function of the SimpleWallet, which simply calls an arbitrary address, with arbitrary value and arbitrary callData.

Let's run this code with npm start. You should see the callData printed to the console.

Generated callData: 0xb61d27f6000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa9604500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000568656c6c6f000000000000000000000000000000000000000000000000000000

Fill out remaining UserOperation values

We're almost there, now let's fill out the rest of the UserOperation values.

Add the following to the bottom of index.ts:

const gasPrice = await bundlerClient.getUserOperationGasPrice()
 
const userOperation = {
	sender: senderAddress,
	nonce: 0n,
	factory: factory as Address,
	factoryData,
	callData,
	maxFeePerGas: gasPrice.fast.maxFeePerGas,
	maxPriorityFeePerGas: gasPrice.fast.maxPriorityFeePerGas,
	// dummy signature, needs to be there so the SimpleAccount doesn't immediately revert because of invalid signature length
	signature:
		"0xa15569dd8f8324dbeabf8073fdec36d4b754f53ce5901e283c6de79af177dc94557fa3c9922cd7af2a96ca94402d35c39f266925ee6407aeb32b31d76978d4ba1c" as Hex,
}

Request Pimlico verifying paymaster sponsorship

To make the operation gasless, we will leverage Pimlico's verifying paymaster. Using paymasters allows you to delegate the gas fee payment to a third-party contract that can decide whether it is willing to pay the gas fees for the user operation. In this case, Pimlico's verifying paymaster checks whether its off-chain signer has signed off on the user operation. To request Pimlico's signer to sign your user operation, call the pm_sponsorUserOperation endpoint or use the sponsorUserOperation method from the permissionless.js Pimlico paymaster actions.

Add the following to the bottom of index.ts:

const sponsorUserOperationResult = await paymasterClient.sponsorUserOperation({
	userOperation,
})
 
const sponsoredUserOperation: UserOperation<"v0.7"> = {
	...userOperation,
	...sponsorUserOperationResult,
}
 
console.log("Received paymaster sponsor result:", sponsorUserOperationResult)

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

Received paymaster sponsor result: {
  paymaster: '0xcF60744ef322396a6d0a5B7d396F5814176855F1',
  paymasterVerificationGasLimit: 526114n,
  paymasterPostOpGasLimit: 75900n,
  paymasterData: '000000000000000000000000000000000000000000000000000000006518a0c100000000000000000000000000000000000000000000000000000000000000003251910f0e14691ca19a8e2cca216ce77a1ce8d002e975e113cfbc36d8e489a550dbb24ff485ee61c1b91cf8e20261d307ef79f22cf9359fba3271f721dade4d1b',
  preVerificationGas: 247487n,
  verificationGasLimit: 526114n,
  callGasLimit: 75900n
}

Great! Now we have received the gas limit estimates and added the paymaster-related fields containing Pimlico's signature to our UserOperation.

Sign the UserOperation

The last field to fill out is the signature. This is a simple ECDSA signature consistent with typical Ethereum private key signing procedures.

Add the following to the bottom of index.ts:

const signature = await signUserOperationHashWithECDSA({
	account: owner,
	userOperation: sponsoredUserOperation,
	chainId: sepolia.id,
	entryPoint: ENTRYPOINT_ADDRESS_V07,
})
sponsoredUserOperation.signature = signature
 
console.log("Generated signature:", signature)

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

Generated signature: 0xcaae357fcd3882f1ea4b48f7dcce9d7f2482794ab72d1075ce5d7fcef4c5ec03265fe03e5fd7f8af65a4cd05b7e01300f3938f7e245dc8038748ddef93d5f4061c

Submit the UserOperation to be bundled

Finally, we're ready to submit the UserOperation to Pimlico's bundler, which will include it on-chain. The eth_sendUserOperation RPC call or the sendUserOperation method from permissionless.js will submit the UserOperation to the bundler.

You can also query for receipts to keep checking the status of the UserOperation until it is included.

Add the following to the bottom of index.ts:

const userOperationHash = await bundlerClient.sendUserOperation({
	userOperation: sponsoredUserOperation,
})
 
console.log("Received User Operation hash:", userOperationHash)
 
// let's also wait for the userOperation to be included, by continually querying for the receipts
console.log("Querying for receipts...")
const receipt = await bundlerClient.waitForUserOperationReceipt({
	hash: userOperationHash,
})
const txHash = receipt.receipt.transactionHash
 
console.log(`UserOperation included: https://sepolia.etherscan.io/tx/${txHash}`)

If we run this code with npm start, we will go through the whole flow of executing the User Operation. You should see something like this:

UserOperation included: https://sepolia.etherscan.io/tx/0xe0a47e0e6637d0d4062cda3683afe2f5809bf545b917888277905de38f08e188

Once the UserOperation is included, you can view the transaction on the Sepolia testnet explorer.

That's it! You've successfully generated a UserOperation and submitted it using Pimlico's Alto bundler.

By leveraging Pimlico's paymaster, you were able to make the User Operation completely gasless, and by using Pimlico's Alto bundler, you were able to submit the User Operation to the chain without having to worry about maintaining your own relaying infrastructure.

Congratulations, you are now a pioneer of Account Abstraction! 🎉

Please get in touch if you have any questions or if you'd like to share what you're building!

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!