package etherscan import ( "context" "fmt" "github.com/nanmu42/etherscan-api" "github.com/synapsecns/sanguine/core/config" "net/http" "os" "path/filepath" "strconv" "strings" "time" ) // Client implements an etherscan client. type Client struct { *etherscan.Client // rateLimiter is a file based rate limiter rateLimiter *fileRateLimiter } // timeout is the http timeout for a request. const timeout = time.Second * 30 // newEtherscanABIClient creates a new etherscan client. func newEtherscanABIClient(parentCtx context.Context, chainID uint32, url string, disableRateLimiter bool) (*Client, error) { var client Client ctx, cancel := context.WithCancel(parentCtx) // Try chain-specific key first, fall back to generic ETHERSCAN_KEY apiKeyEnv := strings.ToUpper(fmt.Sprintf("%d_KEY", chainID)) apiKey := os.Getenv(apiKeyEnv) if apiKey == "" { apiKey = os.Getenv("ETHERSCAN_KEY") } customization := etherscan.Customization{ Client: &http.Client{ Timeout: timeout, }, BaseURL: url, Key: apiKey, Verbose: true, // Enable verbose logging } // waitBetweenRequest is how long to wait between requests. If an analytics key is enabled, rate limiting is disabled waitBetweenRequests := time.Second * 5 rateLimiterEnabled := apiKey == "" && !disableRateLimiter if rateLimiterEnabled { configDir, err := config.GetConfigDir() if err != nil { cancel() return nil, fmt.Errorf("could not create file rate limiter: %w", err) } rateLimitDir := filepath.Join(configDir, strconv.Itoa(int(chainID))) client.rateLimiter, err = newFileRateLimiter(ctx, rateLimitDir, waitBetweenRequests) if err != nil { cancel() return nil, fmt.Errorf("could not create file rate limiter: %w", err) } customization.BeforeRequest = func(_, _ string, _ map[string]interface{}) error { _, err := client.rateLimiter.obtainLock(ctx) return err } customization.AfterRequest = func(_, action string, _ map[string]interface{}, _ interface{}, _ error) { _, err = client.rateLimiter.releaseLock() if err != nil { logger.Error(err) cancel() } } } else { // context cancellation is handled by the parent _ = cancel } client.Client = etherscan.NewCustomized(customization) return &client, nil }