Track ERC-4337 transactions with an onchain identifier
In this guide, you will learn how to get an identifier and track all your ERC-4337 Safe transactions onchain by attaching it to the user operation's callData
.
This guide summarizes all the steps included in the Safe accounts with the Safe4337Module guide, with an additional step to add the onchainIdentifier
to your Safe transactions. If you already have the Relay Kit integrated, continue reading to learn how to get your onchain identifier, and feel free to jump straight to step number five to learn how to use it afterward.
Prerequisites
- Node.js and npm (opens in a new tab).
- A Pimlico account (opens in a new tab) and an API key.
Install dependencies
Steps
Imports
Here are all the necessary imports for this guide.
_10import { Safe4337Pack } from '@safe-global/relay-kit'_10import { concat, toHex } from 'viem'
Get your on-chain identifier
This identifier is unique for every project and has a length of 16 bytes. You can create a random one or derive it from a text string, maybe from your project name:
_10const onchainIdentifier = toHex(_10 'TEXT_TO_DERIVE_THE_IDENTIFIER', // Could be your project name_10 { size: 16 }_10)
If you are applying to the Safe{Core} Gas Station Program (opens in a new tab), make sure to provide the value of onchainIdentifier
in the Gas Station form (opens in a new tab) when asked.
Initialize the Safe4337Pack
Feel free to skip this step if you have the Safe4337Pack
already integrated into your application.
Check the Safe4337Module guide to get all the details on the different ways of initializing the Safe4337Pack
, whether there is a new Safe to deploy or an existing one to use, as well as several possible configurations for the Paymaster.
_15const safe4337Pack = await Safe4337Pack.init({_15 provider,_15 signer,_15 bundlerUrl,_15 options: {_15 safeAddress_15 },_15 paymasterOptions: {_15 isSponsored,_15 paymasterUrl,_15 paymasterAddres,_15 paymasterTokenAddress,_15 sponsorshipPolicyId // Don't forget about your sponsorship policy id_15 }_15})
Create a user operation
To create a Safe user operation, use the createTransaction()
method. This method takes the array of transactions to execute and returns a SafeOperation
object.
_10// Define the transactions to execute_10const transaction1 = { to, data, value }_10const transaction2 = { to, data, value }_10_10// Build the transaction array_10const transactions = [transaction1, transaction2]_10_10// Create the SafeOperation with all the transactions_10const safeOperation = await safe4337Pack.createTransaction({ transactions })
The safeOperation
object has the data
and signatures
properties, which contain all the information about the transaction batch and the signatures of the Safe owners, respectively.
Add the onchainIdentifier
to the callData
Once the safeOperation
is ready, you need to add the onchainIdentifier
at the end of the ERC-4337 user operation callData
.
_10safeOperation.data.callData = concat([_10 safeOperation.data.callData as `0x{string}`,_10 onchainIdentifier_10]).toString()_10_10const identifiedSafeOperation = await safe4337Pack.getEstimateFee({_10 safeOperation_10})
Once added, the Safe owners can sign the identifiedSafeOperation
.
Sign the user operation
Before sending the user operation to the bundler, the safeOperation
object must be signed with the connected signer. The signSafeOperation()
method, which receives a SafeOperation
object, generates a signature that will be checked when the Safe4337Module
validates the user operation.
_10const signedSafeOperation = await safe4337Pack.signSafeOperation(safeOperation)
Submit the user operation
Once the safeOperation
object is signed, we can call the executeTransaction()
method to submit the user operation to the bundler.
_10const userOperationHash = await safe4337Pack.executeTransaction({_10 executable: signedSafeOperation_10})
Once the transaction is executed, you will see the onchainIdentifier
included at the end of the user operation callData
.
Check the transaction status
To check the transaction status, we can use the getTransactionReceipt()
method, which returns the transaction receipt after execution.
_10let userOperationReceipt = null_10_10while (!userOperationReceipt) {_10 // Wait 2 seconds before checking the status again_10 await new Promise((resolve) => setTimeout(resolve, 2000))_10 userOperationReceipt = await safe4337Pack.getUserOperationReceipt(_10 userOperationHash_10 )_10}
Now, tracking all your transactions is possible by performing onchain queries looking for your identifier at the end of the callData
field.
Recap and further reading
If you are not using the Safe{Core} SDK to submit transactions, the following GitHub repository may be helpful:
- The safe-onchain-identifiers (opens in a new tab) repository showcases where and how to add the identifier at the end of your Safe transactions data without using the Relay Kit and with a low-level detail. Check also the specific code (opens in a new tab) where the identifier is concatenated to the
callData
.