Skip to Content
⚙️ Core SDKBundler & UserOperations

Bundler & UserOperations

Submit, estimate, and track ERC-4337 UserOperations through the embarkAI bundler.

The bundler module provides direct access to UserOperation submission, gas estimation, and fee calculation. Most users should prefer the higher-level ServerWallet.sendUserOperation() API — use this module when you need fine-grained control over gas parameters or fee strategies.

Setup

Chain-specific helpers take a chainId and read the bundler URL and EntryPoint v0.7 address from the built-in chain config. Fee helpers take a FeeChainContext:

import { createFeeChainContext, setPublicClient } from '@embarkai/core' const context = createFeeChainContext(994873017) // { client, bundlerUrl, chainConfig } // Register the client for helpers that read on-chain state (nonce, base fee) setPublicClient(context.client)

Pass { client, bundlerUrl } as the second argument to use your own viem client or bundler endpoint.

UserOperationV07 Type

The module uses the ERC-4337 v0.7 UserOperation format. Every numeric field is a hex string, not a bigint. The factory and paymaster fields are all optional and replace initCode / paymasterAndData of the packed form:

import type { UserOperationV07 } from '@embarkai/core' const userOp: UserOperationV07 = { sender: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', nonce: '0x0', callData: '0x', callGasLimit: '0x7a120', verificationGasLimit: '0x7a120', preVerificationGas: '0x186a0', maxFeePerGas: '0x3b9aca00', maxPriorityFeePerGas: '0x5f5e100', signature: '0x', // optional: factory, factoryData (first operation of an undeployed account) // optional: paymaster, paymasterData, paymasterVerificationGasLimit, paymasterPostOpGasLimit }

Use toHex from viem to convert bigint values. The snippets below reuse this userOp.

Submitting

import { sendUserOperationRaw } from '@embarkai/core' const userOpHash = await sendUserOperationRaw(userOp, { chainId: 994873017 })

sendUserOperationWithRetry retries on transient failures (nonce races, rate limits). You supply a resigner callback that produces a fresh signed UserOperationV07 for a given on-chain nonce:

import { sendUserOperationWithRetry } from '@embarkai/core' import type { UserOperationV07 } from '@embarkai/core' async function signUserOpAtNonce(nonce: `0x${string}`): Promise<UserOperationV07> { // rebuild and sign the operation with the new nonce return { ...userOp, nonce } } const userOpHash = await sendUserOperationWithRetry(userOp, signUserOpAtNonce, { chainId: 994873017, maxRetries: 3, })

Receipt

The low-level receipt helper takes the bundler URL explicitly and returns the raw eth_getUserOperationReceipt result:

import { getUserOperationReceipt } from '@embarkai/core' import { getChainConfig } from '@embarkai/core/read' const chainConfig = getChainConfig(994873017)! const userOpHash = '0x…' as `0x${string}` const receipt = await getUserOperationReceipt(userOpHash, chainConfig.bundlerUrl) if (receipt?.success) { console.log('TX:', receipt.receipt.transactionHash) }

Gas Estimation

import { estimateUserOperationGas } from '@embarkai/core' const gasEstimate = await estimateUserOperationGas(userOp, { chainId: 994873017, retryWithoutFactory: false, // true: retry with factory fields stripped if the account already exists }) // gasEstimate: { callGasLimit, verificationGasLimit, preVerificationGas } as hex strings

estimateUserOperationGasWithDynamicFees builds the operation from UserOperationParams, estimates gas and applies the chosen fee strategy in one call:

import { estimateUserOperationGasWithDynamicFees, createFeeChainContext } from '@embarkai/core' const context = createFeeChainContext(994873017) const result = await estimateUserOperationGasWithDynamicFees( { sender: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', nonce: '0x0', callData: '0x', feeType: 'standard', // 'economy' | 'standard' | 'fast' }, context, ) // result: gas limits plus calculatedFees with maxFeePerGas / maxPriorityFeePerGas

estimateUserOperationGasWithSignature(userOp, getSignature, getUserOpHash, options) runs the estimate with a real signature instead of the placeholder one. It is used internally by ServerWallet; prefer estimateUserOperationGas in your own code.

Simulation

Dry-run a UserOperation to see asset changes and revert reasons before submitting. Both helpers use a Lumia-specific bundler extension and are marked @experimental:

import { simulateUserOperation, validateUserOperation } from '@embarkai/core' const simulation = await simulateUserOperation(userOp, { chainId: 994873017 }) if (simulation.error) { console.error('Would revert:', simulation.error.message) } else { for (const change of simulation.changes) { console.log(change.assetType, change.changeType, change.from, '→', change.to) } } const validation = await validateUserOperation(userOp, { chainId: 994873017 }) console.log('Valid:', validation.valid, validation.error)

Fee Calculation

All fee helpers take the FeeChainContext from above:

StrategyFunctionDescription
StandardcalculateDynamicFees(config, context)EIP-1559 fees from the current base fee and bundler priority fee; config defaults to DEFAULT_FEE_CONFIG
FastcalculateFastFees(context)Larger buffers for faster inclusion
EconomycalculateEconomyFees(context)Smaller buffers, longer confirmation time
LegacycalculateLegacyFees(bufferPercent, context)Pre-EIP-1559 gas price plus a percentage buffer; used automatically on chains without EIP-1559 support
import { calculateDynamicFees, calculateFastFees, calculateEconomyFees, getCurrentBaseFee, getFeeEstimates, CONSERVATIVE_FEE_CONFIG, createFeeChainContext, } from '@embarkai/core' const context = createFeeChainContext(994873017) // Current base fee in wei const baseFee = await getCurrentBaseFee(context) // Calculate fees with a specific strategy const standardFees = await calculateDynamicFees(undefined, context) const conservativeFees = await calculateDynamicFees(CONSERVATIVE_FEE_CONFIG, context) const fastFees = await calculateFastFees(context) const economyFees = await calculateEconomyFees(context) // All three strategies at once const estimates = await getFeeEstimates(context) console.log('Economy:', estimates.economy.maxFeePerGas) console.log('Standard:', estimates.standard.maxFeePerGas) console.log('Fast:', estimates.fast.maxFeePerGas)

Every helper returns CalculatedFees with maxFeePerGas and maxPriorityFeePerGas as hex strings, ready to drop into a UserOperationV07.

calculateDynamicFees accepts a FeeConfig:

import type { FeeConfig } from '@embarkai/core' const config: FeeConfig = { baseFeeBuffer: 0.5, // +50% on top of the current base fee priorityFeeBuffer: 0.05, // +5% on top of the bundler's suggested priority fee fallbackMaxFeePerGas: 3_000_000_000n, // used when the RPC does not report a base fee fallbackMaxPriorityFeePerGas: 3_000_000_000n, }

Built-in presets:

PresetbaseFeeBufferpriorityFeeBufferFallback max fee / priority fee
DEFAULT_FEE_CONFIG (standard)0.50.053 gwei / 3 gwei
CONSERVATIVE_FEE_CONFIG1.00.255 gwei / 3 gwei
calculateFastFees0.750.53 gwei / 2 gwei
calculateEconomyFees0.270.011 gwei / 0.5 gwei

Building with Dynamic Fees

createUserOperationWithDynamicFees assembles a UserOperationV07 from the params and fills in the fees for the chosen strategy:

import { createUserOperationWithDynamicFees, createFeeChainContext } from '@embarkai/core' const context = createFeeChainContext(994873017) const op = await createUserOperationWithDynamicFees( { sender: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', nonce: '0x0', callData: '0x', feeType: 'fast' }, context, )

Packing

Convert a v0.7 UserOperation into the packed format required by the EntryPoint contract. The second argument tells the packer whether to include factory / factoryData as initCode:

import { packUserOperationV07 } from '@embarkai/core' const packed = packUserOperationV07(userOp, false) // packed: PackedUserOperationV07 with initCode, accountGasLimits, gasFees, paymasterAndData

Retryable Errors

import { isNetworkError } from '@embarkai/core' try { // ... } catch (error) { if (isNetworkError(String(error))) { // safe to retry } }

Usage with ServerWallet

In most cases ServerWallet handles all bundler interactions internally. sendUserOperation estimates gas, applies the chosen fee strategy, signs with MPC and submits to the bundler of the manager’s chain:

import { createServerWalletManager, MemoryKeyshareStorage } from '@embarkai/core' const manager = createServerWalletManager({ apiKey: process.env.EMBARK_API_KEY!, chainId: 994873017, storage: new MemoryKeyshareStorage(), }) const wallet = await manager.getWallet('treasury') const userOpHash = await wallet.sendUserOperation( '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', '1000000000000000000', '0x', 'fast', // 'economy' | 'standard' | 'fast' ) const receipt = await wallet.waitForUserOperationReceipt(userOpHash, { pollIntervalMs: 2000, maxPollTimeMs: 60000, }) console.log(receipt.success, receipt.transactionHash)

Next Steps

Last updated on