Files
mev-beta/pkg/transport/provider_pools.go
Krypto Kajun 850223a953 fix(multicall): resolve critical multicall parsing corruption issues
- Added comprehensive bounds checking to prevent buffer overruns in multicall parsing
- Implemented graduated validation system (Strict/Moderate/Permissive) to reduce false positives
- Added LRU caching system for address validation with 10-minute TTL
- Enhanced ABI decoder with missing Universal Router and Arbitrum-specific DEX signatures
- Fixed duplicate function declarations and import conflicts across multiple files
- Added error recovery mechanisms with multiple fallback strategies
- Updated tests to handle new validation behavior for suspicious addresses
- Fixed parser test expectations for improved validation system
- Applied gofmt formatting fixes to ensure code style compliance
- Fixed mutex copying issues in monitoring package by introducing MetricsSnapshot
- Resolved critical security vulnerabilities in heuristic address extraction
- Progress: Updated TODO audit from 10% to 35% complete

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 00:12:55 -05:00

1283 lines
34 KiB
Go

package transport
import (
"context"
"fmt"
"os/exec"
"strconv"
"sync"
"time"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/rpc"
)
// ProviderHealthCheckFunc defines the function signature for performing health checks
type ProviderHealthCheckFunc func(ctx context.Context, provider *Provider) error
// performProviderHealthCheckWithMetrics is a shared utility function that performs health checks on providers
// and updates additional metrics for execution pools
func performProviderHealthCheckWithMetrics(provider *Provider, checkFunc ProviderHealthCheckFunc) {
if !provider.Config.HealthCheck.Enabled {
return
}
ctx, cancel := context.WithTimeout(context.Background(), provider.Config.HealthCheck.Timeout)
defer cancel()
start := time.Now()
var err error
// Execute the provider-specific health check
err = checkFunc(ctx, provider)
provider.mutex.Lock()
provider.LastHealthCheck = time.Now()
provider.IsHealthy = (err == nil)
if err == nil {
provider.AvgResponseTime = time.Since(start)
}
if err != nil {
provider.ErrorCount++
}
provider.RequestCount++
provider.mutex.Unlock()
}
// performProviderHealthCheck is a shared utility function that performs health checks on providers
func performProviderHealthCheck(provider *Provider, checkFunc ProviderHealthCheckFunc) {
if !provider.Config.HealthCheck.Enabled {
return
}
ctx, cancel := context.WithTimeout(context.Background(), provider.Config.HealthCheck.Timeout)
defer cancel()
start := time.Now()
var err error
// Execute the provider-specific health check
err = checkFunc(ctx, provider)
provider.mutex.Lock()
provider.LastHealthCheck = time.Now()
provider.IsHealthy = (err == nil)
if err == nil {
provider.AvgResponseTime = time.Since(start)
}
provider.mutex.Unlock()
}
// ProviderPool interface defines common functionality for all provider pools
type ProviderPool interface {
GetHTTPClient() (*ethclient.Client, error)
GetWSClient() (*ethclient.Client, error)
GetRPCClient(preferWS bool) (*rpc.Client, error)
GetStats() map[string]interface{}
Close() error
}
// ReadOnlyProviderPool manages providers optimized for data fetching and real-time events
type ReadOnlyProviderPool struct {
providers []*Provider
config ProviderPoolConfig
providerConfigs map[string]ProviderConfig
currentProvider int
mutex sync.RWMutex
healthTicker *time.Ticker
stopChan chan struct{}
}
// NewReadOnlyProviderPool creates a new read-only provider pool
func NewReadOnlyProviderPool(poolConfig ProviderPoolConfig, providerConfigs map[string]ProviderConfig) (*ReadOnlyProviderPool, error) {
pool := &ReadOnlyProviderPool{
config: poolConfig,
providerConfigs: providerConfigs,
stopChan: make(chan struct{}),
}
if err := pool.initializeProviders(); err != nil {
return nil, fmt.Errorf("failed to initialize read-only providers: %w", err)
}
pool.startHealthChecks()
return pool, nil
}
// initializeProviders sets up all configured providers for read-only operations
func (p *ReadOnlyProviderPool) initializeProviders() error {
p.providers = make([]*Provider, 0, len(p.config.Providers))
for _, providerName := range p.config.Providers {
providerConfig, exists := p.providerConfigs[providerName]
if !exists {
continue // Skip missing providers
}
provider, err := p.createReadOnlyProvider(providerConfig)
if err != nil {
// Log error but continue with other providers
continue
}
p.providers = append(p.providers, provider)
}
if len(p.providers) == 0 {
return fmt.Errorf("no read-only providers successfully initialized")
}
return nil
}
// createReadOnlyProvider creates a provider optimized for read-only operations
func (p *ReadOnlyProviderPool) createReadOnlyProvider(config ProviderConfig) (*Provider, error) {
provider, err := createProvider(config)
if err != nil {
return nil, err
}
// For read-only pool, prefer WebSocket connections when available
if config.WSEndpoint != "" && provider.WSClient == nil {
// Try to establish WebSocket connection again with longer timeout
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
wsClient, err := rpc.DialWebsocket(ctx, config.WSEndpoint, "")
if err == nil {
provider.WSConn = wsClient
provider.WSClient = ethclient.NewClient(wsClient)
}
}
return provider, nil
}
// GetHTTPClient returns an HTTP client optimized for read-only operations
func (p *ReadOnlyProviderPool) GetHTTPClient() (*ethclient.Client, error) {
p.mutex.RLock()
defer p.mutex.RUnlock()
provider, err := p.getHealthyProvider()
if err != nil {
return nil, err
}
if provider.HTTPClient == nil {
return nil, fmt.Errorf("provider %s has no HTTP client", provider.Config.Name)
}
return provider.HTTPClient, nil
}
// GetWSClient returns a WebSocket client for real-time events
func (p *ReadOnlyProviderPool) GetWSClient() (*ethclient.Client, error) {
p.mutex.RLock()
defer p.mutex.RUnlock()
// Prefer providers with WebSocket support based on strategy
if p.config.Strategy == "websocket_preferred" {
for _, provider := range p.providers {
if p.isProviderUsable(provider) && provider.WSClient != nil {
return provider.WSClient, nil
}
}
}
// Fallback to any healthy provider with WebSocket
provider, err := p.getHealthyProvider()
if err != nil {
return nil, err
}
if provider.WSClient == nil {
return nil, fmt.Errorf("provider %s has no WebSocket client", provider.Config.Name)
}
return provider.WSClient, nil
}
// GetRPCClient returns a raw RPC client, preferring WebSocket for real-time operations
func (p *ReadOnlyProviderPool) GetRPCClient(preferWS bool) (*rpc.Client, error) {
p.mutex.RLock()
defer p.mutex.RUnlock()
provider, err := p.getHealthyProvider()
if err != nil {
return nil, err
}
// For read-only operations, prefer WebSocket when available
if (preferWS || p.config.Strategy == "websocket_preferred") && provider.WSConn != nil {
return provider.WSConn, nil
}
if provider.HTTPConn != nil {
return provider.HTTPConn, nil
}
return nil, fmt.Errorf("provider %s has no available RPC client", provider.Config.Name)
}
// getHealthyProvider returns the next healthy provider based on strategy
func (p *ReadOnlyProviderPool) getHealthyProvider() (*Provider, error) {
if len(p.providers) == 0 {
return nil, fmt.Errorf("no providers available")
}
switch p.config.Strategy {
case "websocket_preferred":
return p.getWebSocketPreferredProvider()
case "round_robin":
return p.getRoundRobinProvider()
case "priority_based":
return p.getPriorityProvider()
default:
return p.getWebSocketPreferredProvider()
}
}
// getWebSocketPreferredProvider returns a provider, preferring those with WebSocket support
func (p *ReadOnlyProviderPool) getWebSocketPreferredProvider() (*Provider, error) {
// First try to find a healthy provider with WebSocket
for _, provider := range p.providers {
if p.isProviderUsable(provider) && provider.WSClient != nil {
return provider, nil
}
}
// Fallback to any healthy provider
for _, provider := range p.providers {
if p.isProviderUsable(provider) {
return provider, nil
}
}
return nil, fmt.Errorf("no healthy providers available")
}
// getRoundRobinProvider implements round-robin selection for read-only pool
func (p *ReadOnlyProviderPool) getRoundRobinProvider() (*Provider, error) {
startIndex := p.currentProvider
for i := 0; i < len(p.providers); i++ {
index := (startIndex + i) % len(p.providers)
provider := p.providers[index]
if p.isProviderUsable(provider) {
p.currentProvider = (index + 1) % len(p.providers)
return provider, nil
}
}
return nil, fmt.Errorf("no healthy providers available")
}
// getPriorityProvider returns the highest priority healthy provider
func (p *ReadOnlyProviderPool) getPriorityProvider() (*Provider, error) {
var bestProvider *Provider
highestPriority := int(^uint(0) >> 1) // Max int
for _, provider := range p.providers {
if p.isProviderUsable(provider) && provider.Config.Priority < highestPriority {
bestProvider = provider
highestPriority = provider.Config.Priority
}
}
if bestProvider == nil {
return nil, fmt.Errorf("no healthy providers available")
}
return bestProvider, nil
}
// isProviderUsable checks if a provider is healthy and within rate limits
func (p *ReadOnlyProviderPool) isProviderUsable(provider *Provider) bool {
provider.mutex.RLock()
defer provider.mutex.RUnlock()
// Check health status
if p.config.FailoverEnabled && !provider.IsHealthy {
return false
}
// Check rate limit
if !provider.RateLimiter.Allow() {
return false
}
return true
}
// startHealthChecks starts periodic health checking
func (p *ReadOnlyProviderPool) startHealthChecks() {
interval, err := time.ParseDuration(p.config.HealthCheckInterval)
if err != nil {
interval = time.Minute // Default to 1 minute
}
p.healthTicker = time.NewTicker(interval)
go p.healthCheckLoop()
}
// healthCheckLoop periodically checks provider health
func (p *ReadOnlyProviderPool) healthCheckLoop() {
for {
select {
case <-p.healthTicker.C:
p.performHealthChecks()
case <-p.stopChan:
return
}
}
}
// performHealthChecks checks all providers' health
func (p *ReadOnlyProviderPool) performHealthChecks() {
for _, provider := range p.providers {
go p.checkProviderHealth(provider)
}
}
// checkProviderHealth performs a health check on a single provider
func (p *ReadOnlyProviderPool) checkProviderHealth(provider *Provider) {
performProviderHealthCheck(provider, func(ctx context.Context, provider *Provider) error {
// For read-only pool, test both HTTP and WebSocket if available
if provider.WSClient != nil {
_, err := provider.WSClient.BlockNumber(ctx)
return err
} else if provider.HTTPClient != nil {
_, err := provider.HTTPClient.BlockNumber(ctx)
return err
}
return nil
})
}
// GetStats returns current read-only provider pool statistics
func (p *ReadOnlyProviderPool) GetStats() map[string]interface{} {
p.mutex.RLock()
defer p.mutex.RUnlock()
stats := map[string]interface{}{
"pool_type": "read_only",
"strategy": p.config.Strategy,
"total_providers": len(p.providers),
"providers": make(map[string]interface{}),
}
healthyCount := 0
wsCount := 0
for _, provider := range p.providers {
provider.mutex.RLock()
providerStats := map[string]interface{}{
"name": provider.Config.Name,
"healthy": provider.IsHealthy,
"has_websocket": provider.WSClient != nil,
"last_health_check": provider.LastHealthCheck,
"request_count": provider.RequestCount,
"error_count": provider.ErrorCount,
"avg_response_time": provider.AvgResponseTime,
}
provider.mutex.RUnlock()
stats["providers"].(map[string]interface{})[provider.Config.Name] = providerStats
if provider.IsHealthy {
healthyCount++
}
if provider.WSClient != nil {
wsCount++
}
}
stats["healthy_providers"] = healthyCount
stats["websocket_providers"] = wsCount
return stats
}
// Close shuts down the read-only provider pool
func (p *ReadOnlyProviderPool) Close() error {
close(p.stopChan)
if p.healthTicker != nil {
p.healthTicker.Stop()
}
// Close all connections
for _, provider := range p.providers {
if provider.HTTPConn != nil {
provider.HTTPConn.Close()
}
if provider.WSConn != nil {
provider.WSConn.Close()
}
}
return nil
}
// ExecutionProviderPool manages providers optimized for transaction execution
type ExecutionProviderPool struct {
providers []*Provider
config ProviderPoolConfig
providerConfigs map[string]ProviderConfig
currentProvider int
mutex sync.RWMutex
healthTicker *time.Ticker
stopChan chan struct{}
}
// NewExecutionProviderPool creates a new execution provider pool
func NewExecutionProviderPool(poolConfig ProviderPoolConfig, providerConfigs map[string]ProviderConfig) (*ExecutionProviderPool, error) {
pool := &ExecutionProviderPool{
config: poolConfig,
providerConfigs: providerConfigs,
stopChan: make(chan struct{}),
}
if err := pool.initializeProviders(); err != nil {
return nil, fmt.Errorf("failed to initialize execution providers: %w", err)
}
pool.startHealthChecks()
return pool, nil
}
// initializeProviders sets up all configured providers for execution operations
func (p *ExecutionProviderPool) initializeProviders() error {
p.providers = make([]*Provider, 0, len(p.config.Providers))
for _, providerName := range p.config.Providers {
providerConfig, exists := p.providerConfigs[providerName]
if !exists {
continue // Skip missing providers
}
provider, err := p.createExecutionProvider(providerConfig)
if err != nil {
// Log error but continue with other providers
continue
}
p.providers = append(p.providers, provider)
}
if len(p.providers) == 0 {
return fmt.Errorf("no execution providers successfully initialized")
}
return nil
}
// createExecutionProvider creates a provider optimized for transaction execution
func (p *ExecutionProviderPool) createExecutionProvider(config ProviderConfig) (*Provider, error) {
provider, err := createProvider(config)
if err != nil {
return nil, err
}
// For execution pool, ensure HTTP connection is stable and secure
if provider.HTTPClient == nil {
return nil, fmt.Errorf("execution provider %s must have HTTP client", config.Name)
}
return provider, nil
}
// GetHTTPClient returns an HTTP client optimized for transaction execution
func (p *ExecutionProviderPool) GetHTTPClient() (*ethclient.Client, error) {
p.mutex.RLock()
defer p.mutex.RUnlock()
provider, err := p.getHealthyProvider()
if err != nil {
return nil, err
}
if provider.HTTPClient == nil {
return nil, fmt.Errorf("provider %s has no HTTP client", provider.Config.Name)
}
return provider.HTTPClient, nil
}
// GetWSClient returns a WebSocket client (execution pool typically uses HTTP)
func (p *ExecutionProviderPool) GetWSClient() (*ethclient.Client, error) {
p.mutex.RLock()
defer p.mutex.RUnlock()
provider, err := p.getHealthyProvider()
if err != nil {
return nil, err
}
if provider.WSClient == nil {
return nil, fmt.Errorf("provider %s has no WebSocket client", provider.Config.Name)
}
return provider.WSClient, nil
}
// GetRPCClient returns a raw RPC client, preferring HTTP for execution reliability
func (p *ExecutionProviderPool) GetRPCClient(preferWS bool) (*rpc.Client, error) {
p.mutex.RLock()
defer p.mutex.RUnlock()
provider, err := p.getHealthyProvider()
if err != nil {
return nil, err
}
// For execution pool, prefer HTTP for reliability unless explicitly requesting WebSocket
if preferWS && provider.WSConn != nil {
return provider.WSConn, nil
}
if provider.HTTPConn != nil {
return provider.HTTPConn, nil
}
return nil, fmt.Errorf("provider %s has no available RPC client", provider.Config.Name)
}
// getHealthyProvider returns the next healthy provider based on reliability strategy
func (p *ExecutionProviderPool) getHealthyProvider() (*Provider, error) {
if len(p.providers) == 0 {
return nil, fmt.Errorf("no providers available")
}
switch p.config.Strategy {
case "reliability_first":
return p.getReliabilityFirstProvider()
case "priority_based":
return p.getPriorityProvider()
case "round_robin":
return p.getRoundRobinProvider()
default:
return p.getReliabilityFirstProvider()
}
}
// getReliabilityFirstProvider returns the most reliable provider
func (p *ExecutionProviderPool) getReliabilityFirstProvider() (*Provider, error) {
var bestProvider *Provider
bestReliability := -1.0
for _, provider := range p.providers {
if !p.isProviderUsable(provider) {
continue
}
// Calculate reliability score based on error rate and response time
reliability := p.calculateReliabilityScore(provider)
if reliability > bestReliability {
bestProvider = provider
bestReliability = reliability
}
}
if bestProvider == nil {
return nil, fmt.Errorf("no healthy providers available")
}
return bestProvider, nil
}
// calculateReliabilityScore calculates a reliability score for a provider
func (p *ExecutionProviderPool) calculateReliabilityScore(provider *Provider) float64 {
provider.mutex.RLock()
defer provider.mutex.RUnlock()
// Base score on health status
if !provider.IsHealthy {
return 0.0
}
score := 1.0
// Factor in error rate
if provider.RequestCount > 0 {
errorRate := float64(provider.ErrorCount) / float64(provider.RequestCount)
score *= (1.0 - errorRate)
}
// Factor in response time (prefer faster responses)
if provider.AvgResponseTime > 0 {
// Normalize response time (consider anything under 1 second as good)
responseScore := 1.0 - (float64(provider.AvgResponseTime.Milliseconds()) / 1000.0)
if responseScore < 0 {
responseScore = 0.1 // Minimum score for slow but working providers
}
score *= responseScore
}
return score
}
// getRoundRobinProvider implements round-robin selection for execution pool
func (p *ExecutionProviderPool) getRoundRobinProvider() (*Provider, error) {
startIndex := p.currentProvider
for i := 0; i < len(p.providers); i++ {
index := (startIndex + i) % len(p.providers)
provider := p.providers[index]
if p.isProviderUsable(provider) {
p.currentProvider = (index + 1) % len(p.providers)
return provider, nil
}
}
return nil, fmt.Errorf("no healthy providers available")
}
// getPriorityProvider returns the highest priority healthy provider
func (p *ExecutionProviderPool) getPriorityProvider() (*Provider, error) {
var bestProvider *Provider
highestPriority := int(^uint(0) >> 1) // Max int
for _, provider := range p.providers {
if p.isProviderUsable(provider) && provider.Config.Priority < highestPriority {
bestProvider = provider
highestPriority = provider.Config.Priority
}
}
if bestProvider == nil {
return nil, fmt.Errorf("no healthy providers available")
}
return bestProvider, nil
}
// isProviderUsable checks if a provider is healthy and within rate limits
func (p *ExecutionProviderPool) isProviderUsable(provider *Provider) bool {
provider.mutex.RLock()
defer provider.mutex.RUnlock()
// Check health status (critical for execution)
if !provider.IsHealthy {
return false
}
// Check rate limit
if !provider.RateLimiter.Allow() {
return false
}
return true
}
// startHealthChecks starts periodic health checking for execution pool
func (p *ExecutionProviderPool) startHealthChecks() {
interval, err := time.ParseDuration(p.config.HealthCheckInterval)
if err != nil {
interval = 30 * time.Second // Default to 30 seconds for execution pool
}
p.healthTicker = time.NewTicker(interval)
go p.healthCheckLoop()
}
// healthCheckLoop periodically checks provider health
func (p *ExecutionProviderPool) healthCheckLoop() {
for {
select {
case <-p.healthTicker.C:
p.performHealthChecks()
case <-p.stopChan:
return
}
}
}
// performHealthChecks checks all providers' health
func (p *ExecutionProviderPool) performHealthChecks() {
for _, provider := range p.providers {
go p.checkProviderHealth(provider)
}
}
// checkProviderHealth performs a health check on a single provider
func (p *ExecutionProviderPool) checkProviderHealth(provider *Provider) {
performProviderHealthCheckWithMetrics(provider, func(ctx context.Context, provider *Provider) error {
// For execution pool, test HTTP client reliability
if provider.HTTPClient != nil {
_, err := provider.HTTPClient.BlockNumber(ctx)
return err
}
return nil
})
}
// GetStats returns current execution provider pool statistics
func (p *ExecutionProviderPool) GetStats() map[string]interface{} {
p.mutex.RLock()
defer p.mutex.RUnlock()
stats := map[string]interface{}{
"pool_type": "execution",
"strategy": p.config.Strategy,
"total_providers": len(p.providers),
"providers": make(map[string]interface{}),
}
healthyCount := 0
totalRequests := int64(0)
totalErrors := int64(0)
for _, provider := range p.providers {
provider.mutex.RLock()
reliability := p.calculateReliabilityScore(provider)
providerStats := map[string]interface{}{
"name": provider.Config.Name,
"healthy": provider.IsHealthy,
"reliability_score": reliability,
"last_health_check": provider.LastHealthCheck,
"request_count": provider.RequestCount,
"error_count": provider.ErrorCount,
"avg_response_time": provider.AvgResponseTime,
}
provider.mutex.RUnlock()
stats["providers"].(map[string]interface{})[provider.Config.Name] = providerStats
if provider.IsHealthy {
healthyCount++
}
totalRequests += provider.RequestCount
totalErrors += provider.ErrorCount
}
stats["healthy_providers"] = healthyCount
stats["total_requests"] = totalRequests
stats["total_errors"] = totalErrors
if totalRequests > 0 {
stats["error_rate"] = float64(totalErrors) / float64(totalRequests)
}
return stats
}
// Close shuts down the execution provider pool
func (p *ExecutionProviderPool) Close() error {
close(p.stopChan)
if p.healthTicker != nil {
p.healthTicker.Stop()
}
// Close all connections
for _, provider := range p.providers {
if provider.HTTPConn != nil {
provider.HTTPConn.Close()
}
if provider.WSConn != nil {
provider.WSConn.Close()
}
}
return nil
}
// TestingProviderPool manages Anvil forked instances for testing
type TestingProviderPool struct {
providers []*Provider
config ProviderPoolConfig
providerConfigs map[string]ProviderConfig
currentProvider int
mutex sync.RWMutex
healthTicker *time.Ticker
stopChan chan struct{}
// Anvil-specific management
anvilProcesses map[string]*exec.Cmd
processMutex sync.RWMutex
}
// NewTestingProviderPool creates a new testing provider pool with Anvil support
func NewTestingProviderPool(poolConfig ProviderPoolConfig, providerConfigs map[string]ProviderConfig) (*TestingProviderPool, error) {
pool := &TestingProviderPool{
config: poolConfig,
providerConfigs: providerConfigs,
stopChan: make(chan struct{}),
anvilProcesses: make(map[string]*exec.Cmd),
}
if err := pool.initializeProviders(); err != nil {
return nil, fmt.Errorf("failed to initialize testing providers: %w", err)
}
pool.startHealthChecks()
return pool, nil
}
// initializeProviders sets up all configured providers including Anvil instances
func (p *TestingProviderPool) initializeProviders() error {
p.providers = make([]*Provider, 0, len(p.config.Providers))
for _, providerName := range p.config.Providers {
providerConfig, exists := p.providerConfigs[providerName]
if !exists {
continue // Skip missing providers
}
provider, err := p.createTestingProvider(providerConfig)
if err != nil {
// Log error but continue with other providers
continue
}
p.providers = append(p.providers, provider)
}
if len(p.providers) == 0 {
return fmt.Errorf("no testing providers successfully initialized")
}
return nil
}
// createTestingProvider creates a provider, starting Anvil if needed
func (p *TestingProviderPool) createTestingProvider(config ProviderConfig) (*Provider, error) {
// Start Anvil instance if this is an anvil_fork type
if config.Type == "anvil_fork" {
if err := p.startAnvilInstance(config); err != nil {
return nil, fmt.Errorf("failed to start Anvil instance: %w", err)
}
// Wait for Anvil to start up
time.Sleep(2 * time.Second)
}
provider, err := createProvider(config)
if err != nil {
// If we started an Anvil instance, clean it up
if config.Type == "anvil_fork" {
p.stopAnvilInstance(config.Name)
}
return nil, err
}
return provider, nil
}
// startAnvilInstance starts an Anvil forked instance
func (p *TestingProviderPool) startAnvilInstance(config ProviderConfig) error {
if config.AnvilConfig == nil {
return fmt.Errorf("anvil config is required for anvil_fork provider")
}
p.processMutex.Lock()
defer p.processMutex.Unlock()
// Check if already running
if _, exists := p.anvilProcesses[config.Name]; exists {
return nil // Already running
}
// Build Anvil command
args := []string{
"--fork-url", config.AnvilConfig.ForkURL,
"--port", strconv.Itoa(config.AnvilConfig.Port),
"--chain-id", strconv.Itoa(config.AnvilConfig.ChainID),
"--block-time", strconv.Itoa(config.AnvilConfig.BlockTime),
"--accounts", "10", // Create 10 test accounts
"--balance", "10000", // Each account gets 10,000 ETH
}
if config.AnvilConfig.AutoImpersonate {
args = append(args, "--auto-impersonate")
}
// Add state interval if specified
if config.AnvilConfig.StateInterval > 0 {
args = append(args, "--state-interval", strconv.Itoa(config.AnvilConfig.StateInterval))
}
// Start the Anvil process
cmd := exec.Command("anvil", args...)
cmd.Stdout = nil // Could pipe to logger if needed
cmd.Stderr = nil
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start anvil process: %w", err)
}
p.anvilProcesses[config.Name] = cmd
// Start a goroutine to monitor the process
go p.monitorAnvilProcess(config.Name, cmd)
return nil
}
// stopAnvilInstance stops an Anvil instance
func (p *TestingProviderPool) stopAnvilInstance(name string) error {
p.processMutex.Lock()
defer p.processMutex.Unlock()
cmd, exists := p.anvilProcesses[name]
if !exists {
return nil // Not running
}
// Kill the process
if err := cmd.Process.Kill(); err != nil {
return fmt.Errorf("failed to kill anvil process: %w", err)
}
// Wait for it to exit
cmd.Wait()
// Remove from map
delete(p.anvilProcesses, name)
return nil
}
// monitorAnvilProcess monitors an Anvil process and restarts if needed
func (p *TestingProviderPool) monitorAnvilProcess(name string, cmd *exec.Cmd) {
err := cmd.Wait()
p.processMutex.Lock()
defer p.processMutex.Unlock()
// Remove from map
delete(p.anvilProcesses, name)
// If the process exited unexpectedly, we could restart it here
// For now, just log the exit
if err != nil {
// Process exited with error - could implement restart logic
}
}
// GetHTTPClient returns an HTTP client for testing operations
func (p *TestingProviderPool) GetHTTPClient() (*ethclient.Client, error) {
p.mutex.RLock()
defer p.mutex.RUnlock()
provider, err := p.getHealthyProvider()
if err != nil {
return nil, err
}
if provider.HTTPClient == nil {
return nil, fmt.Errorf("provider %s has no HTTP client", provider.Config.Name)
}
return provider.HTTPClient, nil
}
// GetWSClient returns a WebSocket client for testing
func (p *TestingProviderPool) GetWSClient() (*ethclient.Client, error) {
p.mutex.RLock()
defer p.mutex.RUnlock()
provider, err := p.getHealthyProvider()
if err != nil {
return nil, err
}
if provider.WSClient == nil {
return nil, fmt.Errorf("provider %s has no WebSocket client", provider.Config.Name)
}
return provider.WSClient, nil
}
// GetRPCClient returns a raw RPC client for advanced testing operations
func (p *TestingProviderPool) GetRPCClient(preferWS bool) (*rpc.Client, error) {
p.mutex.RLock()
defer p.mutex.RUnlock()
provider, err := p.getHealthyProvider()
if err != nil {
return nil, err
}
// For testing pool, prefer local connections
if preferWS && provider.WSConn != nil {
return provider.WSConn, nil
}
if provider.HTTPConn != nil {
return provider.HTTPConn, nil
}
return nil, fmt.Errorf("provider %s has no available RPC client", provider.Config.Name)
}
// getHealthyProvider returns the next healthy provider based on anvil strategy
func (p *TestingProviderPool) getHealthyProvider() (*Provider, error) {
if len(p.providers) == 0 {
return nil, fmt.Errorf("no providers available")
}
switch p.config.Strategy {
case "anvil_preferred":
return p.getAnvilPreferredProvider()
case "priority_based":
return p.getPriorityProvider()
case "round_robin":
return p.getRoundRobinProvider()
default:
return p.getAnvilPreferredProvider()
}
}
// getAnvilPreferredProvider returns an Anvil provider if available, otherwise any healthy provider
func (p *TestingProviderPool) getAnvilPreferredProvider() (*Provider, error) {
// First try to find a healthy Anvil provider
for _, provider := range p.providers {
if p.isProviderUsable(provider) && provider.Config.Type == "anvil_fork" {
return provider, nil
}
}
// Fallback to any healthy provider
for _, provider := range p.providers {
if p.isProviderUsable(provider) {
return provider, nil
}
}
return nil, fmt.Errorf("no healthy providers available")
}
// getRoundRobinProvider implements round-robin selection for testing pool
func (p *TestingProviderPool) getRoundRobinProvider() (*Provider, error) {
startIndex := p.currentProvider
for i := 0; i < len(p.providers); i++ {
index := (startIndex + i) % len(p.providers)
provider := p.providers[index]
if p.isProviderUsable(provider) {
p.currentProvider = (index + 1) % len(p.providers)
return provider, nil
}
}
return nil, fmt.Errorf("no healthy providers available")
}
// getPriorityProvider returns the highest priority healthy provider
func (p *TestingProviderPool) getPriorityProvider() (*Provider, error) {
var bestProvider *Provider
highestPriority := int(^uint(0) >> 1) // Max int
for _, provider := range p.providers {
if p.isProviderUsable(provider) && provider.Config.Priority < highestPriority {
bestProvider = provider
highestPriority = provider.Config.Priority
}
}
if bestProvider == nil {
return nil, fmt.Errorf("no healthy providers available")
}
return bestProvider, nil
}
// isProviderUsable checks if a provider is healthy and within rate limits
func (p *TestingProviderPool) isProviderUsable(provider *Provider) bool {
provider.mutex.RLock()
defer provider.mutex.RUnlock()
// For testing providers, be more lenient with health checks
if p.config.FailoverEnabled && !provider.IsHealthy {
// For Anvil instances, check if the process is still running
if provider.Config.Type == "anvil_fork" {
p.processMutex.RLock()
_, isRunning := p.anvilProcesses[provider.Config.Name]
p.processMutex.RUnlock()
return isRunning
}
return false
}
// Check rate limit (testing usually has very high limits)
if !provider.RateLimiter.Allow() {
return false
}
return true
}
// startHealthChecks starts periodic health checking for testing pool
func (p *TestingProviderPool) startHealthChecks() {
interval, err := time.ParseDuration(p.config.HealthCheckInterval)
if err != nil {
interval = time.Minute // Default to 1 minute for testing pool
}
p.healthTicker = time.NewTicker(interval)
go p.healthCheckLoop()
}
// healthCheckLoop periodically checks provider health
func (p *TestingProviderPool) healthCheckLoop() {
for {
select {
case <-p.healthTicker.C:
p.performHealthChecks()
case <-p.stopChan:
return
}
}
}
// performHealthChecks checks all providers' health
func (p *TestingProviderPool) performHealthChecks() {
for _, provider := range p.providers {
go p.checkProviderHealth(provider)
}
}
// checkProviderHealth performs a health check on a single provider
func (p *TestingProviderPool) checkProviderHealth(provider *Provider) {
performProviderHealthCheck(provider, func(ctx context.Context, provider *Provider) error {
// For testing pool, test the most appropriate client
if provider.HTTPClient != nil {
_, err := provider.HTTPClient.BlockNumber(ctx)
return err
} else if provider.WSClient != nil {
_, err := provider.WSClient.BlockNumber(ctx)
return err
}
return nil
})
}
// GetStats returns current testing provider pool statistics
func (p *TestingProviderPool) GetStats() map[string]interface{} {
p.mutex.RLock()
defer p.mutex.RUnlock()
stats := map[string]interface{}{
"pool_type": "testing",
"strategy": p.config.Strategy,
"total_providers": len(p.providers),
"providers": make(map[string]interface{}),
}
healthyCount := 0
anvilCount := 0
runningAnvilCount := 0
p.processMutex.RLock()
defer p.processMutex.RUnlock()
for _, provider := range p.providers {
provider.mutex.RLock()
isAnvil := provider.Config.Type == "anvil_fork"
_, anvilRunning := p.anvilProcesses[provider.Config.Name]
providerStats := map[string]interface{}{
"name": provider.Config.Name,
"type": provider.Config.Type,
"healthy": provider.IsHealthy,
"is_anvil": isAnvil,
"anvil_running": anvilRunning,
"last_health_check": provider.LastHealthCheck,
"request_count": provider.RequestCount,
"error_count": provider.ErrorCount,
"avg_response_time": provider.AvgResponseTime,
}
if isAnvil && provider.Config.AnvilConfig != nil {
providerStats["anvil_port"] = provider.Config.AnvilConfig.Port
providerStats["anvil_chain_id"] = provider.Config.AnvilConfig.ChainID
providerStats["fork_url"] = provider.Config.AnvilConfig.ForkURL
}
provider.mutex.RUnlock()
stats["providers"].(map[string]interface{})[provider.Config.Name] = providerStats
if provider.IsHealthy {
healthyCount++
}
if isAnvil {
anvilCount++
if anvilRunning {
runningAnvilCount++
}
}
}
stats["healthy_providers"] = healthyCount
stats["anvil_providers"] = anvilCount
stats["running_anvil_instances"] = runningAnvilCount
return stats
}
// CreateSnapshot creates a state snapshot for testing (if supported)
func (p *TestingProviderPool) CreateSnapshot() (string, error) {
rpcClient, err := p.GetRPCClient(false)
if err != nil {
return "", err
}
var snapshotID string
err = rpcClient.Call(&snapshotID, "evm_snapshot")
if err != nil {
return "", fmt.Errorf("failed to create snapshot: %w", err)
}
return snapshotID, nil
}
// RevertToSnapshot reverts to a previously created snapshot
func (p *TestingProviderPool) RevertToSnapshot(snapshotID string) error {
rpcClient, err := p.GetRPCClient(false)
if err != nil {
return err
}
var success bool
err = rpcClient.Call(&success, "evm_revert", snapshotID)
if err != nil {
return fmt.Errorf("failed to revert to snapshot: %w", err)
}
if !success {
return fmt.Errorf("snapshot revert was not successful")
}
return nil
}
// Close shuts down the testing provider pool and all Anvil instances
func (p *TestingProviderPool) Close() error {
close(p.stopChan)
if p.healthTicker != nil {
p.healthTicker.Stop()
}
// Stop all Anvil instances
p.processMutex.Lock()
for name := range p.anvilProcesses {
p.stopAnvilInstance(name)
}
p.processMutex.Unlock()
// Close all connections
for _, provider := range p.providers {
if provider.HTTPConn != nil {
provider.HTTPConn.Close()
}
if provider.WSConn != nil {
provider.WSConn.Close()
}
}
return nil
}