Simulates a ZeroDev user operation and returns a breakdown of expected asset changes, security
validation results, and optionally an estimated gas fee — before the operation is submitted to the
network.
Returns the same result shape as simulateEvmTransaction, since user operations are ultimately EVM
transactions. Powered by Blockaid for security validation.
Installation
npm install @dynamic-labs-sdk/zerodev
Usage
import { simulateZerodevUserOperation } from '@dynamic-labs-sdk/zerodev';
import { isEvmWalletAccount } from '@dynamic-labs-sdk/evm';
import { getPrimaryWalletAccount } from '@dynamic-labs-sdk/client';
import { parseEther, encodeFunctionData } from 'viem';
const walletAccount = getPrimaryWalletAccount();
if (walletAccount && isEvmWalletAccount(walletAccount)) {
const result = await simulateZerodevUserOperation({
walletAccount,
userOperation: {
sender: walletAccount.address,
callData: encodeFunctionData({
abi: erc20Abi,
functionName: 'transfer',
args: [recipientAddress, parseUnits('100', 18)],
}),
},
networkId: '1',
entryPoint: ENTRY_POINT_ADDRESS,
});
console.log('Assets going out:', result.outAssets);
console.log('Security check:', result.validation?.result);
}
Parameters
| Parameter | Type | Description |
|---|
walletAccount | EvmWalletAccount | The ZeroDev-enabled wallet account |
userOperation | Partial<UserOperation> | The user operation to simulate |
networkId | string | The chain ID (e.g., '1' for Ethereum, '137' for Polygon) |
entryPoint | string | The EntryPoint contract address |
includeFees | boolean (optional) | Whether to include a gas fee estimate. Default: false |
client | DynamicClient (optional) | Only required when using multiple clients |
Returns
Promise<EvmSimulationResult> — an object with the following fields:
| Field | Type | Description |
|---|
inAssets | AssetDiff[] | Assets the wallet will receive |
outAssets | AssetDiff[] | Assets the wallet will send |
validation | BlockaidValidation (optional) | Security assessment from Blockaid |
validation.result | 'benign' | 'warning' | 'malicious' | Overall security verdict |
validation.description | string (optional) | Human-readable description |
validation.reason | string (optional) | Reason for the verdict |
feeData | EvmTransactionFeeData (optional) | Present only when includeFees: true |
feeData.humanReadableAmount | string | Fee in ETH, formatted for display |
feeData.nativeAmount | bigint | Fee in wei (0n if the operation is sponsored) |
feeData.usdAmount | string (optional) | Fee in USD if price data is available |
feeData.gasEstimate | bigint | Estimated gas units |
priceData | PriceData | Price information for the assets involved |
counterparties | string[] (optional) | Other addresses involved in the operation |
When the operation is eligible for gas sponsorship, feeData.nativeAmount is 0n — indicating the
user will not pay gas. Use canSponsorUserOperation to check sponsorship eligibility before
simulating.
Examples
Token transfer
import { encodeFunctionData, parseUnits } from 'viem';
const result = await simulateZerodevUserOperation({
walletAccount,
userOperation: {
sender: walletAccount.address,
callData: encodeFunctionData({
abi: erc20Abi,
functionName: 'transfer',
args: [recipientAddress, parseUnits('50', 18)],
}),
},
networkId: '137',
entryPoint: ENTRY_POINT_ADDRESS,
});
console.log('Tokens out:', result.outAssets);
console.log('Tokens in:', result.inAssets);
With gas fee estimation
const result = await simulateZerodevUserOperation({
walletAccount,
userOperation: {
sender: walletAccount.address,
callData,
},
networkId: '1',
entryPoint: ENTRY_POINT_ADDRESS,
includeFees: true,
});
if (result.feeData) {
if (result.feeData.nativeAmount === 0n) {
console.log('Operation is sponsored — no gas cost');
} else {
console.log(`Estimated gas fee: ${result.feeData.humanReadableAmount} ETH`);
}
}
Security validation
const result = await simulateZerodevUserOperation({
walletAccount,
userOperation: { sender: walletAccount.address, callData },
networkId: '1',
entryPoint: ENTRY_POINT_ADDRESS,
});
if (result.validation?.result === 'malicious') {
console.warn('Operation flagged as malicious. Blocking.');
return;
}
Error handling
import { SimulationFailedError, FeeEstimationFailedError } from '@dynamic-labs-sdk/client';
try {
const result = await simulateZerodevUserOperation({
walletAccount,
userOperation,
networkId,
entryPoint,
includeFees: true,
});
} catch (error) {
if (error instanceof SimulationFailedError) {
console.error('Simulation failed:', error.message);
} else if (error instanceof FeeEstimationFailedError) {
console.error('Fee estimation failed:', error.message);
}
}