SDK
Guides
Track ERC-4337 transactions

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

Install dependencies

pnpm
npm
yarn

_10
pnpm add @safe-global/relay-kit viem

Steps

Imports

Here are all the necessary imports for this guide.


_10
import { Safe4337Pack } from '@safe-global/relay-kit'
_10
import { 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:


_10
const 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 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.


_15
const 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
_10
const transaction1 = { to, data, value }
_10
const transaction2 = { to, data, value }
_10
_10
// Build the transaction array
_10
const transactions = [transaction1, transaction2]
_10
_10
// Create the SafeOperation with all the transactions
_10
const 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.


_10
safeOperation.data.callData = concat([
_10
safeOperation.data.callData as `0x{string}`,
_10
onchainIdentifier
_10
]).toString()
_10
_10
const 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.


_10
const 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.


_10
const 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.


_10
let userOperationReceipt = null
_10
_10
while (!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:

Was this page helpful?