dev
dart 592 lines 18.6 KB
Raw
1 import 'dart:convert';
2
3 import 'package:cake_wallet/core/fiat_conversion_service.dart';
4 import 'package:cake_wallet/entities/fiat_api_mode.dart';
5 import 'package:cake_wallet/entities/erc20_token_info_moralis.dart';
6 import 'package:cake_wallet/entities/sort_balance_types.dart';
7 import 'package:cake_wallet/evm/evm.dart';
8 import 'package:cake_wallet/reactions/wallet_connect.dart';
9 import 'package:cake_wallet/solana/solana.dart';
10 import 'package:cake_wallet/store/settings_store.dart';
11 import 'package:cake_wallet/tron/tron.dart';
12 import 'package:cake_wallet/utils/token_utilities.dart';
13 import 'package:cw_core/utils/proxy_wrapper.dart';
14 import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
15 import 'package:cake_wallet/zano/zano.dart';
16 import 'package:cw_core/crypto_currency.dart';
17 import 'package:cw_core/erc20_token.dart';
18 import 'package:cw_core/utils/homoglyph_normalizer.dart';
19 import 'package:cw_core/utils/print_verbose.dart';
20 import 'package:cw_core/wallet_type.dart';
21 import 'package:mobx/mobx.dart';
22 import 'package:cake_wallet/.secrets.g.dart' as secrets;
23
24 part 'home_settings_view_model.g.dart';
25
26 class HomeSettingsViewModel = HomeSettingsViewModelBase with _$HomeSettingsViewModel;
27
28 abstract class HomeSettingsViewModelBase with Store {
29 HomeSettingsViewModelBase(this._settingsStore, this._balanceViewModel)
30 : tokens = ObservableSet<CryptoCurrency>(),
31 isAddingToken = false,
32 isDeletingToken = false,
33 isValidatingContractAddress = false,
34 showCombinedBalance = _balanceViewModel.wallet.walletInfo.showCombinedBalance,
35 favoriteToken = _balanceViewModel.wallet.currency {
36 _updateTokensList();
37 _updateLocalFavoriteToken();
38
39 // React to wallet changes
40 reaction((_) => _balanceViewModel.wallet, (_) {
41 _updateTokensList();
42 });
43 reaction((_) {
44 final wallet = _balanceViewModel.wallet;
45 if (isEVMCompatibleChain(wallet.type)) {
46 final selectedChainId = evm!.getSelectedChainId(wallet);
47 final erc20Currencies = evm!.getERC20Currencies(wallet);
48 return '${wallet.currency.title}_${selectedChainId}_${erc20Currencies.length}';
49 }
50 return null;
51 }, (_) async {
52 await Future.delayed(const Duration(milliseconds: 200));
53 _updateTokensList();
54 });
55 }
56
57 final SettingsStore _settingsStore;
58 final BalanceViewModel _balanceViewModel;
59
60 final ObservableSet<CryptoCurrency> tokens;
61
62 @computed
63 List<CryptoCurrency> get enabledTokens =>
64 [_balanceViewModel.wallet.currency, ...tokens.where((item) => item.enabled).toList()];
65
66 WalletType get walletType => _balanceViewModel.wallet.type;
67
68 @observable
69 bool isAddingToken;
70
71 @observable
72 bool isDeletingToken;
73
74 @observable
75 bool isValidatingContractAddress;
76
77 @observable
78 String searchText = '';
79
80 @computed
81 SortBalanceBy get sortBalanceBy => _settingsStore.sortBalanceBy;
82
83 @action
84 void setSortBalanceBy(SortBalanceBy value) {
85 _settingsStore.sortBalanceBy = value;
86 _updateTokensList();
87 }
88
89 @computed
90 bool get pinNativeToken => _settingsStore.pinNativeTokenAtTop;
91
92 @action
93 void setPinNativeToken(bool value) => _settingsStore.pinNativeTokenAtTop = value;
94
95 @action
96 Future<void> addToken({
97 required String contractAddress,
98 required CryptoCurrency token,
99 }) async {
100 try {
101 isAddingToken = true;
102
103 if (isEVMCompatibleChain(_balanceViewModel.wallet.type)) {
104 final evmToken = Erc20Token(
105 name: token.name,
106 symbol: token.title,
107 decimal: token.decimals,
108 contractAddress: contractAddress.toLowerCase(),
109 iconPath: token.iconPath,
110 isPotentialScam: token.isPotentialScam,
111 );
112 await evm!.addErc20Token(_balanceViewModel.wallet, evmToken);
113 }
114
115 if (_balanceViewModel.wallet.type == WalletType.solana) {
116 final splToken = token.copyWith(enabled: true);
117 await solana!.addSPLToken(
118 _balanceViewModel.wallet,
119 splToken,
120 contractAddress,
121 );
122 }
123
124 if (_balanceViewModel.wallet.type == WalletType.tron) {
125 final tronToken = token.copyWith(enabled: true);
126 await tron!.addTronToken(_balanceViewModel.wallet, tronToken, contractAddress);
127 }
128
129 if (_balanceViewModel.wallet.type == WalletType.zano) {
130 await zano!.addZanoAssetById(_balanceViewModel.wallet, contractAddress);
131 }
132
133 _updateTokensList();
134 _updateFiatPrices(token);
135 } catch (e) {
136 throw e;
137 } finally {
138 isAddingToken = false;
139 }
140 }
141
142 @action
143 bool checkIfTokenIsAlreadyAdded(String contractAddress) {
144 if (isEVMCompatibleChain(_balanceViewModel.wallet.type)) {
145 return evm!.isTokenAlreadyAdded(_balanceViewModel.wallet, contractAddress);
146 }
147
148 if (_balanceViewModel.wallet.type == WalletType.solana) {
149 return solana!.isTokenAlreadyAdded(_balanceViewModel.wallet, contractAddress);
150 }
151
152 if (_balanceViewModel.wallet.type == WalletType.tron) {
153 return tron!.isTokenAlreadyAdded(_balanceViewModel.wallet, contractAddress);
154 }
155
156 if (_balanceViewModel.wallet.type == WalletType.zano) {
157 return zano!.isTokenAlreadyAdded(_balanceViewModel.wallet, contractAddress);
158 }
159
160 return false;
161 }
162
163 @action
164 Future<void> deleteToken(CryptoCurrency token) async {
165 try {
166 isDeletingToken = true;
167 if (isEVMCompatibleChain(_balanceViewModel.wallet.type)) {
168 await evm!.deleteErc20Token(_balanceViewModel.wallet, token as Erc20Token);
169 }
170
171 if (_balanceViewModel.wallet.type == WalletType.solana) {
172 await solana!.deleteSPLToken(_balanceViewModel.wallet, token);
173 }
174
175 if (_balanceViewModel.wallet.type == WalletType.tron) {
176 await tron!.deleteTronToken(_balanceViewModel.wallet, token);
177 }
178 if (_balanceViewModel.wallet.type == WalletType.zano) {
179 await zano!.deleteZanoAsset(_balanceViewModel.wallet, token);
180 }
181 _updateTokensList();
182 } finally {
183 isDeletingToken = false;
184 }
185 }
186
187 Future<bool> checkIfERC20TokenContractAddressIsAPotentialScamAddress(
188 String contractAddress,
189 ) async {
190 try {
191 isValidatingContractAddress = true;
192
193 if (!isEVMCompatibleChain(_balanceViewModel.wallet.type)) {
194 return false;
195 }
196
197 bool isPotentialScamViaMoralis = await _isPotentialScamTokenViaMoralis(
198 contractAddress,
199 getChainNameBasedOnWalletType(_balanceViewModel.wallet.type),
200 );
201
202 bool isUnverifiedContract = await _isContractUnverified(
203 contractAddress,
204 chainId: evm!.getSelectedChainId(_balanceViewModel.wallet).toString(),
205 );
206
207 final showWarningForContractAddress = isPotentialScamViaMoralis || isUnverifiedContract;
208
209 return showWarningForContractAddress;
210 } finally {
211 isValidatingContractAddress = false;
212 }
213 }
214
215 bool checkIfTokenIsWhitelisted(String contractAddress) {
216 // get the default tokens for each currency type:
217 List<String> defaultTokenAddresses = [];
218 switch (_balanceViewModel.wallet.type) {
219 case WalletType.ethereum:
220 case WalletType.polygon:
221 case WalletType.base:
222 case WalletType.arbitrum:
223 case WalletType.bsc:
224 defaultTokenAddresses = evm!.getDefaultTokenContractAddresses(_balanceViewModel.wallet);
225 break;
226 case WalletType.solana:
227 defaultTokenAddresses = solana!.getDefaultTokenContractAddresses();
228 break;
229 case WalletType.tron:
230 defaultTokenAddresses = tron!.getDefaultTokenContractAddresses();
231 break;
232 case WalletType.zano:
233 case WalletType.banano:
234 case WalletType.monero:
235 case WalletType.none:
236 case WalletType.bitcoin:
237 case WalletType.litecoin:
238 case WalletType.haven:
239 case WalletType.nano:
240 case WalletType.wownero:
241 case WalletType.bitcoinCash:
242 case WalletType.decred:
243 case WalletType.dogecoin:
244 case WalletType.zcash:
245 return false;
246 }
247
248 // check if the contractAddress is in the defaultTokenAddresses
249 bool isInWhitelist = defaultTokenAddresses
250 .any((element) => element.toLowerCase() == contractAddress.toLowerCase());
251 return isInWhitelist;
252 }
253
254 bool checkIfTokenSymbolMatchesDefaultToken(String symbol) {
255 final normalizedSymbol = normalizeHomoglyphs(symbol.trim().toUpperCase());
256 if (normalizedSymbol.isEmpty) return false;
257
258 List<String> defaultTokenSymbols = [];
259 switch (_balanceViewModel.wallet.type) {
260 case WalletType.ethereum:
261 case WalletType.polygon:
262 case WalletType.base:
263 case WalletType.arbitrum:
264 case WalletType.bsc:
265 defaultTokenSymbols = evm!.getDefaultTokenSymbols(_balanceViewModel.wallet);
266 break;
267 case WalletType.solana:
268 defaultTokenSymbols = solana!.getDefaultTokenSymbols();
269 break;
270 case WalletType.tron:
271 defaultTokenSymbols = tron!.getDefaultTokenSymbols();
272 break;
273 case WalletType.zano:
274 case WalletType.banano:
275 case WalletType.monero:
276 case WalletType.none:
277 case WalletType.bitcoin:
278 case WalletType.litecoin:
279 case WalletType.haven:
280 case WalletType.nano:
281 case WalletType.wownero:
282 case WalletType.bitcoinCash:
283 case WalletType.decred:
284 case WalletType.dogecoin:
285 case WalletType.zcash:
286 return false;
287 }
288
289 return defaultTokenSymbols.any((s) => s.toUpperCase() == normalizedSymbol);
290 }
291
292 Future<bool> _isPotentialScamTokenViaMoralis(
293 String contractAddress,
294 String chainName,
295 ) async {
296 final uri = Uri.https(
297 'deep-index.moralis.io',
298 '/api/v2.2/erc20/metadata',
299 {
300 "chain": chainName,
301 "addresses": contractAddress,
302 },
303 );
304
305 try {
306 final response = await ProxyWrapper().get(
307 clearnetUri: uri,
308 headers: {
309 "Accept": "application/json",
310 "X-API-Key": secrets.moralisApiKey,
311 },
312 );
313
314 final decodedResponse = jsonDecode(response.body);
315
316 final tokenInfo = Erc20TokenInfoMoralis.fromJson(decodedResponse[0] as Map<String, dynamic>);
317
318 // Based on analysis using Moralis internal metrics
319 if (tokenInfo.possibleSpam == true) {
320 return true;
321 }
322
323 // Tokens whose contract have not been verified are potentially risky tokens.
324 // if (tokenInfo.verifiedContract == false) {
325 // return true;
326 // }
327
328 // Tokens with a security score less than 40 are potentially risky, requiring caution when dealing with them.
329 if (tokenInfo.securityScore != null && tokenInfo.securityScore! < 40) {
330 return true;
331 }
332
333 // Having a Fully Diluted Valiuation of 0 is a significant red flag that could signify:
334 // - An abandoned/unlaunched project
335 // - Incorrect/missing token data
336 // - Suspicious manipulation of token data
337
338 /// commented out as it's failing a lot of legit tokens
339 // if (tokenInfo.fullyDilutedValuation == '0') {
340 // return true;
341 // }
342
343 return false;
344 } catch (e) {
345 printV('Error while checking scam via moralis: ${e.toString()}');
346 return true;
347 }
348 }
349
350 Future<bool> _isContractUnverified(
351 String contractAddress, {
352 required String chainId,
353 }) async {
354 final uri = Uri.https(
355 "api.etherscan.io",
356 "/v2/api",
357 {
358 "chainid": chainId,
359 "module": "contract",
360 "action": "getsourcecode",
361 "address": contractAddress,
362 "apikey": secrets.etherScanApiKey,
363 },
364 );
365
366 try {
367 final response = await ProxyWrapper().get(clearnetUri: uri);
368
369 final decodedResponse = jsonDecode(response.body) as Map<String, dynamic>;
370
371 if (decodedResponse['status'] == '0') {
372 printV('${response.body}\n');
373 printV('${decodedResponse['result']}\n');
374 return true;
375 }
376
377 if (decodedResponse['status'] == '1' &&
378 decodedResponse['result'][0]['ABI'] == 'Contract source code not verified') {
379 printV('Call is valid but contract is not verified');
380 return true; // Contract is not verified
381 } else {
382 printV('Call is valid and contract is verified');
383 return false; // Contract is verified
384 }
385 } catch (e) {
386 printV('Error while checking contract verification: ${e.toString()}');
387 return true;
388 }
389 }
390
391 Future<CryptoCurrency?> getToken(String contractAddress) async {
392 if (isEVMCompatibleChain(_balanceViewModel.wallet.type)) {
393 return await evm!.getErc20Token(_balanceViewModel.wallet, contractAddress);
394 }
395
396 if (_balanceViewModel.wallet.type == WalletType.solana) {
397 return await solana!.getSPLToken(_balanceViewModel.wallet, contractAddress);
398 }
399
400 if (_balanceViewModel.wallet.type == WalletType.tron) {
401 return await tron!.getTronToken(_balanceViewModel.wallet, contractAddress);
402 }
403
404 if (_balanceViewModel.wallet.type == WalletType.zano) {
405 return await zano!.getZanoAsset(_balanceViewModel.wallet, contractAddress);
406 }
407
408 return null;
409 }
410
411 Future<bool?> checkIfTokenIsVerifiedOnJupiter(String mintAddress) async {
412 if (_balanceViewModel.wallet.type != WalletType.solana) {
413 return null;
414 }
415
416 return solana!.isTokenVerifiedOnJupiter(_balanceViewModel.wallet, mintAddress);
417 }
418
419 CryptoCurrency get nativeToken => _balanceViewModel.wallet.currency;
420
421 void _updateFiatPrices(CryptoCurrency token) async {
422 if (token.isPotentialScam) return; // don't fetch price data for potential scam tokens
423 try {
424 _balanceViewModel.fiatConversionStore.prices[token] = await FiatConversionService.fetchPrice(
425 crypto: token,
426 fiat: _settingsStore.fiatCurrency,
427 torOnly: _settingsStore.fiatApiMode == FiatApiMode.torOnly);
428 } catch (_) {}
429 }
430
431 void changeTokenAvailability(CryptoCurrency token, bool value) async {
432 token.enabled = value;
433
434 if (isEVMCompatibleChain(_balanceViewModel.wallet.type)) {
435 evm!.addErc20Token(_balanceViewModel.wallet, token as Erc20Token);
436 if (!value) evm!.removeTokenTransactionsInHistory(_balanceViewModel.wallet, token);
437 }
438
439 if (_balanceViewModel.wallet.type == WalletType.solana) {
440 final address = solana!.getTokenAddress(token);
441 solana!.addSPLToken(_balanceViewModel.wallet, token, address);
442 }
443
444 if (_balanceViewModel.wallet.type == WalletType.tron) {
445 final address = tron!.getTokenAddress(token);
446 tron!.addTronToken(_balanceViewModel.wallet, token, address);
447 }
448
449 if (_balanceViewModel.wallet.type == WalletType.zano) {
450 await zano!.changeZanoAssetAvailability(_balanceViewModel.wallet, token);
451 }
452
453 _refreshTokensList();
454 }
455
456 @action
457 void _updateTokensList() {
458 int _sortFunc(CryptoCurrency e1, CryptoCurrency e2) {
459 int index1 = _balanceViewModel.formattedBalances.indexWhere((element) => element.asset == e1);
460 int index2 = _balanceViewModel.formattedBalances.indexWhere((element) => element.asset == e2);
461
462 if (e1.enabled && !e2.enabled) {
463 return -1;
464 } else if (e2.enabled && !e1.enabled) {
465 return 1;
466 } else if (!e1.enabled && !e2.enabled) {
467 // if both are disabled then sort alphabetically
468 return e1.name.compareTo(e2.name);
469 }
470
471 return index1.compareTo(index2);
472 }
473
474 tokens.clear();
475
476 if (isEVMCompatibleChain(_balanceViewModel.wallet.type)) {
477 tokens.addAll(evm!
478 .getERC20Currencies(_balanceViewModel.wallet)
479 .where((element) => _matchesSearchText(element))
480 .toList()
481 ..sort(_sortFunc));
482 }
483
484 if (_balanceViewModel.wallet.type == WalletType.solana) {
485 tokens.addAll(solana!
486 .getSPLTokenCurrencies(_balanceViewModel.wallet)
487 .where((element) => _matchesSearchText(element))
488 .toList()
489 ..sort(_sortFunc));
490 }
491
492 if (_balanceViewModel.wallet.type == WalletType.tron) {
493 tokens.addAll(tron!
494 .getTronTokenCurrencies(_balanceViewModel.wallet)
495 .where((element) => _matchesSearchText(element))
496 .toList()
497 ..sort(_sortFunc));
498 }
499
500 if (_balanceViewModel.wallet.type == WalletType.zano) {
501 tokens.addAll(zano!
502 .getZanoAssets(_balanceViewModel.wallet)
503 .where((element) => _matchesSearchText(element))
504 .toList()
505 ..sort(_sortFunc));
506 }
507 }
508
509 // FIXME these two observables cause duplicated state and are needed because mobx. remove them as part of the refactor and replace with proper getters
510 @observable
511 bool showCombinedBalance;
512
513 @observable
514 CryptoCurrency favoriteToken;
515
516 @action
517 void setShowCombinedBalance(bool value) {
518 _balanceViewModel.wallet.walletInfo.showCombinedBalance = value;
519 showCombinedBalance = value;
520 _balanceViewModel.wallet.updateBalance();
521 _balanceViewModel.wallet.walletInfo.save();
522 }
523
524 @action
525 Future<void> setFavoriteToken(CryptoCurrency token) async {
526 final String? address;
527
528 if (token == _balanceViewModel.wallet.currency) {
529 address = null;
530 } else {
531 address = getTokenAddressBasedOnWallet(token);
532 }
533
534 _balanceViewModel.wallet.walletInfo.favoriteTokenAddress = address;
535 _balanceViewModel.wallet.walletInfo.save();
536 _updateLocalFavoriteToken();
537 }
538
539 @action
540 Future<void> _updateLocalFavoriteToken() async {
541 favoriteToken = await TokenUtilities.findTokenByAddress(
542 walletType: _balanceViewModel.wallet.type,
543 address: _balanceViewModel.wallet.walletInfo.favoriteTokenAddress ?? "") ??
544 _balanceViewModel.wallet.currency;
545 }
546
547 @action
548 void _refreshTokensList() {
549 final _tokens = Set.of(tokens);
550 tokens.clear();
551 tokens.addAll(_tokens);
552 }
553
554 @action
555 void changeSearchText(String text) {
556 searchText = text;
557 _updateTokensList();
558 }
559
560 bool _matchesSearchText(CryptoCurrency asset) {
561 final address = getTokenAddressBasedOnWallet(asset);
562
563 // The homes settings would only be displayed for either of Tron, EVM or Solana Wallets.
564 if (address == null) return false;
565
566 return searchText.isEmpty ||
567 asset.fullName!.toLowerCase().contains(searchText.toLowerCase()) ||
568 asset.title.toLowerCase().contains(searchText.toLowerCase()) ||
569 address == searchText;
570 }
571
572 String? getTokenAddressBasedOnWallet(CryptoCurrency asset) {
573 if (_balanceViewModel.wallet.type == WalletType.tron) {
574 return tron!.getTokenAddress(asset);
575 }
576
577 if (_balanceViewModel.wallet.type == WalletType.solana) {
578 return solana!.getTokenAddress(asset);
579 }
580
581 if (isEVMCompatibleChain(_balanceViewModel.wallet.type)) {
582 return evm!.getTokenAddress(asset);
583 }
584
585 if (_balanceViewModel.wallet.type == WalletType.zano) {
586 return zano!.getZanoAssetAddress(asset);
587 }
588
589 // We return null if it's neither Tron, EVM or Solana wallet (which is actually impossible because we only display home settings for either of these four wallets).
590 return null;
591 }
592 }