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