Skip to content

How to conditionally sponsor a user operation

The permissionless.js library allows you to conditionally sponsor a user operation. This can be useful in situations like:

  • Sponsor only first 10 transactions
  • Sponsor if they have a specific NFT

The use cases can be many, but the general idea is to use the middleware.sponsorUserOperation parameter to conditionally sponsor a user operation.

Let's take an example where you want to sponsor only the first 10 transactions.

Create the clients

First we must create the public, (optionally) pimlico paymaster clients that will be used to interact with the SimpleAccount.

import { createPublicClient, http } from "viem"
import { decodeNonce } from "permissionless/utils"
import { createPimlicoClient } from "permissionless/clients/pimlico"
import { entryPoint07Address } from "viem/account-abstraction"
 
const publicClient = createPublicClient({
	chain: sepolia,
	transport: http("https://rpc.ankr.com/eth_sepolia"),
})
 
const pimlicoUrl = `https://api.pimlico.io/v2/sepolia/rpc?apikey=${apiKey}`
 
const pimlicoClient = createPimlicoClient({
	transport: http(pimlicoUrl),
	entryPoint: {
		address: entryPoint07Address,
		version: "0.7",
	},
})

Create an account

Now, create an account. This can any of the accounts supported by permissionless.js or custom accounts conforming to the interface. For this example, we'll use a Simple account.

import { toSimpleSmartAccount } from "permissionless/accounts"
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"
 
export const simpleSmartAccount = await toSimpleSmartAccount({
	client: publicClient,
	owner: privateKeyToAccount(generatePrivateKey()),
	entryPoint: {
		address: entryPoint07Address,
		version: "0.7",
	},
})

Create the smart account client with the custom paymaster logic

When creating the smartAccountClient, we can pass in a middleware.sponsorUserOperation function that will be called before a user operation is signed and sent. This function can conditionally choose to add a paymaster to the user operation if the desired conditions are met.

import { sepolia } from "viem/chains"
import { createSmartAccountClient } from "permissionless"
import { getAccountNonce } from "permissionless/actions"
 
const smartAccountClient = createSmartAccountClient({
	client: publicClient,
	account: simpleSmartAccount,
	chain: sepolia,
	bundlerTransport: http(pimlicoUrl),
	paymaster: {
		async getPaymasterData(parameters) {
			const nonce = await getAccountNonce(publicClient, {
				address: parameters.sender,
				entryPointAddress: parameters.entryPointAddress,
			})
 
			const { sequence } = decodeNonce(nonce)
 
			if (sequence < 10) {
				// sponsor it for the user as the sequence is less than 10
				// sequence is the number of transactions sent by the user for a given key
				// by default key is 0x0
				return pimlicoClient.getPaymasterData(parameters)
			}
 
			return {
				paymaster: "0x",
				paymasterData: "0x",
				paymasterVerificationGasLimit: 0n,
				paymasterPostOpGasLimit: 0n,
			}
		},
		async getPaymasterStubData(parameters) {
			const nonce = await getAccountNonce(publicClient, {
				address: parameters.sender,
				entryPointAddress: parameters.entryPointAddress,
			})
 
			const { sequence } = decodeNonce(nonce)
 
			if (sequence < 10) {
				// sponsor it for the user as the sequence is less than 10
				// sequence is the number of transactions sent by the user for a given key
				// by default key is 0x0
				return pimlicoClient.getPaymasterStubData(parameters)
			}
 
			return {
				paymaster: "0x",
				paymasterData: "0x",
				paymasterVerificationGasLimit: 0n,
				paymasterPostOpGasLimit: 0n,
			}
		},
	},
	userOperation: {
		estimateFeesPerGas: async () => {
			return (await pimlicoClient.getUserOperationGasPrice()).fast
		},
	},
})