dev
dart 803 lines 25.8 KB
Raw
1 import 'dart:async';
2 import 'dart:convert';
3 import 'dart:developer';
4
5 import 'package:cw_core/amount/money.dart';
6 import 'package:cw_core/crypto_currency.dart';
7 import 'package:cw_core/erc20_token.dart';
8 import 'package:cw_core/node.dart';
9 import 'package:cw_core/utils/print_verbose.dart';
10 import 'package:cw_core/utils/proxy_wrapper.dart';
11 import 'package:cw_evm/evm_chain_transaction_model.dart';
12 import 'package:cw_evm/evm_chain_transaction_priority.dart';
13 import 'package:cw_evm/evm_erc20_balance.dart';
14 import 'package:cw_evm/pending_evm_chain_transaction.dart';
15 import 'package:cw_evm/.secrets.g.dart' as secrets;
16 import 'package:cw_evm/utils/evm_chain_utils.dart';
17 import 'package:flutter/foundation.dart';
18 import 'package:hex/hex.dart' as hex;
19 import 'package:web3dart/web3dart.dart';
20
21 import '../contract/erc20.dart';
22
23 class EVMChainClient {
24 late final client = ProxyWrapper().getHttpIOClient();
25 Web3Client? _client;
26 final int _chainId;
27
28 EVMChainClient({required int chainId}) : _chainId = chainId;
29
30 //! Can be overridden by child classes
31
32 int get chainId => _chainId;
33
34 Future<List<EVMChainTransactionModel>> fetchTransactions(String address,
35 {String? contractAddress}) async {
36 try {
37 if (secrets.etherScanApiKey.isEmpty) {
38 printV('Etherscan API key is empty, cannot fetch transactions');
39 return [];
40 }
41
42 /// when adding new chains, make sure they are supported by the same api through https://docs.etherscan.io/supported-chains
43 final response = await client.get(Uri.https("api.etherscan.io", "/v2/api", {
44 "chainid": "$chainId",
45 "module": "account",
46 "action": contractAddress != null ? "tokentx" : "txlist",
47 if (contractAddress != null) "contractaddress": contractAddress,
48 "address": address,
49 "apikey": secrets.etherScanApiKey,
50 }));
51
52 final jsonResponse = json.decode(response.body) as Map<String, dynamic>;
53
54 if (jsonResponse['result'] is String) {
55 log(jsonResponse['result']);
56 return [];
57 }
58
59 if (response.statusCode >= 200 && response.statusCode < 300 && jsonResponse['status'] != 0) {
60 final res = (jsonResponse['result'] as List);
61 res.removeWhere((e) => e['value'] == '0');
62
63 // Filter out spam native transactions below 0.00001 ETH (10000000000000 wei)
64 if (contractAddress == null) {
65 final spamThresholdWei = BigInt.from(10000000000000);
66 res.removeWhere((e) {
67 try {
68 final value = BigInt.parse(e['value'] ?? '0');
69 final isIncoming = e['to']?.toLowerCase() == address.toLowerCase() &&
70 e['from']?.toLowerCase() != address.toLowerCase();
71 return isIncoming && value < spamThresholdWei;
72 } catch (_) {
73 return false;
74 }
75 });
76 }
77
78 // Merge split transfers (same hash + same token)
79 final Map<String, Map<String, dynamic>> mergedMap = {};
80 for (var tx in res) {
81 final hash = tx['hash'];
82 final key = '${hash}_${tx['contractAddress'] ?? ''}';
83
84 if (mergedMap.containsKey(key)) {
85 try {
86 final currentNet = getNetFlow(mergedMap[key]!, address);
87 final newNet = getNetFlow(tx, address);
88 final totalNet = currentNet + newNet;
89
90 mergedMap[key]!['value'] = totalNet.abs().toString();
91 if (totalNet < BigInt.zero) {
92 mergedMap[key]!['from'] = address;
93 } else {
94 mergedMap[key]!['to'] = address;
95 mergedMap[key]!['from'] = '';
96 }
97 } catch (e) {
98 printV('Error merging transaction values: $e');
99 }
100 } else {
101 mergedMap[key] = Map<String, dynamic>.from(tx);
102 }
103 }
104
105 final mergedList = mergedMap.values.toList();
106
107 final symbol = EVMChainUtils.getFeeCurrency(chainId);
108
109 return mergedList
110 .map((e) => EVMChainTransactionModel.fromJson(e, symbol, chainId))
111 .toList();
112 }
113
114 return [];
115 } catch (e) {
116 log(e.toString());
117 return [];
118 }
119 }
120
121 BigInt getNetFlow(Map<String, dynamic> txData, String address) {
122 final val = BigInt.parse(txData['value'] ?? '0');
123 final isIncoming = txData['to']?.toLowerCase() == address.toLowerCase();
124 final isOutgoing = txData['from']?.toLowerCase() == address.toLowerCase();
125
126 if (isIncoming && !isOutgoing) return val;
127 if (isOutgoing && !isIncoming) return -val;
128 return BigInt.zero;
129 }
130
131 Future<List<EVMChainTransactionModel>> fetchInternalTransactions(String address) async {
132 try {
133 if (secrets.etherScanApiKey.isEmpty) {
134 printV('Etherscan API key is empty, cannot fetch internal transactions');
135 return [];
136 }
137
138 final response = await client.get(Uri.https("api.etherscan.io", "/v2/api", {
139 "chainid": "$chainId",
140 "module": "account",
141 "action": "txlistinternal",
142 "address": address,
143 "apikey": secrets.etherScanApiKey,
144 }));
145
146 final jsonResponse = json.decode(response.body) as Map<String, dynamic>;
147
148 if (response.statusCode >= 200 &&
149 response.statusCode < 300 &&
150 jsonResponse['status'] != 0 &&
151 jsonResponse['result'] is List) {
152 final symbol = EVMChainUtils.getFeeCurrency(chainId);
153
154 return (jsonResponse['result'] as List)
155 .map((e) =>
156 EVMChainTransactionModel.fromJson(e as Map<String, dynamic>, symbol, chainId))
157 .toList();
158 }
159
160 printV(
161 'Etherscan API returned invalid response for internal transactions: status=${jsonResponse['status']}, statusCode=${response.statusCode}');
162 return [];
163 } catch (e, stackTrace) {
164 printV('Error fetching internal transactions: ${e.toString()}');
165 printV('Stack trace: ${stackTrace.toString()}');
166 return [];
167 }
168 }
169
170 Uint8List prepareSignedTransactionForSending(Uint8List signedTransaction) => signedTransaction;
171
172 //! Common methods across all child classes
173
174 bool connect(Node node) {
175 try {
176 Uri? rpcUri;
177 bool isModifiedNodeUri = false;
178
179 if (node.uriRaw.contains('nownodes.io')) {
180 isModifiedNodeUri = true;
181 String nowNodeApiKey = secrets.nowNodesApiKey;
182
183 if (nowNodeApiKey.isEmpty) {
184 printV('NowNodes API key is empty, cannot connect to ${node.uriRaw}');
185 return false;
186 }
187
188 rpcUri = Uri.https(node.uriRaw, '/$nowNodeApiKey');
189 }
190
191 _client = Web3Client(isModifiedNodeUri ? rpcUri!.toString() : node.uri.toString(), client);
192
193 return true;
194 } catch (e) {
195 printV('Error connecting to node ${node.uriRaw}: ${e.toString()}');
196 return false;
197 }
198 }
199
200 void setListeners(EthereumAddress userAddress, Function() onNewTransaction) async {
201 // _client?.pendingTransactions().listen((transactionHash) async {
202 // final transaction = await _client!.getTransactionByHash(transactionHash);
203 //
204 // if (transaction.from.hex == userAddress || transaction.to?.hex == userAddress) {
205 // onNewTransaction();
206 // }
207 // });
208 }
209
210 Future<EtherAmount> getBalance(EthereumAddress address) async {
211 try {
212 return await _client!.getBalance(address);
213 } catch (_) {
214 rethrow;
215 }
216 }
217
218 Future<int> getGasUnitPrice() async {
219 try {
220 final gasPrice = await _client!.getGasPrice();
221
222 return gasPrice.getInWei.toInt();
223 } catch (e) {
224 printV('Error getting gas unit price: ${e.toString()}');
225 rethrow;
226 }
227 }
228
229 Future<int?> getGasBaseFee() async {
230 try {
231 final blockInfo = await _client!.getBlockInformation(isContainFullObj: false);
232 final baseFee = blockInfo.baseFeePerGas;
233
234 return baseFee?.getInWei.toInt();
235 } catch (e) {
236 printV('Error getting gas base fee: ${e.toString()}');
237 return null;
238 }
239 }
240
241 Future<int> getEstimatedGasUnitsForTransaction({
242 required EthereumAddress toAddress,
243 required EthereumAddress senderAddress,
244 required EtherAmount value,
245 String? contractAddress,
246 EtherAmount? gasPrice,
247 EtherAmount? maxFeePerGas,
248 Uint8List? data,
249 }) async {
250 try {
251 if (contractAddress == null) {
252 final estimatedGas = await _client!.estimateGas(
253 sender: senderAddress,
254 to: toAddress,
255 value: value,
256 data: data,
257 );
258
259 return estimatedGas.toInt();
260 } else {
261 final contract = DeployedContract(
262 ethereumContractAbi,
263 EthereumAddress.fromHex(contractAddress),
264 );
265
266 final transfer = contract.function('transfer');
267
268 // Estimate gas units
269 final gasEstimate = await _client!.estimateGas(
270 sender: senderAddress,
271 to: EthereumAddress.fromHex(contractAddress),
272 data: data ??
273 transfer.encodeCall([
274 toAddress,
275 value.getInWei,
276 ]),
277 );
278
279 return gasEstimate.toInt();
280 }
281 } catch (_) {
282 return 0;
283 }
284 }
285
286 Uint8List getEncodedDataForApprovalTransaction({
287 required EthereumAddress toAddress,
288 required EtherAmount value,
289 required EthereumAddress contractAddress,
290 }) {
291 final contract = DeployedContract(ethereumContractAbi, contractAddress);
292
293 final approve = contract.function('approve');
294
295 return approve.encodeCall([
296 toAddress,
297 value.getInWei,
298 ]);
299 }
300
301 Future<PendingEVMChainTransaction> signTransaction({
302 required Credentials privateKey,
303 required String toAddress,
304 required Money amount,
305 required Money gasFee,
306 required int estimatedGasUnits,
307 required int maxFeePerGas,
308 required EVMChainTransactionPriority? priority,
309 required CryptoCurrency currency,
310 required String feeCurrency,
311 String? contractAddress,
312 String? data,
313 int? gasPrice,
314 bool useBlinkProtection = true,
315 }) async {
316 assert(currency == CryptoCurrency.eth ||
317 currency == CryptoCurrency.maticpoly ||
318 currency == CryptoCurrency.baseEth ||
319 currency == CryptoCurrency.arbEth ||
320 currency == CryptoCurrency.bnb ||
321 contractAddress != null);
322
323 final isNativeToken = [
324 CryptoCurrency.eth,
325 CryptoCurrency.maticpoly,
326 CryptoCurrency.baseEth,
327 CryptoCurrency.arbEth,
328 CryptoCurrency.bnb
329 ].contains(currency);
330
331 // Get nonce with "pending" block tag to include pending transactions
332 // This prevents "Nonce too low" errors when sending multiple transactions quickly
333 final nonce = await _client!.getTransactionCount(
334 privateKey.address,
335 atBlock: const BlockNum.pending(),
336 );
337
338 final Transaction transaction = createTransaction(
339 from: privateKey.address,
340 to: EthereumAddress.fromHex(toAddress),
341 maxPriorityFeePerGas:
342 priority != null ? EtherAmount.fromInt(EtherUnit.gwei, priority.tip) : null,
343 amount: isNativeToken ? EtherAmount.inWei(amount.amount) : EtherAmount.zero(),
344 data: data != null ? hexToBytes(data) : null,
345 maxGas: estimatedGasUnits,
346 maxFeePerGas: EtherAmount.fromInt(EtherUnit.wei, maxFeePerGas),
347 gasPrice: gasPrice != null ? EtherAmount.fromInt(EtherUnit.wei, gasPrice) : null,
348 nonce: nonce,
349 );
350
351 Uint8List signedTransaction;
352
353 final Function _sendTransaction;
354
355 if (isNativeToken) {
356 signedTransaction = await _client!.signTransaction(privateKey, transaction, chainId: chainId);
357 } else {
358 final erc20 = ERC20(
359 client: _client!,
360 address: EthereumAddress.fromHex(contractAddress!),
361 chainId: chainId,
362 );
363
364 signedTransaction = await erc20.transfer(
365 EthereumAddress.fromHex(toAddress),
366 amount.amount,
367 credentials: privateKey,
368 transaction: transaction,
369 );
370 }
371
372 _sendTransaction = () async =>
373 await sendTransaction(signedTransaction, useBlinkProtection: useBlinkProtection);
374
375 return PendingEVMChainTransaction(
376 signedTransaction: prepareSignedTransactionForSending(signedTransaction),
377 amount: amount,
378 fee: gasFee,
379 sendTransaction: _sendTransaction,
380 );
381 }
382
383 Future<PendingEVMChainTransaction> signApprovalTransaction({
384 required Credentials privateKey,
385 required String spender,
386 required Money amount,
387 required Money gasFee,
388 required int estimatedGasUnits,
389 required int maxFeePerGas,
390 required EVMChainTransactionPriority? priority,
391 required String contractAddress,
392 int? gasPrice,
393 bool useBlinkProtection = true,
394 }) async {
395 final nonce = await _client!.getTransactionCount(
396 privateKey.address,
397 atBlock: const BlockNum.pending(),
398 );
399
400 final Transaction transaction = createTransaction(
401 from: privateKey.address,
402 to: EthereumAddress.fromHex(contractAddress),
403 maxPriorityFeePerGas:
404 priority != null ? EtherAmount.fromInt(EtherUnit.gwei, priority.tip) : null,
405 amount: EtherAmount.zero(),
406 maxGas: estimatedGasUnits,
407 maxFeePerGas: EtherAmount.fromInt(EtherUnit.wei, maxFeePerGas),
408 gasPrice: gasPrice != null ? EtherAmount.fromInt(EtherUnit.wei, gasPrice) : null,
409 nonce: nonce,
410 );
411
412 final erc20 = ERC20(
413 client: _client!,
414 address: EthereumAddress.fromHex(contractAddress),
415 chainId: chainId,
416 );
417
418 final signedTransaction = await erc20.approve(
419 EthereumAddress.fromHex(spender),
420 amount.amount,
421 credentials: privateKey,
422 transaction: transaction,
423 );
424
425 return PendingEVMChainTransaction(
426 signedTransaction: prepareSignedTransactionForSending(signedTransaction),
427 amount: amount,
428 fee: gasFee,
429 sendTransaction: () =>
430 sendTransaction(signedTransaction, useBlinkProtection: useBlinkProtection),
431 isInfiniteApproval: amount.amount.toRadixString(16) ==
432 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
433 );
434 }
435
436 Transaction createTransaction({
437 required EthereumAddress from,
438 required EthereumAddress to,
439 required EtherAmount amount,
440 EtherAmount? maxPriorityFeePerGas,
441 EtherAmount? gasPrice,
442 EtherAmount? maxFeePerGas,
443 Uint8List? data,
444 int? maxGas,
445 int? nonce,
446 }) {
447 return Transaction(
448 from: from,
449 to: to,
450 maxPriorityFeePerGas: maxPriorityFeePerGas,
451 value: amount,
452 data: data,
453 maxGas: maxGas,
454 gasPrice: gasPrice,
455 maxFeePerGas: maxFeePerGas,
456 nonce: nonce,
457 );
458 }
459
460 String _blinkUrl(String apiKey) => 'https://eth.blinklabs.xyz/v1/$apiKey';
461
462 Future<String> sendTransaction(
463 Uint8List signedTransaction, {
464 bool useBlinkProtection = false,
465 }) async {
466 final prepared = prepareSignedTransactionForSending(signedTransaction);
467
468 if (useBlinkProtection && secrets.blinkApiKey.isNotEmpty) {
469 final blinkClient = Web3Client(_blinkUrl(secrets.blinkApiKey), client);
470 try {
471 return await blinkClient.sendRawTransaction(prepared);
472 } catch (e) {
473 printV('Blink failed, retrying without Blink: $e');
474 return await _client!.sendRawTransaction(prepared);
475 } finally {
476 await blinkClient.dispose();
477 }
478 }
479
480 return await _client!.sendRawTransaction(prepared);
481 }
482
483 Future getTransactionDetails(String transactionHash) async {
484 // Wait for the transaction receipt to become available
485 TransactionReceipt? receipt;
486 while (receipt == null) {
487 receipt = await _client!.getTransactionReceipt(transactionHash);
488 await Future.delayed(const Duration(seconds: 1));
489 }
490
491 // Print the receipt information
492 log('Transaction Hash: ${receipt.transactionHash}');
493 log('Block Hash: ${receipt.blockHash}');
494 log('Block Number: ${receipt.blockNumber}');
495 log('Gas Used: ${receipt.gasUsed}');
496
497 /*
498 Transaction Hash: [112, 244, 4, 238, 89, 199, 171, 191, 210, 236, 110, 42, 185, 202, 220, 21, 27, 132, 123, 221, 137, 90, 77, 13, 23, 43, 12, 230, 93, 63, 221, 116]
499 I/flutter ( 4474): Block Hash: [149, 44, 250, 119, 111, 104, 82, 98, 17, 89, 30, 190, 25, 44, 218, 118, 127, 189, 241, 35, 213, 106, 25, 95, 195, 37, 55, 131, 185, 180, 246, 200]
500 I/flutter ( 4474): Block Number: 17120242
501 I/flutter ( 4474): Gas Used: 21000
502 */
503
504 // Wait for the transaction receipt to become available
505 TransactionInformation? transactionInformation;
506 while (transactionInformation == null) {
507 log("********************************");
508 transactionInformation = await _client!.getTransactionByHash(transactionHash);
509 await Future.delayed(const Duration(seconds: 1));
510 }
511 // Print the receipt information
512 log('Transaction Hash: ${transactionInformation.hash}');
513 log('Block Hash: ${transactionInformation.blockHash}');
514 log('Block Number: ${transactionInformation.blockNumber}');
515 log('Gas Used: ${transactionInformation.gas}');
516
517 /*
518 Transaction Hash: 0x70f404ee59c7abbfd2ec6e2ab9cadc151b847bdd895a4d0d172b0ce65d3fdd74
519 I/flutter ( 4474): Block Hash: 0x952cfa776f68526211591ebe192cda767fbdf123d56a195fc3253783b9b4f6c8
520 I/flutter ( 4474): Block Number: 17120242
521 I/flutter ( 4474): Gas Used: 53000
522 */
523 }
524
525 Future<EVMChainERC20Balance> fetchERC20Balances(
526 EthereumAddress userAddress, Erc20Token token) async {
527 try {
528 final erc20 =
529 ERC20(address: EthereumAddress.fromHex(token.contractAddress), client: _client!);
530 final balance = await erc20.balanceOf(userAddress);
531
532 return EVMChainERC20Balance(Money(balance, token));
533 } on RangeError catch (_) {
534 throw Exception('Invalid token contract for this network.');
535 } catch (e) {
536 if (e.toString().contains("hostUnreachable")) {
537 return EVMChainERC20Balance(Money.zero(token));
538 }
539 throw Exception('Could not fetch balances: ${e.toString()}');
540 }
541 }
542
543 Future<Erc20Token?> getErc20Token(String contractAddress, String chainName) async {
544 try {
545 final token = await getErc20TokenFromMoralis(contractAddress, chainName);
546
547 if (token == null || token.name.isEmpty || token.symbol.isEmpty) {
548 return await getErcTokenInfoFromNode(contractAddress, chainName);
549 }
550
551 return token;
552 } catch (e) {
553 try {
554 return await getErcTokenInfoFromNode(contractAddress, chainName);
555 } catch (e) {
556 return null;
557 }
558 }
559 }
560
561 Future<Erc20Token?> getErc20TokenFromMoralis(String contractAddress, String chainName) async {
562 if (secrets.moralisApiKey.isEmpty) {
563 printV('Moralis API key is empty, cannot fetch token info');
564 return null;
565 }
566 final uri = Uri.https(
567 'deep-index.moralis.io',
568 '/api/v2.2/erc20/metadata',
569 {
570 "chain": chainName,
571 "addresses": contractAddress,
572 },
573 );
574
575 final response = await client.get(
576 uri,
577 headers: {
578 "Accept": "application/json",
579 "X-API-Key": secrets.moralisApiKey,
580 },
581 );
582
583 final decodedResponse = jsonDecode(response.body)[0] as Map<String, dynamic>;
584
585 final symbol = (decodedResponse['symbol'] ?? '') as String;
586 String filteredSymbol = symbol.replaceFirst(RegExp('^\\\$'), '');
587
588 final name = (decodedResponse['name'] ?? '').toString();
589 final decimal = decodedResponse['decimals'] ?? '0';
590 final iconPath = decodedResponse['logo'] ?? '';
591
592 return Erc20Token(
593 name: name,
594 symbol: filteredSymbol,
595 contractAddress: contractAddress,
596 decimal: int.tryParse(decimal) ?? 0,
597 iconPath: iconPath,
598 );
599 }
600
601 Future<Erc20Token?> getErcTokenInfoFromNode(String contractAddress, String chainName) async {
602 final erc20 = ERC20(address: EthereumAddress.fromHex(contractAddress), client: _client!);
603 final name = await erc20.name();
604 final symbol = await erc20.symbol();
605 final decimal = await erc20.decimals();
606
607 return Erc20Token(
608 name: name,
609 symbol: symbol,
610 contractAddress: contractAddress,
611 decimal: decimal.toInt(),
612 );
613 }
614
615 Future<List<MoralisWalletTokenBalance>> fetchWalletTokensFromMoralis(
616 String address,
617 String chainName,
618 ) async {
619 try {
620 if (secrets.moralisApiKey.isEmpty) {
621 printV('Moralis API key is empty, cannot fetch wallet tokens');
622 return [];
623 }
624
625 const maxPages = 3;
626 String? cursor;
627 int pageCount = 0;
628 final List<MoralisWalletTokenBalance> tokens = [];
629
630 do {
631 final params = <String, String>{
632 "chain": chainName,
633 if (cursor != null && cursor.isNotEmpty) "cursor": cursor,
634 };
635
636 final uri = Uri.https(
637 'deep-index.moralis.io',
638 '/api/v2.2/wallets/$address/tokens',
639 params,
640 );
641
642 final response = await client.get(
643 uri,
644 headers: {
645 "Accept": "application/json",
646 "X-API-Key": secrets.moralisApiKey,
647 },
648 );
649
650 if (response.statusCode < 200 || response.statusCode >= 300) {
651 printV('Moralis API returned invalid status code: ${response.statusCode}');
652 return tokens;
653 }
654
655 final decoded = jsonDecode(response.body);
656 if (decoded is! Map<String, dynamic>) return tokens;
657
658 final result = decoded['result'];
659 if (result is! List) return tokens;
660
661 for (final item in result) {
662 if (item is! Map<String, dynamic>) continue;
663 final tokenData = item;
664
665 final nativeRaw = tokenData['native_token'];
666 final nativeToken =
667 nativeRaw is bool ? nativeRaw : (nativeRaw?.toString().toLowerCase() == 'true');
668 if (nativeToken) continue;
669
670 final balanceStr = tokenData['balance'] as String? ?? '0';
671 final balanceWei = BigInt.tryParse(balanceStr) ?? BigInt.zero;
672 if (balanceWei == BigInt.zero) continue;
673
674 final contractAddress = (tokenData['token_address'] as String? ?? '').toLowerCase();
675 final name = (tokenData['name'] as String? ?? '').toString();
676 final symbol = (tokenData['symbol'] as String? ?? '').toString();
677 final symbolFiltered = symbol.replaceFirst(RegExp('^\\\$'), '');
678
679 final decimalsRaw = tokenData['decimals'];
680 final decimals =
681 decimalsRaw is int ? decimalsRaw : int.tryParse(decimalsRaw.toString()) ?? 18;
682
683 final logo = tokenData['logo'] as String?;
684 final thumbnail = tokenData['thumbnail'] as String?;
685 final iconUrl = logo ?? thumbnail;
686
687 final possibleSpamRaw = tokenData['possible_spam'];
688 final possibleSpam = possibleSpamRaw is bool
689 ? possibleSpamRaw
690 : (possibleSpamRaw?.toString().toLowerCase() == 'true');
691
692 final verifiedContractRaw = tokenData['verified_contract'];
693 final verifiedContract = verifiedContractRaw is bool
694 ? verifiedContractRaw
695 : (verifiedContractRaw?.toString().toLowerCase() == 'true');
696
697 final usdPriceRaw = tokenData['usd_price'];
698 final double? usdPrice = usdPriceRaw is num
699 ? usdPriceRaw.toDouble()
700 : (usdPriceRaw is String ? double.tryParse(usdPriceRaw) : null);
701
702 final usdValueRaw = tokenData['usd_value'];
703 final double? usdValue = usdValueRaw is num
704 ? usdValueRaw.toDouble()
705 : (usdValueRaw is String ? double.tryParse(usdValueRaw) : null);
706
707 final securityRaw = tokenData['security_score'];
708 final int? securityScore = securityRaw is int
709 ? securityRaw
710 : (securityRaw is num
711 ? securityRaw.toInt()
712 : (securityRaw is String ? int.tryParse(securityRaw) : null));
713
714 tokens.add(
715 MoralisWalletTokenBalance(
716 contractAddress: contractAddress,
717 name: name,
718 symbol: symbolFiltered,
719 decimals: decimals,
720 iconUrl: iconUrl,
721 balanceWei: balanceWei,
722 possibleSpam: possibleSpam,
723 verifiedContract: verifiedContract,
724 usdPrice: usdPrice,
725 usdValue: usdValue,
726 securityScore: securityScore,
727 ),
728 );
729 }
730
731 final nextCursor = decoded['cursor'];
732 cursor = nextCursor is String && nextCursor.isNotEmpty ? nextCursor : null;
733 pageCount++;
734 } while (cursor != null && pageCount < maxPages);
735
736 return tokens;
737 } catch (e, stackTrace) {
738 printV('Error fetching wallet tokens from Moralis: ${e.toString()}');
739 printV('Stack trace: ${stackTrace.toString()}');
740 return [];
741 }
742 }
743
744 Uint8List hexToBytes(String hexString) {
745 return Uint8List.fromList(
746 hex.HEX.decode(hexString.startsWith('0x') ? hexString.substring(2) : hexString));
747 }
748
749 void stop() {
750 _client?.dispose();
751 }
752
753 Web3Client? getWeb3Client() {
754 return _client;
755 }
756
757 // Future<int> _getDecimalPlacesForContract(DeployedContract contract) async {
758 // final String abi = await rootBundle.loadString("assets/abi_json/erc20_abi.json");
759 // final contractAbi = ContractAbi.fromJson(abi, "ERC20");
760 //
761 // final contract = DeployedContract(
762 // contractAbi,
763 // EthereumAddress.fromHex(_erc20Currencies[erc20Currency]!),
764 // );
765 // final decimalsFunction = contract.function('decimals');
766 // final decimals = await _client!.call(
767 // contract: contract,
768 // function: decimalsFunction,
769 // params: [],
770 // );
771 //
772 // int exponent = int.parse(decimals.first.toString());
773 // return exponent;
774 // }
775 }
776
777 class MoralisWalletTokenBalance {
778 final String contractAddress;
779 final String name;
780 final String symbol;
781 final int decimals;
782 final String? iconUrl;
783 final BigInt balanceWei;
784 final bool possibleSpam;
785 final bool verifiedContract;
786 final double? usdPrice;
787 final double? usdValue;
788 final int? securityScore;
789
790 MoralisWalletTokenBalance({
791 required this.contractAddress,
792 required this.name,
793 required this.symbol,
794 required this.decimals,
795 this.iconUrl,
796 required this.balanceWei,
797 required this.possibleSpam,
798 required this.verifiedContract,
799 this.usdPrice,
800 this.usdValue,
801 this.securityScore,
802 });
803 }