LCOV - code coverage report
Current view: top level - crypto/evm/repositories/etherscan - etherscan_repository.dart (source / functions) Coverage Total Hit
Test: lcov.info Lines: 74.6 % 67 50
Test Date: 2025-01-30 01:10:00 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 base;
       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.base,
      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            1 :   Map<String, String> _buildRequestHeaders() =>
      50            1 :       {'Content-Type': 'application/json'};
      51              : 
      52            1 :   String getBaseEtherscanEndpoint(String fullUrl) {
      53            1 :     Uri uri = Uri.parse(fullUrl);
      54              : 
      55              :     // Extract the scheme, host, and path
      56            4 :     String baseUrl = '${uri.scheme}://${uri.host}${uri.path}';
      57              : 
      58              :     // Get the query parameters
      59            1 :     Map<String, String> queryParams = uri.queryParameters;
      60              : 
      61              :     // Check if 'module' and 'action' parameters exist
      62            1 :     if (queryParams.containsKey('module') &&
      63            1 :         queryParams.containsKey('action')) {
      64            1 :       String module = queryParams['module']!;
      65            1 :       String action = queryParams['action']!;
      66              : 
      67              :       // Construct the base endpoint
      68            1 :       return '$baseUrl&module=$module&action=$action';
      69              :     } else {
      70              :       // If 'module' or 'action' is missing, return the original URL
      71              :       return fullUrl;
      72              :     }
      73              :   }
      74              : 
      75            1 :   Future<T> fetchEtherscanWithRatelimitRetries<T>(
      76              :     String rawEndpoint, {
      77              :     int maxRetries = 10,
      78              :   }) async {
      79            1 :     final baseEndpoint = getBaseEtherscanEndpoint(rawEndpoint);
      80              : 
      81              :     bool maybeUseApiKey = false;
      82              : 
      83            2 :     for (var i = 0; i < maxRetries; i++) {
      84              :       String endpoint = rawEndpoint;
      85              :       String? currentApiKey;
      86              : 
      87            1 :       if (_needsApiKey(baseEndpoint)) {
      88              :         maybeUseApiKey = false;
      89            1 :         currentApiKey = _getRandomApiKey();
      90              :         if (currentApiKey == null) {
      91            0 :           Logger.logError("No available API keys");
      92            0 :           throw Exception("No available API keys");
      93              :         }
      94            1 :         endpoint = "$rawEndpoint&apikey=$currentApiKey";
      95              :       } else if (maybeUseApiKey) {
      96              :         maybeUseApiKey = false;
      97            0 :         currentApiKey = _getRandomApiKey();
      98              :         if (currentApiKey != null) {
      99            0 :           endpoint = "$rawEndpoint&apikey=$currentApiKey";
     100              :         }
     101              :       }
     102              : 
     103            2 :       final response = await HTTPService.client.get(
     104            1 :         Uri.parse(endpoint),
     105            1 :         headers: _buildRequestHeaders(),
     106              :       );
     107              : 
     108            2 :       if (response.statusCode == 200) {
     109            2 :         final body = jsonDecode(response.body);
     110            2 :         int status = int.tryParse(body['status'] ?? '') ?? -1;
     111            1 :         final result = body['result'];
     112              : 
     113            1 :         if (status == 1) return result;
     114              : 
     115            1 :         if (status == 0) {
     116            1 :           final result_s = result is String ? result : 'empty';
     117              :           if (result == null) {
     118              :             continue;
     119              :           }
     120              : 
     121            1 :           if (result == "Missing/Invalid API Key") {
     122            1 :             _setNeedsApiKey(baseEndpoint, true);
     123            1 :           } else if (result_s.contains('Invalid API Key')) {
     124            0 :             invalidApiKeys.add(currentApiKey!);
     125            0 :             if (_getRandomApiKey() == null) {
     126            0 :               await Future.delayed(noApiKeyRetryIntervall);
     127              :             } else {
     128              :               maybeUseApiKey = true; // Try again with an API key
     129              :             }
     130            1 :           } else if (result_s.contains("Max daily rate limit")) {
     131              :             if (currentApiKey != null) {
     132            0 :               _excludeApiKey(currentApiKey);
     133              :             }
     134            0 :             if (_getRandomApiKey() == null) {
     135            0 :               await Future.delayed(noApiKeyRetryIntervall);
     136              :             } else {
     137              :               maybeUseApiKey = true; // Try again with an API key
     138              :             }
     139            1 :           } else if (result_s.contains('for higher rate limit')) {
     140            0 :             if (_getRandomApiKey() == null) {
     141            0 :               await Future.delayed(noApiKeyRetryIntervall);
     142              :             } else {
     143              :               maybeUseApiKey = true; // Try again with an API key
     144              :             }
     145            1 :           } else if (result_s.contains("Max calls per sec")) {
     146            2 :             await Future.delayed(apiKeyRetryIntervall);
     147              :           } else {
     148            1 :             String message = body['message'];
     149            1 :             if (message != "NOTOK") return result;
     150              :           }
     151              :         }
     152              :       }
     153              :     }
     154              : 
     155            0 :     throw Exception("Failed to fetch $rawEndpoint after $maxRetries retries");
     156              :   }
     157              : }
        

Generated by: LCOV version 2.0-1