This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Alto is a TypeScript implementation of the ERC-4337 bundler specification, designed for high transaction inclusion reliability. It supports multiple ERC-4337 versions (0.6, 0.7, and 0.8) and includes chain-specific optimizations.
# Install dependencies
pnpm install
# Build everything (including smart contracts)
pnpm run build
# Run in development mode with auto-reload
pnpm run dev
# Start the bundler
pnpm start
# Run tests
pnpm test
# Run a specific test
cd e2e && pnpm test -t "test name"
# Lint and format code
pnpm run lint
pnpm run format# Build all contract versions
pnpm run build:contracts
# Build specific version contracts
pnpm run build:contracts-v06
pnpm run build:contracts-v07
pnpm run build:contracts-v08src/cli/: CLI entry point and option parsingsrc/rpc/: JSON-RPC server with ERC-4337 methods (eth_sendUserOperation, etc.)src/executor/: Bundle creation and submission logic, implements transaction execution strategiessrc/mempool/: User operation pool management with validation and reputation trackingsrc/store/: Storage abstraction layer (Redis or in-memory)src/handlers/: Chain-specific gas price managers (Arbitrum, Optimism, Mantle)src/utils/: Shared utilities, validation helpers, and common types
- Multi-version Support: Each ERC-4337 version has dedicated handlers in separate directories (v06, v07, v08)
- Chain Abstraction: Chain-specific logic is isolated in handlers, allowing easy addition of new chains
- Storage Flexibility: Store interface allows switching between Redis and in-memory storage
- Executor Strategies: Supports different bundle submission strategies (conditional, flashbots)
- Comprehensive Validation: Multiple validation layers including simulation, reputation, and paymaster checks
src/cli/config/bundle.ts: CLI configuration and option definitionssrc/executor/executor.ts: Main bundle execution logicsrc/mempool/mempool.ts: User operation mempool implementationsrc/rpc/server.ts: RPC server setupsrc/validator/validator.ts: User operation validation logic
- Runtime: Node.js 18+ with ESM modules
- Language: TypeScript 5.x with strict mode
- Web Framework: Fastify for HTTP/WebSocket
- Smart Contracts: Solidity with Foundry toolchain
- Storage: Redis (optional) or in-memory
- Monitoring: OpenTelemetry, Prometheus metrics
- Code Quality: Biome for linting/formatting
- Testing: Vitest for e2e tests
- Validation: Zod for runtime type validation
- Logging: Pino with custom serializers
- The project uses pnpm workspaces - always use
pnpminstead ofnpmoryarn - Smart contracts must be built before running the bundler
- For debugging, enable verbose logging with
--verboseflag - Use
--dangerous-skip-user-operation-validationonly for testing - The bundler requires an Ethereum node with
debug_traceCallsupport
- E2E tests are in the
e2e/directory using Vitest - Test against local Anvil instances or testnets
- Mock mode available for development without real blockchain
- Create a new handler in
src/handlers/ - Implement the
GasPriceManagerinterface - Register in the appropriate version's handler factory
- Add the schema for your new endpoint in
src/types/schemas.ts:- Define the schema using Zod (e.g.,
pimlicoNewEndpointSchema) - Add it to both
bundlerRequestSchemaandbundlerRpcSchemaunions
- Define the schema using Zod (e.g.,
- Create a new file in
src/rpc/methods/following the naming convention (e.g.,pimlico_newEndpoint.ts) - Implement the endpoint handler following the existing pattern using
createMethodHandler - Import and register the handler in
src/rpc/methods/index.ts - The endpoint will be automatically registered with the RPC server
- Update the method in
src/rpc/methods/ - Ensure compatibility across all supported versions
- Update validation logic if needed
- Validation logic is in
src/validator/ - Mempool operations are in
src/mempool/ - Execution logic is in
src/executor/
- Strict Mode: Always enabled with additional checks
- Module System: ESM with
@alto/*aliases for internal imports - Target: ESNext for modern JavaScript features
- Type Safety: Never use
anytype - use proper type definitions,unknown, or type assertions when needed
- Interfaces: Prefixed with
Interface(e.g.,InterfaceValidator) - Types: PascalCase for type definitions
- Files: kebab-case for filenames (e.g.,
gas-price-manager.ts) - Constants: UPPER_SNAKE_CASE for constants
- Functions/Methods: camelCase
- UserOperation Naming:
- Local variables and parameters: Use
userOp(e.g.,submittedUserOp,validUserOp,queuedUserOps) - Local method names: Use
userOp(e.g.,dropUserOps,addUserOp,getUserOpHash) - RPC endpoints: Use full
userOperationname (e.g.,eth_sendUserOperation) - Types and interfaces: Use full
UserOperationname (e.g.,UserOperationV07,PackedUserOperation) - Zod schemas: Use full
userOperationname (e.g.,userOperationSchema,userOperationV06Schema) - Solidity contracts: Use full
UserOperationname - Inline comments: Use full
userOperationwhen referring to the concept
- Local variables and parameters: Use
- EntryPoint Naming:
- Local variables and parameters: Use
entryPoint(e.g.,entryPoint,supportedEntryPoints) - Avoid:
entryPointAddress- prefer justentryPointsince it's understood to be an address
- Local variables and parameters: Use
- External dependencies
- Internal type imports (
import type { ... } from "@alto/types") - Internal module imports (
import { ... } from "@alto/utils") - Relative imports
// Use object destructuring for multiple parameters
async function functionName({
param1,
param2
}: {
param1: Type1
param2: Type2
}): Promise<ReturnType> {
// Implementation
}- Use custom error classes (e.g.,
RpcError) - Include specific error codes from enums
- Viem errors are wrapped: Never use direct
instanceofchecks on caught viem errors. Viem wraps errors (e.g.,InsufficientFundsErrorinsideTransactionExecutionError). Always useBaseError.walk()to find the actual error in the cause chain:if (e instanceof BaseError) { const isInsufficientFunds = e.walk( (err) => err instanceof InsufficientFundsError ) if (isInsufficientFunds) { // handle } }
- Return error tuples for non-throwing operations
- Use structured logging with Pino
- Create child loggers with context
- Convert BigInts to hex strings in logs
- Include relevant data in log objects
- Pino expects errors to be logged with the
errkey (noterror):logger.error({ err: error }, "message")
- Use Zod schemas for runtime validation
- Transform values in schemas (e.g.,
transform((val) => val as Hex)) - Create branded types for type safety
- Validate at system boundaries (RPC, storage)
- Use Vitest with
describe.eachfor version testing - Follow Arrange-Act-Assert pattern
- Use
beforeEachfor test setup - Test against real blockchain (Anvil) when possible
- Constructor-based injection
- Pass configuration and dependencies as objects
- Use interfaces for testability
- Use
Promise.allfor parallel operations - Proper error handling in try-catch blocks
- Explicit return types for async functions
- Important: When making RPC calls for methods that are not natively supported by viem, use the viem client's
requestmethod - The viem
Clienttype provides arequestmethod for custom RPC calls:client.request({ method: 'custom_method', params: [...] }) - Only use this for non-standard RPC methods (e.g.,
debug_traceCall, custom bundler methods) - For standard methods, use viem's built-in functions (e.g.,
client.getBalance()instead ofclient.request({ method: 'eth_getBalance' }))
- Export public API through index files
- Keep version-specific logic in separate directories
- Use factory pattern for creating handlers
- Indentation: 4 spaces
- Line Width: 80 characters
- Semicolons: Omitted where possible
- Trailing Commas: None
- Run
pnpm run formatbefore committing
When working with BigInt calculations, use the utility functions from @alto/utils:
- scaleBigIntByPercent: Scale a BigInt by a percentage (e.g.,
scaleBigIntByPercent(value, 150n)for 150%) - minBigInt/maxBigInt: Get min/max of two BigInts
- roundUpBigInt: Round up to nearest multiple
- Never use manual percentage calculations like
(value * 150n) / 100n
When extracting userOp hashes from UserOpInfo[] arrays, use getUserOpHashes from @alto/executor:
- getUserOpHashes: Extract hashes from userOp info array (e.g.,
getUserOpHashes(userOps)returnsstring[]) - Never manually map like
userOps.map(op => op.userOpHash)- use the helper instead
- Batch operations when possible
- Use efficient data structures
- Minimize BigInt conversions
- Cache expensive computations
- Never log sensitive data (private keys, etc.)
- Validate all external inputs
- Use checksummed addresses
- Follow ERC-4337 security guidelines