CW-1228: Automatically Fetch All Tokens (#2869)

* feat: Automatically detect wallet tokens for EVM chains * feat: Disable potential tg scam tokens by default * feat: Add homoglyph normalization to detect spoofing attacks in token symbols * refactor: Improve scam token detection and apply to automatically fetched EVM tokens * refactor: Enhance scam detection for automatically detected tokens in evm wallets * feat: Add fiat check to scam checks on automatically imported token and disable tokens that fail the check * feat: Add fiat check to scam checks on automatically imported token and disable tokens that fail the check * feat: Add fiat check to scam checks on automatically imported token and disable tokens that fail the check * feat: Enable whitelisted tokens with balance when importing wallet tokens * feat: Disable imported tokens with balance less than 0.1 usd * feat: Disable imported tokens with balance less than 0.1 usd - Update configure file

David Adegoke committed Feb 5, 2026 at 23:52 UTC 4ae94db3eec46a486818628933222972716637bd
4 files changed +61 -27
cw_evm/lib/evm_chain_wallet.dart
+31 -8
@@ -509,17 +509,17 @@ abstract class EVMChainWalletBase
509 }
510 }
511
512 - Future<List<Erc20Token>> discoverTokensFromMoralis() async {
512 + Future<MoralisDiscoveryResult> discoverTokensFromMoralis() async {
513 try {
514 - if (!evmChainErc20TokensBox.isOpen) return [];
514 + if (!evmChainErc20TokensBox.isOpen) return MoralisDiscoveryResult.empty;
515
516 final address = walletAddresses.address;
517 - if (address.isEmpty) return [];
517 + if (address.isEmpty) return MoralisDiscoveryResult.empty;
518
519 final chainName = EVMChainUtils.getDefaultTokenSymbol(selectedChainId).toLowerCase();
520
521 final walletTokens = await _client.fetchWalletTokensFromMoralis(address, chainName);
522 - if (walletTokens.isEmpty) return [];
522 + if (walletTokens.isEmpty) return MoralisDiscoveryResult.empty;
523
524 final existingTokenAddresses = {
525 for (final token in evmChainErc20TokensBox.values)
@@ -529,7 +529,7 @@ abstract class EVMChainWalletBase
529 final whitelistedTokenAddresses =
530 getDefaultTokenContractAddresses.map((a) => a.toLowerCase()).toSet();
531
532 - final List<Erc20Token> newTokens = [];
532 + final newTokens = <DiscoveredToken>[];
533
534 for (final token in walletTokens) {
535 final addr = token.contractAddress.toLowerCase();
@@ -554,13 +554,18 @@ abstract class EVMChainWalletBase
554 isPotentialScam: token.possibleSpam,
555 );
556
557 - newTokens.add(newToken);
557 + newTokens.add(
558 + DiscoveredToken(
559 + token: newToken,
560 + balanceWei: token.balanceWei,
561 + ),
562 + );
563 }
564
560 - return newTokens;
565 + return MoralisDiscoveryResult(newTokens: newTokens);
566 } catch (e) {
567 printV('Error discovering tokens from Moralis: ${e.toString()}');
563 - return [];
568 + return MoralisDiscoveryResult.empty;
569 }
570 }
571
@@ -1535,3 +1540,21 @@ class GasParamsHandler {
1540 );
1541 }
1542 }
1543 +
1544 +class DiscoveredToken {
1545 + final Erc20Token token;
1546 + final BigInt balanceWei;
1547 +
1548 + const DiscoveredToken({
1549 + required this.token,
1550 + required this.balanceWei,
1551 + });
1552 +}
1553 +
1554 +class MoralisDiscoveryResult {
1555 + final List<DiscoveredToken> newTokens;
1556 +
1557 + const MoralisDiscoveryResult({required this.newTokens});
1558 +
1559 + static const MoralisDiscoveryResult empty = MoralisDiscoveryResult(newTokens: []);
1560 +}
cw_evm/lib/utils/evm_chain_utils.dart
+1 -1
@@ -86,7 +86,7 @@ class EVMChainUtils {
86 137 => 'MATIC',
87 8453 => 'BASE',
88 42161 => 'ARBITRUM',
89 - 56 => 'BNB',
89 + 56 => 'BSC',
90 _ => 'ETH',
91 };
92 }
lib/evm/cw_evm.dart
+26 -16
@@ -514,49 +514,59 @@ class CWEVM extends EVM {
514 @override
515 bool hasPriorityFee(int chainId) => EVMChainUtils.hasPriorityFee(chainId);
516
517 - @override
518 - Future<bool> checkTokenFiatPrice(WalletBase wallet, Erc20Token token) async {
517 + Future<({double usdValue, bool hasValidFiatPrice})> _getTokenUsdValueAndFiatCheck(
518 + Erc20Token token,
519 + BigInt balanceWei,
520 + ) async {
521 try {
522 final settingsStore = getIt.get<SettingsStore>();
521 - final fiatCurrency = settingsStore.fiatCurrency;
523 final torOnly = settingsStore.fiatApiMode == FiatApiMode.torOnly;
524
525 final price = await FiatConversionService.fetchPrice(
526 crypto: token,
526 - fiat: fiatCurrency,
527 + fiat: FiatCurrency.usd,
528 torOnly: torOnly,
529 );
530
530 - return price > 0;
531 + final hasValidFiatPrice = price > 0;
532 +
533 + final decimals = token.decimal;
534 + final balance = balanceWei.toDouble() / math.pow(10, decimals);
535 + final usdValue = balance * price;
536 +
537 + return (usdValue: usdValue, hasValidFiatPrice: hasValidFiatPrice);
538 } catch (e) {
532 - return false;
539 + return (usdValue: 0.0, hasValidFiatPrice: false);
540 }
541 }
542
543 + static const _minTokenUsdValue = 0.1;
544 @override
545 Future<void> discoverAndAddWalletTokens(WalletBase wallet) async {
546 if (wallet is! EVMChainWallet) return;
547
548 try {
541 - final discoveredTokens = await wallet.discoverTokensFromMoralis();
549 + final result = await wallet.discoverTokensFromMoralis();
550
543 - if (discoveredTokens.isEmpty) return;
551 + if (result.newTokens.isEmpty) return;
552
553 final List<Future<void>> tokenChecks = [];
554
547 - for (final token in discoveredTokens) {
555 + for (final item in result.newTokens) {
556 tokenChecks.add((() async {
549 - final isPropertiesSuspicious = wallet.isTokenPropertiesSuspicious(token);
557 + final token = item.token;
558
551 - bool hasValidFiatPrice = true;
552 - if (!isPropertiesSuspicious) {
553 - hasValidFiatPrice = await checkTokenFiatPrice(wallet, token);
554 - }
559 + final isPropertiesSuspicious = wallet.isTokenPropertiesSuspicious(token);
560
556 - final isSpam = isPropertiesSuspicious || !hasValidFiatPrice;
561 + final fiatResult = await _getTokenUsdValueAndFiatCheck(
562 + token,
563 + item.balanceWei,
564 + );
565 + final isSpam = isPropertiesSuspicious || !fiatResult.hasValidFiatPrice;
566
567 token.isPotentialScam = isSpam;
559 - token.enabled = !isSpam;
568 +
569 + token.enabled = (fiatResult.usdValue >= _minTokenUsdValue) && !isSpam;
570
571 await wallet.addErc20Token(token);
572 })());
tool/configure.dart
+3 -2
@@ -1322,6 +1322,7 @@ abstract class DogeCoin {
1322 Future<void> generateEVM(bool hasImplementation) async {
1323 final outputFile = File(evmOutputPath);
1324 const evmCommonHeaders = """
1325 +import 'dart:math' as math;
1326 import 'package:cake_wallet/core/utilities.dart';
1327 import 'package:cake_wallet/view_model/send/output.dart';
1328 import 'package:cw_core/crypto_currency.dart';
@@ -1348,6 +1349,7 @@ import 'package:web3dart/web3dart.dart';
1349 import 'package:cake_wallet/core/fiat_conversion_service.dart';
1350 import 'package:cake_wallet/di.dart';
1351 import 'package:cake_wallet/entities/fiat_api_mode.dart';
1352 +import 'package:cake_wallet/entities/fiat_currency.dart';
1353 import 'package:cake_wallet/store/settings_store.dart';
1354 import 'package:cw_evm/utils/evm_chain_formatter.dart';
1355 import 'package:cw_evm/evm_chain_mnemonics.dart';
@@ -1523,7 +1525,6 @@ abstract class EVM {
1525
1526 bool hasPriorityFee(int chainId);
1527
1526 - Future<bool> checkTokenFiatPrice(WalletBase wallet, Erc20Token token);
1528 Future<void> discoverAndAddWalletTokens(WalletBase wallet);
1529 }
1530
@@ -1792,7 +1793,7 @@ Future<void> generatePubspec({
1793 output += '\n$flutterSecureStorage\n';
1794 }
1795
1795 - if (hasEthereum || hasPolygon || hasBase || hasArbitrum) {
1796 + if (hasEthereum || hasPolygon || hasBase || hasArbitrum || hasBsc) {
1797 output += '\n$cwEVM';
1798 }
1799