CW-1228: Automatically detect wallet tokens for EVM chains (#2827)

* 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
This commit is contained in:
David Adegoke
2026-02-05 13:53:39 +01:00
committed by GitHub
parent 308de0389d
commit 4f8cccce4f
8 changed files with 419 additions and 73 deletions
+52
View File
@@ -513,4 +513,56 @@ class CWEVM extends EVM {
@override
bool hasPriorityFee(int chainId) => EVMChainUtils.hasPriorityFee(chainId);
@override
Future<bool> checkTokenFiatPrice(WalletBase wallet, Erc20Token token) async {
try {
final settingsStore = getIt.get<SettingsStore>();
final fiatCurrency = settingsStore.fiatCurrency;
final torOnly = settingsStore.fiatApiMode == FiatApiMode.torOnly;
final price = await FiatConversionService.fetchPrice(
crypto: token,
fiat: fiatCurrency,
torOnly: torOnly,
);
return price > 0;
} catch (e) {
return false;
}
}
@override
Future<void> discoverAndAddWalletTokens(WalletBase wallet) async {
if (wallet is! EVMChainWallet) return;
try {
final discoveredTokens = await wallet.discoverTokensFromMoralis();
if (discoveredTokens.isEmpty) return;
final List<Future<void>> tokenChecks = [];
for (final token in discoveredTokens) {
tokenChecks.add((() async {
final isPropertiesSuspicious = wallet.isTokenPropertiesSuspicious(token);
bool hasValidFiatPrice = true;
if (!isPropertiesSuspicious) {
hasValidFiatPrice = await checkTokenFiatPrice(wallet, token);
}
final isSpam = isPropertiesSuspicious || !hasValidFiatPrice;
token.isPotentialScam = isSpam;
token.enabled = !isSpam;
await wallet.addErc20Token(token);
})());
}
await Future.wait(tokenChecks);
} catch (_) {}
}
}
@@ -107,6 +107,10 @@ void startCurrentWalletChangeReaction(
await wallet.walletInfo.save();
}
if (isEVMCompatibleChain(wallet.type)) {
await evm!.discoverAndAddWalletTokens(wallet);
}
} catch (e) {
printV(e.toString());
}
@@ -101,78 +101,83 @@ class BalanceRowWidget extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: hasAdditionalBalance
? () => _showBalanceDescription(
context, S.of(context).available_balance_description)
: null,
child: Row(
children: [
Semantics(
hint: 'Double tap to see more information',
container: true,
child: Text(
'${availableBalanceLabel}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
height: 1,
),
),
),
if (hasAdditionalBalance)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Icon(
Icons.help_outline,
size: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: hasAdditionalBalance
? () => _showBalanceDescription(
context, S.of(context).available_balance_description)
: null,
child: Row(
children: [
Semantics(
hint: 'Double tap to see more information',
container: true,
child: Text(
'${availableBalanceLabel}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
height: 1,
),
),
),
],
if (hasAdditionalBalance)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Icon(
Icons.help_outline,
size: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
),
SizedBox(height: 6),
AutoSizeText(
availableBalance,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w900,
fontSize: 24,
height: 1,
),
maxLines: 1,
textAlign: TextAlign.start,
),
SizedBox(height: 6),
if (isTestnet)
Text(
S.of(context).testnet_coins_no_value,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
SizedBox(height: 6),
AutoSizeText(
availableBalance,
minFontSize: 16,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w900,
fontSize: 24,
height: 1,
),
maxLines: 1,
textAlign: TextAlign.start,
),
if (!isTestnet)
Text(
'${availableFiatBalance}',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurfaceVariant,
height: 1,
),
),
],
SizedBox(height: 6),
if (isTestnet)
Text(
S.of(context).testnet_coins_no_value,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
height: 1,
),
),
if (!isTestnet)
Text(
'${availableFiatBalance}',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurfaceVariant,
height: 1,
),
),
],
),
),
SizedBox(
//width: min(MediaQuery.of(context).size.width * 0.2, 100),
child: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
CakeImageWidget(
imageUrl: currency.iconPath,
@@ -7,6 +7,7 @@ import 'package:cw_core/transaction_history.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/balance.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/erc20_token.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cake_wallet/generated/i18n.dart';
@@ -378,6 +379,20 @@ abstract class BalanceViewModelBase with Store {
if (a.asset == wallet.currency) return -1;
}
if (isEVMCompatibleChain(wallet.type)) {
final aIsToken = a.asset is Erc20Token;
final bIsToken = b.asset is Erc20Token;
final aHasBalance = (double.tryParse(a.availableBalance) ?? 0) > 0;
final bHasBalance = (double.tryParse(b.availableBalance) ?? 0) > 0;
// Adding this so tokens with balance come before tokens without balance
if (aIsToken && bIsToken) {
if (aHasBalance && !bHasBalance) return -1;
if (!aHasBalance && bHasBalance) return 1;
}
}
switch (sortBalanceBy) {
case SortBalanceBy.FiatBalance:
final aFiatBalance = _getFiatBalance(