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.
- Standard publicClient for normal Ethereum RPC calls — https://rpc.sepolia.org
- Pimlico v1 api for the Bundler methods — https://api.pimlico.io/v2/sepolia/rpc?apikey=YOUR_PIMLICO_API_KEY
- 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, {
timeout: 30_000
}),
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!