Line data Source code
1 : import 'package:walletkit_dart/src/common/types.dart';
2 : import 'package:walletkit_dart/src/crypto/evm/entities/transactions/etherscan_transaction.dart';
3 : import 'package:walletkit_dart/src/crypto/evm/repositories/etherscan/etherscan_repository.dart';
4 : import 'package:walletkit_dart/src/domain/entities/amount.dart';
5 : import 'package:walletkit_dart/src/domain/entities/coin_entity.dart';
6 : import 'package:walletkit_dart/src/domain/entities/tx_gasFee_entity.dart';
7 :
8 : enum Sorting { asc, desc }
9 :
10 : class EtherscanExplorer extends EtherscanRepository {
11 : final EvmCoinEntity currency;
12 :
13 1 : EtherscanExplorer(super.base, super.apiKeys, this.currency);
14 :
15 1 : @override
16 4 : String get base => "${super.base}?chainid=${currency.chainID}";
17 :
18 1 : String buildBalanceEndpoint(String address) =>
19 1 : "$base&module=account&action=balance&address=$address"
20 2 : .addOptionalParameter('tag', 'latest');
21 :
22 1 : String buildTokenBalanceEndpoint(String address, String contractAddress) =>
23 1 : "$base&module=account&action=tokenbalance&address=$address&contractaddress=$contractAddress"
24 2 : .addOptionalParameter('tag', 'latest');
25 :
26 1 : String buildTransactionEndpoint({
27 : required String address,
28 : int? startblock,
29 : int? endblock,
30 : int? page,
31 : int? offset,
32 : Sorting? sorting,
33 : }) =>
34 1 : "$base&module=account&action=txlist&address=$address"
35 2 : .addOptionalParameter('startblock', startblock)
36 1 : .addOptionalParameter('endblock', endblock)
37 1 : .addOptionalParameter('page', page)
38 1 : .addOptionalParameter('offset', offset)
39 1 : .addOptionalParameter('sort', sorting?.name);
40 :
41 1 : String buildERC20TransactionEndpoint({
42 : required String address,
43 : required String contractAddress,
44 : int? startblock,
45 : int? endblock,
46 : int? page,
47 : int? offset,
48 : Sorting? sorting,
49 : }) =>
50 1 : "$base&module=account&action=tokentx&address=$address&contractaddress=$contractAddress"
51 2 : .addOptionalParameter('startblock', startblock)
52 1 : .addOptionalParameter('endblock', endblock)
53 1 : .addOptionalParameter('page', page)
54 1 : .addOptionalParameter('offset', offset)
55 1 : .addOptionalParameter('sort', sorting?.name);
56 :
57 1 : String buildERC721TransactionEndpoint({
58 : required String address,
59 : String? contractAddress,
60 : int? startblock,
61 : int? endblock,
62 : int? page,
63 : int? offset,
64 : Sorting? sorting,
65 : }) =>
66 1 : "$base&module=account&action=tokennfttx&address=$address"
67 2 : .addOptionalParameter('contractaddress', contractAddress)
68 1 : .addOptionalParameter('startblock', startblock)
69 1 : .addOptionalParameter('endblock', endblock)
70 1 : .addOptionalParameter('page', page)
71 1 : .addOptionalParameter('offset', offset)
72 1 : .addOptionalParameter('sort', sorting?.name);
73 :
74 0 : String buildERC1155TransactionEndpoint({
75 : required String address,
76 : required String contractAddress,
77 : int? startblock,
78 : int? endblock,
79 : int? page,
80 : int? offset,
81 : Sorting? sorting,
82 : }) =>
83 0 : "$base&module=account&action=token1155tx&contractaddress=$contractAddress&address=$address"
84 0 : .addOptionalParameter('page', page)
85 0 : .addOptionalParameter('offset', offset)
86 0 : .addOptionalParameter('startblock', startblock)
87 0 : .addOptionalParameter('endblock', endblock)
88 0 : .addOptionalParameter('sort', sorting?.name);
89 :
90 : ///
91 : /// Fetch all Transactions for the given [token] on the given [address]
92 : ///
93 1 : Future<List<EtherscanTransaction>> fetchTransactions({
94 : required String address,
95 : int? startblock,
96 : int? endblock,
97 : int? page,
98 : int? offset,
99 : Sorting? sorting,
100 : }) async {
101 1 : final endpoint = buildTransactionEndpoint(
102 : address: address,
103 : startblock: startblock,
104 : endblock: endblock,
105 : page: page,
106 : offset: offset,
107 : sorting: sorting,
108 : );
109 :
110 1 : final txResults = await fetchEtherscanWithRatelimitRetries(endpoint);
111 1 : return [
112 1 : for (final tx in txResults)
113 1 : EtherscanTransaction.fromJson(
114 : tx,
115 1 : token: currency,
116 : address: address,
117 : ),
118 : ];
119 : }
120 :
121 1 : Future<List<EtherscanTransaction>> fetchERC1155Transactions({
122 : required String contractAddress,
123 : required String address,
124 : int? startblock,
125 : int? endblock,
126 : int? page,
127 : int? offset,
128 : Sorting? sorting,
129 : }) async {
130 1 : final endpoint = buildERC1155TransactionEndpoint(
131 : address: address,
132 : contractAddress: contractAddress,
133 : startblock: startblock,
134 : endblock: endblock,
135 : page: page,
136 : offset: offset,
137 : sorting: sorting,
138 : );
139 1 : final txResults = await fetchEtherscanWithRatelimitRetries(endpoint);
140 :
141 1 : return [
142 1 : for (final tx in txResults)
143 1 : EtherscanTransaction.fromJsonErc1155(
144 : tx,
145 : address: address,
146 1 : currency: currency,
147 : ),
148 : ];
149 : }
150 :
151 : ///
152 : /// Fetch all ERC20 Transactions for a given [token] and [address]
153 : ///
154 1 : Future<List<EtherscanTransaction>> fetchERC20Transactions({
155 : required String contractAddress,
156 : required String address,
157 : int? startblock,
158 : int? endblock,
159 : int? page,
160 : int? offset,
161 : Sorting? sorting,
162 : }) async {
163 1 : final endpoint = buildERC20TransactionEndpoint(
164 : address: address,
165 : contractAddress: contractAddress,
166 : startblock: startblock,
167 : endblock: endblock,
168 : page: page,
169 : offset: offset,
170 : sorting: sorting,
171 : );
172 :
173 1 : final txResults = await fetchEtherscanWithRatelimitRetries(endpoint);
174 1 : return [
175 1 : for (final tx in txResults)
176 1 : EtherscanTransaction.fromJsonErc20(
177 : tx,
178 : address: address,
179 1 : currency: currency,
180 : )
181 : ];
182 : }
183 :
184 1 : Future<BigInt> fetchBalance({
185 : required String address,
186 : }) async {
187 1 : final endpoint = buildBalanceEndpoint(address);
188 1 : final result = await fetchEtherscanWithRatelimitRetries<String>(endpoint);
189 :
190 1 : final balance = BigInt.tryParse(result);
191 :
192 : if (balance == null) {
193 0 : throw Exception('Failed to parse balance: $result');
194 : }
195 :
196 : return balance;
197 : }
198 :
199 1 : Future<BigInt> fetchTokenBalance({
200 : required String address,
201 : required String contractAddress,
202 : }) async {
203 1 : final endpoint = buildTokenBalanceEndpoint(address, contractAddress);
204 1 : final result = await fetchEtherscanWithRatelimitRetries<String>(endpoint);
205 :
206 1 : final balance = BigInt.tryParse(result);
207 :
208 : if (balance == null) {
209 0 : throw Exception('Failed to parse balance: $result');
210 : }
211 :
212 : return balance;
213 : }
214 :
215 : ///
216 : /// Fetch the Balance of a [token] given for a given [address]
217 : ///
218 0 : Future<Amount> fetchBalanceForToken(
219 : String address,
220 : CoinEntity token,
221 : ) async =>
222 : switch (token) {
223 0 : ERC20Entity erc20 => fetchTokenBalance(
224 : address: address,
225 0 : contractAddress: erc20.contractAddress,
226 0 : ).then((balance) => Amount(value: balance, decimals: erc20.decimals)),
227 0 : CoinEntity coin => fetchBalance(
228 : address: address,
229 0 : ).then((balance) => Amount(value: balance, decimals: coin.decimals)),
230 : };
231 :
232 : ///
233 : /// Fetch a list of all ERC721 Tokens for a given [address]
234 : ///
235 1 : Future<List<EtherscanTransaction>> fetchERC721Transactions({
236 : required String address,
237 : String? contractAddress,
238 : int? startblock,
239 : int? endblock,
240 : int? page,
241 : int? offset,
242 : Sorting? sorting,
243 : }) async {
244 1 : final endpoint = buildERC721TransactionEndpoint(
245 : address: address,
246 : contractAddress: contractAddress,
247 : startblock: startblock,
248 : endblock: endblock,
249 : page: page,
250 : offset: offset,
251 : sorting: sorting,
252 : );
253 :
254 : final result =
255 1 : await fetchEtherscanWithRatelimitRetries(endpoint) as List<dynamic>;
256 :
257 1 : return [
258 1 : for (final tx in result)
259 1 : EtherscanTransaction.fromJsonErc721(
260 : tx,
261 1 : currency: currency,
262 : address: address,
263 : ),
264 : ];
265 : }
266 :
267 : ///
268 : /// Fetch Gas Prices
269 : ///
270 1 : Future<EvmNetworkFees> fetchGasPrice() async {
271 2 : final gasOracleEndpoint = "${base}&module=gastracker&action=gasoracle";
272 :
273 1 : final result = await fetchEtherscanWithRatelimitRetries(gasOracleEndpoint);
274 1 : if (result is! Json) {
275 0 : throw Exception("Failed to fetch gas price");
276 : }
277 1 : final entity = EvmNetworkFees.fromJson(result);
278 :
279 : return entity;
280 : }
281 :
282 0 : Future<int?> fetchEstimatedTime(int gasPrice) async {
283 : final endpoint =
284 0 : "$base&module=gastracker&action=gasestimate&gasprice=$gasPrice";
285 0 : final result = await fetchEtherscanWithRatelimitRetries(endpoint);
286 0 : if (result is! String) {
287 0 : throw Exception("Failed to fetch gas price");
288 : }
289 0 : return int.tryParse(result);
290 : }
291 : }
292 :
293 0 : Future<List<T>> batchFutures<T>(
294 : Iterable<Future<T>> futures, {
295 : int batchSize = 10,
296 : }) async {
297 0 : final results = <T>[];
298 0 : final batches = [
299 0 : for (var i = 0; i < futures.length; i += batchSize)
300 0 : futures.skip(i).take(batchSize)
301 : ];
302 :
303 0 : for (final batch in batches) {
304 0 : final batchResults = await Future.wait(batch);
305 0 : results.addAll(batchResults);
306 : }
307 :
308 : return results;
309 : }
310 :
311 : extension URLBuilder on String {
312 1 : String addOptionalParameter(String key, dynamic value) {
313 : if (value == null) {
314 : return this;
315 : }
316 1 : return "$this&$key=$value";
317 : }
318 : }
319 :
320 : class ZeniqScanExplorer extends EtherscanExplorer {
321 1 : ZeniqScanExplorer(super.base, super.apiKeys, super.currency);
322 :
323 1 : @override
324 : String buildTransactionEndpoint({
325 : required String address,
326 : int? startblock,
327 : int? endblock,
328 : int? page,
329 : int? offset,
330 : Sorting? sorting,
331 : }) {
332 1 : return "$base&module=account&action=txlist&address=$address"
333 2 : .addOptionalParameter('start_block', startblock)
334 1 : .addOptionalParameter('end_block', endblock)
335 1 : .addOptionalParameter('page', page)
336 1 : .addOptionalParameter('offset', offset)
337 1 : .addOptionalParameter('sort', sorting?.name);
338 : }
339 :
340 1 : @override
341 : String buildERC20TransactionEndpoint({
342 : required String address,
343 : required String contractAddress,
344 : int? startblock,
345 : int? endblock,
346 : int? page,
347 : int? offset,
348 : Sorting? sorting,
349 : }) {
350 1 : return "$base&module=account&action=tokentx&address=$address&contractaddress=$contractAddress"
351 2 : .addOptionalParameter('start_block', startblock)
352 1 : .addOptionalParameter('end_block', endblock)
353 1 : .addOptionalParameter('page', page)
354 1 : .addOptionalParameter('offset', offset)
355 1 : .addOptionalParameter('sort', sorting?.name);
356 : }
357 :
358 1 : @override
359 : String buildERC1155TransactionEndpoint({
360 : required String address,
361 : required String contractAddress,
362 : int? startblock,
363 : int? endblock,
364 : int? page,
365 : int? offset,
366 : Sorting? sorting,
367 : }) {
368 1 : return "$base&module=account&action=tokentx&address=$address&contractaddress=$contractAddress"
369 2 : .addOptionalParameter('start_block', startblock)
370 1 : .addOptionalParameter('end_block', endblock)
371 1 : .addOptionalParameter('page', page)
372 1 : .addOptionalParameter('offset', offset)
373 1 : .addOptionalParameter('sort', sorting?.name);
374 : }
375 : }
|