LCOV - code coverage report
Current view: top level - crypto/evm/repositories/etherscan - etherscan_repository.dart (source / functions) Coverage Total Hit
Test: lcov.info Lines: 73.8 % 65 48
Test Date: 2025-07-02 01:23:33 Functions: - 0 0

            Line data    Source code
       1              : import 'dart:convert';
       2              : import 'dart:math';
       3              : import 'package:walletkit_dart/src/common/http_client.dart';
       4              : import 'package:walletkit_dart/src/common/logger.dart';
       5              : 
       6              : class EtherscanRepository {
       7              :   final String baseUrl;
       8              :   final List<String> apiKeys;
       9              :   final Map<String, bool> endpointNeedsApiKey = {};
      10              :   final Map<String, DateTime> apiKeyExcludedUntil = {};
      11              :   final List<String> invalidApiKeys = [];
      12              : 
      13              :   final Duration noApiKeyRetryIntervall;
      14              :   final Duration apiKeyRetryIntervall;
      15              : 
      16            1 :   EtherscanRepository(
      17              :     this.baseUrl,
      18              :     this.apiKeys, {
      19              :     this.noApiKeyRetryIntervall = const Duration(seconds: 5),
      20              :     this.apiKeyRetryIntervall = const Duration(seconds: 3),
      21              :   });
      22              : 
      23            1 :   String? _getRandomApiKey() {
      24            2 :     if (apiKeys.isEmpty) return null;
      25            1 :     final now = DateTime.now();
      26            3 :     final availableKeys = apiKeys.where((key) {
      27            2 :       if (invalidApiKeys.contains(key)) return false;
      28            2 :       final excludedUntil = apiKeyExcludedUntil[key];
      29              : 
      30            0 :       return excludedUntil == null || now.isAfter(excludedUntil);
      31            1 :     }).toList();
      32            1 :     if (availableKeys.isEmpty) return null;
      33            4 :     return availableKeys[Random().nextInt(availableKeys.length)];
      34              :   }
      35              : 
      36            1 :   bool _needsApiKey(String endpoint) {
      37            2 :     return endpointNeedsApiKey[endpoint] ?? false;
      38              :   }
      39              : 
      40            1 :   void _setNeedsApiKey(String endpoint, bool needs) {
      41            2 :     endpointNeedsApiKey[endpoint] = needs;
      42              :   }
      43              : 
      44            0 :   void _excludeApiKey(String apiKey) {
      45            0 :     Logger.log("Excluding API key $apiKey for 1 hour");
      46            0 :     apiKeyExcludedUntil[apiKey] = DateTime.now().add(Duration(hours: 1));
      47              :   }
      48              : 
      49            2 :   Map<String, String> _buildRequestHeaders() => {'Content-Type': 'application/json'};
      50              : 
      51            1 :   String getBaseEtherscanEndpoint(String fullUrl) {
      52            1 :     Uri uri = Uri.parse(fullUrl);
      53              : 
      54              :     // Extract the scheme, host, and path
      55            4 :     String baseUrl = '${uri.scheme}://${uri.host}${uri.path}';
      56              : 
      57              :     // Get the query parameters
      58            1 :     Map<String, String> queryParams = uri.queryParameters;
      59              : 
      60              :     // Check if 'module' and 'action' parameters exist
      61            2 :     if (queryParams.containsKey('module') && queryParams.containsKey('action')) {
      62            1 :       String module = queryParams['module']!;
      63            1 :       String action = queryParams['action']!;
      64              : 
      65              :       // Construct the base endpoint
      66            1 :       return '$baseUrl&module=$module&action=$action';
      67              :     } else {
      68              :       // If 'module' or 'action' is missing, return the original URL
      69              :       return fullUrl;
      70              :     }
      71              :   }
      72              : 
      73            1 :   Future<T> fetchEtherscanWithRatelimitRetries<T>(
      74              :     String rawEndpoint, {
      75              :     int maxRetries = 10,
      76              :   }) async {
      77            1 :     final baseEndpoint = getBaseEtherscanEndpoint(rawEndpoint);
      78              : 
      79              :     bool maybeUseApiKey = false;
      80              : 
      81            2 :     for (var i = 0; i < maxRetries; i++) {
      82              :       String endpoint = rawEndpoint;
      83              :       String? currentApiKey;
      84              : 
      85            1 :       if (_needsApiKey(baseEndpoint)) {
      86              :         maybeUseApiKey = false;
      87            1 :         currentApiKey = _getRandomApiKey();
      88              :         if (currentApiKey == null) {
      89            0 :           Logger.logError("No available API keys");
      90            0 :           throw Exception("No available API keys");
      91              :         }
      92            1 :         endpoint = "$rawEndpoint&apikey=$currentApiKey";
      93              :       } else if (maybeUseApiKey) {
      94              :         maybeUseApiKey = false;
      95            0 :         currentApiKey = _getRandomApiKey();
      96              :         if (currentApiKey != null) {
      97            0 :           endpoint = "$rawEndpoint&apikey=$currentApiKey";
      98              :         }
      99              :       }
     100              : 
     101            2 :       final response = await HTTPService.client.get(
     102            1 :         Uri.parse(endpoint),
     103            1 :         headers: _buildRequestHeaders(),
     104              :       );
     105              : 
     106            2 :       if (response.statusCode == 200) {
     107            2 :         final body = jsonDecode(response.body);
     108            2 :         int status = int.tryParse(body['status'] ?? '') ?? -1;
     109            1 :         final result = body['result'];
     110              : 
     111            1 :         if (status == 1) return result;
     112              : 
     113            1 :         if (status == 0) {
     114            1 :           final result_s = result is String ? result : 'empty';
     115              :           if (result == null) {
     116              :             continue;
     117              :           }
     118              : 
     119            1 :           if (result == "Missing/Invalid API Key") {
     120            1 :             _setNeedsApiKey(baseEndpoint, true);
     121            1 :           } else if (result_s.contains('Invalid API Key')) {
     122            0 :             invalidApiKeys.add(currentApiKey!);
     123            0 :             if (_getRandomApiKey() == null) {
     124            0 :               await Future.delayed(noApiKeyRetryIntervall);
     125              :             } else {
     126              :               maybeUseApiKey = true; // Try again with an API key
     127              :             }
     128            1 :           } else if (result_s.contains("Max daily rate limit")) {
     129              :             if (currentApiKey != null) {
     130            0 :               _excludeApiKey(currentApiKey);
     131              :             }
     132            0 :             if (_getRandomApiKey() == null) {
     133            0 :               await Future.delayed(noApiKeyRetryIntervall);
     134              :             } else {
     135              :               maybeUseApiKey = true; // Try again with an API key
     136              :             }
     137            1 :           } else if (result_s.contains('for higher rate limit')) {
     138            0 :             if (_getRandomApiKey() == null) {
     139            0 :               await Future.delayed(noApiKeyRetryIntervall);
     140              :             } else {
     141              :               maybeUseApiKey = true; // Try again with an API key
     142              :             }
     143            1 :           } else if (result_s.contains("Max calls per sec")) {
     144            2 :             await Future.delayed(apiKeyRetryIntervall);
     145              :           } else {
     146            1 :             String message = body['message'];
     147            1 :             if (message != "NOTOK") return result;
     148              :           }
     149              :         }
     150              :       }
     151              :     }
     152              : 
     153            0 :     throw Exception("Failed to fetch $rawEndpoint after $maxRetries retries");
     154              :   }
     155              : }
        

Generated by: LCOV version 2.0-1