Sending Transactions
In ERC-4337, a transaction is known as a "UserOp." A UserOp looks mostly like a regular transaction, but it contains some extra information specific to AA, such as whether the UserOp should be sponsored.
There are two ways to send UserOps:
- Sending raw UserOps
- Sending regular transactions through the Viem API, which ZeroDev then "translates" into UserOps
The former enables the highest degree of flexibility, whereas the latter is more interoperable with existing libraries like Viem that deal only with transactions and not UserOps.
We will now describe both approaches. We assume that you have already created a Kernel account.
Using the Viem API
Since the Kernel account client implements Viem's wallet client interface, you can send UserOps with standard Viem methods.
Sending Transactions
const txnHash = await kernelClient.sendTransaction({
to: "TO_ADDRESS",
value: VALUE, // default to 0
data: "0xDATA", // default to 0x
})This function returns the transaction hash of the ERC-4337 bundle that contains the UserOp. Due to the way that ERC-4337 works, by the time we get the transaction hash, the ERC-4337 bundle (and therefore the UserOps includeded within) will have already been mined, meaning that you don't have to wait with the hash.
If you need to separate the sending from the waiting of the UserOp, try sending raw UserOps.
Interacting with Contracts
First, construct a Viem contract instance by passing the Kernel account client as the walletClient:
import { getContract } from 'viem'
const contract = getContract({
address: '0xADDRESS',
abi: abi,
publicClient: publicClient,
walletClient: kernelClient,
})Then, interact with the contract like how you normally would:
// Example code from Viem
const balance = await contract.read.balanceOf([
'0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC',
])
const hash = await contract.write.mint([69420])
const logs = await contract.getEvents.Transfer()
const unwatch = contract.watchEvent.Transfer(
{
from: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045',
to: '0xa5cc3c03994db5b0d9a5eedd10cabab0813678ac'
},
{ onLogs: logs => console.log(logs) }
)Sending UserOps
UserOp API
Sending raw UserOps affords you with the highest degree of control. To send a raw UserOp, use sendUserOperation:
const userOpHash = await kernelClient.sendUserOperation({
callData: "0x..."
})While callData is the only required field, there are many other fields that you can read about on the ERC-4337 spec:
const userOpHash = await kernelClient.sendUserOperation({
sender: "0x0C123D90Da0a640fFE54a2359D159629065775C5",
nonce: 3n,
initCode: "0x",
callData: "0x18dfb3c7000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000d2f598c826429eee7c071c02735549acd88f2c09000000000000000000000000d2f598c826429eee7c071c02735549acd88f2c090000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000044a9059cbb00000000000000000000000043a4eacb7839f202d9cab465dbdd77d4fabe0a1800000000000000000000000000000000000000000000000003782dace9d90000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044a9059cbb000000000000000000000000982e148216e3aa6b38f9d901ef578b5c06dd750200000000000000000000000000000000000000000000000005d423c655aa000000000000000000000000000000000000000000000000000000000000",
callGasLimit: 50305n,
verificationGasLimit: 80565n,
preVerificationGas: 56135n,
maxFeePerGas: 113000000n,
maxPriorityFeePerGas: 113000100n,
paymasterAndData: "0xe93eca6595fe94091dc1af46aac2a8b5d79907700000000000000000000000000000000000000000000000000000000065133b6f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005d3d07ae8973ba1b8a26d0d72d8882dfa97622942a63c4b655f4928385ce587f6aa2fa1ab347e615d5f39e1214d18f426375da8a01514fb126eb0bb29f0c319d1b",
signature: "0xf1513a8537a079a4d728bb87099b2c901e2c9034e60c95a4d41ac1ed75d6ee90270d52b48af30aa036e9a205ea008e1c62b317e7b3f88b3f302d45fb1ba76a191b"
})Other than callData, every field has a sensible default:
senderdefaults to the Kernel account addressnoncedefaults to the next available nonceinitCodedefaults to0xif the account has been deployed, or the correctinitCodeif not.callGasLimit,verificationGasLimit, andpreVerificationGasdefault to estimations provided by the underlying bundler and paymaster.maxFeePerGasandmaxPriorityFeePerGasdefault to estimations provided by the public client.paymasterAndDatadefaults to0xif no paymaster was specified when you created the Kernel account object, or it will use the value provided by the paymaster.signaturedefaults to the signature provided by the signer.
Encoding callData
To encode the calldata, use the encodeCalls function from the account object:
const userOpHash = await kernelClient.sendUserOperation({
callData: kernelClient.account.encodeCalls([{
to,
value,
data,
}]),
})You can use Viem's helper functions such as encodeFunctionData to encode function calls. For example:
const userOpHash = await kernelClient.sendUserOperation({
callData: await kernelClient.account.encodeCalls([{
to: contractAddress,
value: BigInt(0),
data: encodeFunctionData({
abi: contractABI,
functionName: "functionName",
args: [args1, args2],
}),
}]),
})Waiting for a UserOp to complete
To wait for a UserOp to complete, call waitForUserOperationReceipt using a kernelClient client:
const receipt = await kernelClient.waitForUserOperationReceipt({
hash: userOpHash,
})Constructing a UserOp for sending later
In some applications, you might want to construct a UserOp but not immediately send it. There are two possible flows:
-
If you want to separate signing and sending:
- Create and sign a UserOp with
kernelClient.signUserOperation() - Send the UserOp with
kernelClient.sendUserOperation()
- Create and sign a UserOp with
-
If you want to separate the constructing, signing, and sending:
- Create an unsigned UserOp with
kernelClient.prepareUserOperation() - Sign the UserOp with
kernelClient.account.signUserOperation()and manually set theuserOp.signaturefield - Send the UserOp with
kernelClient.sendUserOperation()
- Create an unsigned UserOp with