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 : }
|