package arbitrum import ( "context" "fmt" "os" "strings" "time" "github.com/ethereum/go-ethereum/ethclient" "golang.org/x/time/rate" "github.com/fraktal/mev-beta/internal/config" "github.com/fraktal/mev-beta/internal/logger" ) // RateLimitedClient wraps ethclient.Client with rate limiting and circuit breaker type RateLimitedClient struct { *ethclient.Client limiter *rate.Limiter circuitBreaker *CircuitBreaker logger *logger.Logger } // RateLimitConfig represents the configuration for rate limiting type RateLimitConfig struct { RequestsPerSecond float64 `yaml:"requests_per_second"` MaxConcurrent int `yaml:"max_concurrent"` Burst int `yaml:"burst"` } // NewRateLimitedClient creates a new rate limited client func NewRateLimitedClient(client *ethclient.Client, requestsPerSecond float64, logger *logger.Logger) *RateLimitedClient { // Create a rate limiter limiter := rate.NewLimiter(rate.Limit(requestsPerSecond), int(requestsPerSecond*2)) // Create circuit breaker with default configuration circuitBreakerConfig := &CircuitBreakerConfig{ FailureThreshold: 5, Timeout: 30 * time.Second, SuccessThreshold: 3, } circuitBreaker := NewCircuitBreaker(circuitBreakerConfig) circuitBreaker.SetLogger(logger) return &RateLimitedClient{ Client: client, limiter: limiter, circuitBreaker: circuitBreaker, logger: logger, } } // CallWithRateLimit executes a call with rate limiting and circuit breaker protection func (rlc *RateLimitedClient) CallWithRateLimit(ctx context.Context, call func() error) error { // Check circuit breaker state if rlc.circuitBreaker.GetState() == Open { return fmt.Errorf("circuit breaker is open") } // Wait for rate limiter if err := rlc.limiter.Wait(ctx); err != nil { return fmt.Errorf("rate limiter wait error: %w", err) } // Execute the call through circuit breaker with retry on rate limit errors var lastErr error maxRetries := 3 for attempt := 0; attempt < maxRetries; attempt++ { err := rlc.circuitBreaker.Call(ctx, call) // Check if this is a rate limit error if err != nil && strings.Contains(err.Error(), "RPS limit") { rlc.logger.Warn(fmt.Sprintf("⚠️ RPC rate limit hit (attempt %d/%d), applying exponential backoff", attempt+1, maxRetries)) // Exponential backoff: 1s, 2s, 4s backoffDuration := time.Duration(1< 0 { requestsPerSecond = float64(cm.config.RateLimit.RequestsPerSecond) } cm.logger.Info(fmt.Sprintf("📊 Rate limiting configured: %.1f requests/second", requestsPerSecond)) rateLimitedClient := NewRateLimitedClient(client, requestsPerSecond, cm.logger) return rateLimitedClient, nil } // testConnection tests if a client connection is working func (cm *ConnectionManager) testConnection(ctx context.Context, client *ethclient.Client) error { // Increased timeout from 5s to 15s for production stability testCtx, cancel := context.WithTimeout(ctx, 15*time.Second) defer cancel() // Try to get chain ID as a simple connection test chainID, err := client.ChainID(testCtx) if err != nil { return err } cm.logger.Info(fmt.Sprintf("✅ Connected to chain ID: %s", chainID.String())) return nil } // Close closes all client connections func (cm *ConnectionManager) Close() { if cm.primaryClient != nil { cm.primaryClient.Client.Close() cm.primaryClient = nil } for _, client := range cm.fallbackClients { if client != nil { client.Client.Close() } } cm.fallbackClients = nil } // GetClientWithRetry returns a client with automatic retry on failure func (cm *ConnectionManager) GetClientWithRetry(ctx context.Context, maxRetries int) (*RateLimitedClient, error) { var lastErr error cm.logger.Info(fmt.Sprintf("🔄 Starting connection attempts (max retries: %d)", maxRetries)) for attempt := 0; attempt < maxRetries; attempt++ { cm.logger.Info(fmt.Sprintf("📡 Connection attempt %d/%d", attempt+1, maxRetries)) client, err := cm.GetClient(ctx) if err == nil { cm.logger.Info("✅ Successfully connected to RPC endpoint") return client, nil } lastErr = err cm.logger.Warn(fmt.Sprintf("❌ Connection attempt %d failed: %v", attempt+1, err)) // Wait before retry (exponential backoff with cap at 8 seconds) if attempt < maxRetries-1 { waitTime := time.Duration(1< 8*time.Second { waitTime = 8 * time.Second } cm.logger.Info(fmt.Sprintf("⏳ Waiting %v before retry...", waitTime)) select { case <-ctx.Done(): return nil, fmt.Errorf("context cancelled during retry: %w", ctx.Err()) case <-time.After(waitTime): // Continue to next attempt } } } return nil, fmt.Errorf("failed to connect after %d attempts (last error: %w)", maxRetries, lastErr) } // GetHealthyClient returns a client that passes health checks func GetHealthyClient(ctx context.Context, logger *logger.Logger) (*RateLimitedClient, error) { cfg := &config.ArbitrumConfig{} // Use default config cm := NewConnectionManager(cfg, logger) defer cm.Close() return cm.GetClientWithRetry(ctx, 3) }