- 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>
1283 lines
34 KiB
Go
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
|
|
}
|