Bundler & UserOperations
Submit, estimate, and track ERC-4337 UserOperations through the low-level bundler API.
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
Configure the public client for on-chain reads (fee estimation, nonce queries):
import { setPublicClient } from '@embarkai/core'
import { createPublicClient, http } from 'viem'
const client = createPublicClient({
transport: http('https://rpc.lumia.org'),
})
setPublicClient(client)UserOperationV07 Type
The module uses the ERC-4337 v0.7 UserOperation format:
import type { UserOperationV07 } from '@embarkai/core'
const userOp: UserOperationV07 = {
sender: '0xYourSmartAccountAddress',
nonce: 0n,
initCode: '0x',
callData: '0x...',
callGasLimit: 500000n,
verificationGasLimit: 500000n,
preVerificationGas: 100000n,
maxFeePerGas: 1000000000n,
maxPriorityFeePerGas: 100000000n,
paymasterAndData: '0x',
signature: '0x',
}Submitting UserOperations
Basic Submission
import {
sendUserOperationRaw,
ENTRYPOINT_V07,
} from '@embarkai/core'
const userOpHash = await sendUserOperationRaw(
bundlerClient,
ENTRYPOINT_V07,
packedUserOp,
)Submission with Retry
Automatically retries on transient network errors:
import { sendUserOperationWithRetry, ENTRYPOINT_V07 } from '@embarkai/core'
const userOpHash = await sendUserOperationWithRetry(
bundlerClient,
ENTRYPOINT_V07,
packedUserOp,
3, // maxRetries (default: 3)
)Get Receipt
import { getUserOperationReceipt } from '@embarkai/core'
const receipt = await getUserOperationReceipt(userOpHash)
if (receipt?.success) {
console.log('TX:', receipt.transactionHash)
}Gas Estimation
Standard Estimation
import { estimateUserOperationGas } from '@embarkai/core'
const gasEstimate = await estimateUserOperationGas(userOp)
// gasEstimate: { callGasLimit, verificationGasLimit, preVerificationGas }With Existing Signature
Skip dummy signature injection when your UserOp is already signed:
import { estimateUserOperationGasWithSignature } from '@embarkai/core'
const gasEstimate = await estimateUserOperationGasWithSignature(signedUserOp)With Dynamic Fees
Estimates gas and applies current fee calculations in one call:
import { estimateUserOperationGasWithDynamicFees } from '@embarkai/core'
const result = await estimateUserOperationGasWithDynamicFees(userOp)
// Returns gas limits with maxFeePerGas and maxPriorityFeePerGas appliedSimulation
Dry-run a UserOperation to check for revert reasons before submitting:
import { simulateUserOperation, validateUserOperation } from '@embarkai/core'
const simResult = await simulateUserOperation(userOp)
console.log('Success:', simResult.success)
const validationResult = await validateUserOperation(userOp)
console.log('Valid:', validationResult.success)Fee Calculation
The module offers multiple fee strategies for different use cases:
| Strategy | Function | Description |
|---|---|---|
| Dynamic | calculateDynamicFees() | EIP-1559 fees based on current base fee + priority fee |
| Legacy | calculateLegacyFees() | Pre-EIP-1559 gas price calculation |
| Fast | calculateFastFees() | Higher priority fee for faster inclusion |
| Economy | calculateEconomyFees() | Lower fees, longer confirmation time |
Fee Strategy Comparison
| Strategy | Priority Fee Multiplier | Base Fee Buffer | Best For |
|---|---|---|---|
| Economy | 1x | +10% | Non-urgent background operations |
| Dynamic (Standard) | 1.5x | +20% | Most transactions |
| Fast | 2x | +30% | Time-sensitive operations |
Usage
import {
calculateDynamicFees,
calculateFastFees,
calculateEconomyFees,
getCurrentBaseFee,
getFeeEstimates,
} from '@embarkai/core'
// Get current base fee
const baseFee = await getCurrentBaseFee()
// Calculate fees with a specific strategy
const dynamicFees = await calculateDynamicFees()
const fastFees = await calculateFastFees()
const economyFees = await calculateEconomyFees()
// Get all strategies at once
const estimates = await getFeeEstimates()
console.log('Economy:', estimates.economy.maxFeePerGas)
console.log('Standard:', estimates.standard.maxFeePerGas)
console.log('Fast:', estimates.fast.maxFeePerGas)Custom Fee Config
Override defaults with DEFAULT_FEE_CONFIG or CONSERVATIVE_FEE_CONFIG:
import { DEFAULT_FEE_CONFIG, CONSERVATIVE_FEE_CONFIG } from '@embarkai/core'
// DEFAULT_FEE_CONFIG: balanced defaults
// CONSERVATIVE_FEE_CONFIG: higher buffers for congested networksPacking UserOperations
Convert a v0.7 UserOperation into the packed format required by the EntryPoint contract:
import { packUserOperationV07 } from '@embarkai/core'
const packed = packUserOperationV07(userOp)
// packed: PackedUserOperationV07 ready for on-chain submissionUtility Functions
import {
DUMMY_SIGNATURE,
createDummyUserOperation,
createUserOperationWithDynamicFees,
isNetworkError,
} from '@embarkai/core'
// Create a placeholder UserOp for gas estimation
const dummyOp = createDummyUserOperation(senderAddress)
// Create a UserOp with current dynamic fees pre-filled
const userOp = await createUserOperationWithDynamicFees(baseUserOp)
// Check if an error is a retryable network issue
if (isNetworkError(error)) {
// safe to retry
}Usage with ServerWallet
In most cases, ServerWallet handles all bundler interactions internally:
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 hash = await wallet.sendUserOperation(
'0xRecipient',
'1000000000000000000',
'0x',
'fast', // uses calculateFastFees internally
)Next Steps
- Contract Encoding — encode calldata for token transfers and contract calls
- Server Wallets — high-level wallet API that wraps the bundler
- Vault Backup — back up keyshares for disaster recovery