CW-1228: Automatically detect wallet tokens for EVM chains (#2827)
What changed, and why it matters
This commit adds a feature that automatically finds and lists Ethereum-compatible tokens a user owns, then tries to protect them from scam tokens. It fetches token data from an external service (Moralis), marks tokens as suspicious if their names/symbols contain scammy keywords or mimic well-known coins using look-alike letters (like Cyrillic or Greek characters), and disables tokens that fail a fiat price check. The change is primarily a defensive security improvement, but it also introduces new code paths that handle external data and user balances, which carry some risk if the checks can be bypassed or the external service is untrusted.
Review the Moralis API response handling for robust validation and error paths; ensure the homoglyph map covers additional confusable Unicode characters; verify that fiat-price failures cannot be abused to hide legitimate tokens; consider adding user-visible warnings rather than silently disabling tokens; and audit the whitelisted-token auto-enable logic to confirm it cannot be triggered by contract-address collisions or chain-specific address reuse.
Security signals we found
New external API integration (Moralis) for token metadata and balances
New homoglyph normalization to detect spoofed token symbols
Heuristic scam detection based on symbol/name keywords and base-currency collision
Fiat price check used as a spam/scam signal
Automatic enabling of whitelisted tokens with non-zero balance
Potential for false positives/negatives in scam detection heuristics
Evidence from the diff
The patch implements automatic EVM token discovery via Moralis API (fetchWalletTokensFromMoralis), adds a homoglyph normalizer to detect spoofed token symbols, and extends scam detection heuristics (suspicious strings, base-currency symbol collision, fiat price presence). Discovered tokens are added to the wallet; tokens flagged as potential scams or lacking a fiat price are disabled by default. Whitelisted tokens with a balance are enabled on import. UI changes sort tokens with balance first and improve balance row layout. The commit is defensive in nature but expands the attack surface by importing third-party token metadata and relying on heuristics that may be incomplete.
Changed components
cw_evm/lib/clients/evm_chain_client.dartcw_evm/lib/evm_chain_wallet.dartlib/evm/cw_evm.dartlib/reactions/on_current_wallet_change.dartcw_core/lib/utils/homoglyph_normalizer.dartlib/view_model/dashboard/balance_view_model.dartlib/src/screens/dashboard/pages/balance/balance_row_widget.darttool/configure.dartInspect captured patch +419 / −73
diff --git a/cw_core/lib/utils/homoglyph_normalizer.dart b/cw_core/lib/utils/homoglyph_normalizer.dart
new file mode 100644
index 00000000..36d15dde
--- /dev/null
+++ b/cw_core/lib/utils/homoglyph_normalizer.dart
@@ -0,0 +1,68 @@
+/// Normalizes homoglyph characters (Cyrillic, Greek, etc.) to their ASCII equivalents
+/// to detect spoofing attacks like "UЅDС" (Cyrillic) vs "USDC" (ASCII)
+String normalizeHomoglyphs(String text) {
+ final homoglyphMap = {
+ // Cyrillic letters that look like Latin
+ 'А': 'A', // Cyrillic A
+ 'В': 'B', // Cyrillic Ve
+ 'Е': 'E', // Cyrillic Ie
+ 'К': 'K', // Cyrillic Ka
+ 'М': 'M', // Cyrillic Em
+ 'Н': 'H', // Cyrillic En
+ 'О': 'O', // Cyrillic O
+ 'Р': 'P', // Cyrillic Er
+ 'С': 'C', // Cyrillic Es
+ 'Т': 'T', // Cyrillic Te
+ 'У': 'Y', // Cyrillic U
+ 'Х': 'X', // Cyrillic Kha
+ 'а': 'a',
+ 'в': 'b',
+ 'е': 'e',
+ 'к': 'k',
+ 'м': 'm',
+ 'н': 'h',
+ 'о': 'o',
+ 'р': 'p',
+ 'с': 'c',
+ 'т': 't',
+ 'у': 'y',
+ 'х': 'x',
+ 'Ѕ': 'S', // Cyrillic Dze (looks like S)
+ 'ѕ': 's',
+ 'І': 'I', // Cyrillic I
+ 'і': 'i',
+ 'Ј': 'J', // Cyrillic Je
+ 'ј': 'j',
+ // Greek letters that look like Latin
+ 'Α': 'A', // Alpha
+ 'Β': 'B', // Beta
+ 'Ε': 'E', // Epsilon
+ 'Ζ': 'Z', // Zeta
+ 'Η': 'H', // Eta
+ 'Ι': 'I', // Iota
+ 'Κ': 'K', // Kappa
+ 'Μ': 'M', // Mu
+ 'Ν': 'N', // Nu
+ 'Ο': 'O', // Omicron
+ 'Ρ': 'P', // Rho
+ 'Τ': 'T', // Tau
+ 'Υ': 'Y', // Upsilon
+ 'Χ': 'X', // Chi
+ 'α': 'a',
+ 'β': 'b',
+ 'ε': 'e',
+ 'ζ': 'z',
+ 'η': 'h',
+ 'ι': 'i',
+ 'κ': 'k',
+ 'μ': 'm',
+ 'ν': 'n',
+ 'ο': 'o',
+ 'ρ': 'p',
+ 'τ': 't',
+ 'υ': 'y',
+ 'χ': 'x',
+ };
+
+ return text.split('').map((char) => homoglyphMap[char] ?? char).join('');
+}
diff --git a/cw_evm/lib/clients/evm_chain_client.dart b/cw_evm/lib/clients/evm_chain_client.dart
index 5bb5d1ab..c1676c5d 100644
--- a/cw_evm/lib/clients/evm_chain_client.dart
+++ b/cw_evm/lib/clients/evm_chain_client.dart
@@ -518,6 +518,10 @@ class EVMChainClient {
}
Future<Erc20Token?> getErc20TokenFromMoralis(String contractAddress, String chainName) async {
+ if (secrets.moralisApiKey.isEmpty) {
+ printV('Moralis API key is empty, cannot fetch token info');
+ return null;
+ }
final uri = Uri.https(
'deep-index.moralis.io',
'/api/v2.2/erc20/metadata',
@@ -567,6 +571,93 @@ class EVMChainClient {
);
}
+ Future<List<MoralisWalletTokenBalance>> fetchWalletTokensFromMoralis(
+ String address,
+ String chainName,
+ ) async {
+ try {
+ if (secrets.moralisApiKey.isEmpty) {
+ printV('Moralis API key is empty, cannot fetch wallet tokens');
+ return [];
+ }
+
+ final uri = Uri.https(
+ 'deep-index.moralis.io',
+ '/api/v2.2/$address/erc20',
+ {
+ "chain": chainName,
+ },
+ );
+
+ final response = await client.get(
+ uri,
+ headers: {
+ "Accept": "application/json",
+ "X-API-Key": secrets.moralisApiKey,
+ },
+ );
+
+ if (response.statusCode < 200 || response.statusCode >= 300) {
+ printV('Moralis API returned invalid status code: ${response.statusCode}');
+ return [];
+ }
+
+ final decodedResponse = jsonDecode(response.body) as List;
+
+ final List<MoralisWalletTokenBalance> tokens = [];
+
+ for (final item in decodedResponse) {
+ final tokenData = item as Map<String, dynamic>;
+
+ final balanceStr = tokenData['balance'] as String? ?? '0';
+ final balanceWei = BigInt.tryParse(balanceStr) ?? BigInt.zero;
+ if (balanceWei == BigInt.zero) continue;
+
+ final contractAddress = (tokenData['token_address'] as String? ?? '').toLowerCase();
+ final name = (tokenData['name'] as String? ?? '').toString();
+ final symbol = (tokenData['symbol'] as String? ?? '').toString();
+ final symbolFiltered = symbol.replaceFirst(RegExp('^\\\$'), '');
+
+ final decimalsRaw = tokenData['decimals'];
+ final decimals =
+ decimalsRaw is int ? decimalsRaw : int.tryParse(decimalsRaw.toString()) ?? 18;
+
+ final logo = tokenData['logo'] as String?;
+ final thumbnail = tokenData['thumbnail'] as String?;
+ final iconUrl = logo ?? thumbnail;
+
+ final possibleSpamRaw = tokenData['possible_spam'];
+ final possibleSpam = possibleSpamRaw is bool
+ ? possibleSpamRaw
+ : (possibleSpamRaw.toString().toLowerCase() == 'true');
+
+ final verifiedContractRaw = tokenData['verified_contract'];
+ final verifiedContract = verifiedContractRaw is bool
+ ? verifiedContractRaw
+ : (verifiedContractRaw.toString().toLowerCase() == 'true');
+
+ tokens.add(
+ MoralisWalletTokenBalance(
+ contractAddress: contractAddress,
+ name: name,
+ symbol: symbolFiltered,
+ decimals: decimals,
+ iconUrl: iconUrl,
+ balanceWei: balanceWei,
+ possibleSpam: possibleSpam,
+ verifiedContract: verifiedContract,
+ ),
+ );
+ }
+
+ return tokens;
+ } catch (e, stackTrace) {
+ printV('Error fetching wallet tokens from Moralis: ${e.toString()}');
+ printV('Stack trace: ${stackTrace.toString()}');
+ return [];
+ }
+ }
+
Uint8List hexToBytes(String hexString) {
return Uint8List.fromList(
hex.HEX.decode(hexString.startsWith('0x') ? hexString.substring(2) : hexString));
@@ -599,3 +690,25 @@ class EVMChainClient {
// return exponent;
// }
}
+
+class MoralisWalletTokenBalance {
+ final String contractAddress;
+ final String name;
+ final String symbol;
+ final int decimals;
+ final String? iconUrl;
+ final BigInt balanceWei;
+ final bool possibleSpam;
+ final bool verifiedContract;
+
+ MoralisWalletTokenBalance({
+ required this.contractAddress,
+ required this.name,
+ required this.symbol,
+ required this.decimals,
+ this.iconUrl,
+ required this.balanceWei,
+ required this.possibleSpam,
+ required this.verifiedContract,
+ });
+}
diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart
index c7f20a79..b9fa6dcd 100644
--- a/cw_evm/lib/evm_chain_wallet.dart
+++ b/cw_evm/lib/evm_chain_wallet.dart
@@ -15,6 +15,7 @@ import 'package:cw_core/pending_transaction.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/transaction_priority.dart';
+import 'package:cw_core/utils/homoglyph_normalizer.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_addresses.dart';
import 'package:cw_core/wallet_base.dart';
@@ -440,24 +441,50 @@ abstract class EVMChainWalletBase
await save();
}
- Future<void> _checkForExistingScamTokens() async {
+ bool isTokenPropertiesSuspicious(Erc20Token token) {
final baseCurrencySymbols = CryptoCurrency.all.map((e) => e.title.toUpperCase()).toList();
- for (var token in erc20Currencies) {
- bool isPotentialScam = false;
+ bool isTokenWhitelisted = getDefaultTokenContractAddresses
+ .any((element) => element.toLowerCase() == token.contractAddress.toLowerCase());
+
+ // Normalize the token data to check for homoglyph spoofing attack, characters that look like ASCII (Cyrillic, Greek, etc.)
+ final normalizedName = normalizeHomoglyphs(token.name.trim().toUpperCase());
+ final normalizedSymbol = normalizeHomoglyphs(token.symbol.trim().toUpperCase());
+ final normalizedTitle = normalizeHomoglyphs(token.title.trim().toUpperCase());
+
+ final suspiciousStrings = [
+ 't.me',
+ '.me',
+ 'telegram',
+ 'http',
+ 'https',
+ '.com',
+ 'airdrop',
+ 'www',
+ '.xyz',
+ '🎁',
+ ];
+
+ final hasSuspiciousData = suspiciousStrings.any(
+ (element) =>
+ normalizedName.toLowerCase().contains(element) ||
+ normalizedSymbol.toLowerCase().contains(element) ||
+ normalizedTitle.toLowerCase().contains(element),
+ );
- bool isWhitelisted = getDefaultTokenContractAddresses
- .any((element) => element.toLowerCase() == token.contractAddress.toLowerCase());
+ // Check if the token symbol is the same as any of the base currencies symbols (ETH, SOL, POL, TRX, etc).
+ // If it is, then it's probably a scam unless it's in the whitelist.
+ final hasSuspiciousSymbol = baseCurrencySymbols.contains(normalizedSymbol);
- final tokenSymbol = token.title.toUpperCase();
+ return hasSuspiciousData || (hasSuspiciousSymbol && !isTokenWhitelisted);
+ }
- // check if the token symbol is the same as any of the base currencies symbols (ETH, SOL, POL, TRX, etc):
- // if it is, then it's probably a scam unless it's in the whitelist
- if (baseCurrencySymbols.contains(tokenSymbol.trim().toUpperCase()) && !isWhitelisted) {
- isPotentialScam = true;
- }
+ Future<void> _checkForExistingScamTokens() async {
+ for (var token in erc20Currencies) {
+ bool isPotentialScam = false;
- if (isPotentialScam) {
+ if (isTokenPropertiesSuspicious(token)) {
+ isPotentialScam = true;
token.isPotentialScam = true;
token.iconPath = null;
await token.save();
@@ -482,6 +509,61 @@ abstract class EVMChainWalletBase
}
}
+ Future<List<Erc20Token>> discoverTokensFromMoralis() async {
+ try {
+ if (!evmChainErc20TokensBox.isOpen) return [];
+
+ final address = walletAddresses.address;
+ if (address.isEmpty) return [];
+
+ final chainName = EVMChainUtils.getDefaultTokenSymbol(selectedChainId).toLowerCase();
+
+ final walletTokens = await _client.fetchWalletTokensFromMoralis(address, chainName);
+ if (walletTokens.isEmpty) return [];
+
+ final existingTokenAddresses = {
+ for (final token in evmChainErc20TokensBox.values)
+ token.contractAddress.toLowerCase(): token,
+ };
+
+ final whitelistedTokenAddresses =
+ getDefaultTokenContractAddresses.map((a) => a.toLowerCase()).toSet();
+
+ final List<Erc20Token> newTokens = [];
+
+ for (final token in walletTokens) {
+ final addr = token.contractAddress.toLowerCase();
+
+ final existingToken = existingTokenAddresses[addr];
+ if (existingToken != null) {
+ if (whitelistedTokenAddresses.contains(addr) && !existingToken.enabled) {
+ existingToken.enabled = true;
+ await existingToken.save();
+ await addErc20Token(existingToken);
+ }
+ continue;
+ }
+
+ final newToken = Erc20Token(
+ name: token.name,
+ symbol: token.symbol,
+ contractAddress: addr,
+ decimal: token.decimals,
+ iconPath: token.iconUrl,
+ tag: EVMChainUtils.getDefaultTokenTag(selectedChainId),
+ isPotentialScam: token.possibleSpam,
+ );
+
+ newTokens.add(newToken);
+ }
+
+ return newTokens;
+ } catch (e) {
+ printV('Error discovering tokens from Moralis: ${e.toString()}');
+ return [];
+ }
+ }
+
@override
int calculateEstimatedFee(TransactionPriority priority, int? amount) => 0;
diff --git a/lib/evm/cw_evm.dart b/lib/evm/cw_evm.dart
index 952a2a04..eff7c40c 100644
--- a/lib/evm/cw_evm.dart
+++ b/lib/evm/cw_evm.dart
@@ -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 (_) {}
+ }
}
diff --git a/lib/reactions/on_current_wallet_change.dart b/lib/reactions/on_current_wallet_change.dart
index d3743675..8c23db2d 100644
--- a/lib/reactions/on_current_wallet_change.dart
+++ b/lib/reactions/on_current_wallet_change.dart
@@ -107,6 +107,10 @@ void startCurrentWalletChangeReaction(
await wallet.walletInfo.save();
}
+
+ if (isEVMCompatibleChain(wallet.type)) {
+ await evm!.discoverAndAddWalletTokens(wallet);
+ }
} catch (e) {
printV(e.toString());
}
diff --git a/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart b/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart
index 766914f7..e2fa6706 100644
--- a/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart
+++ b/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart
@@ -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,
+ ),
),
),
- ],
- ),
- ),
- 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(
- height: 1,
- ),
+ if (hasAdditionalBalance)
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 4),
+ child: Icon(
+ Icons.help_outline,
+ size: 16,
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
+ ),
+ ),
+ ],
+ ),
),
- 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,
+ 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,
),
- ],
+ 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,
diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart
index ded39420..8bdd78cb 100644
--- a/lib/view_model/dashboard/balance_view_model.dart
+++ b/lib/view_model/dashboard/balance_view_model.dart
@@ -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(
diff --git a/tool/configure.dart b/tool/configure.dart
index 4e2fda08..074c3f1c 100644
--- a/tool/configure.dart
+++ b/tool/configure.dart
@@ -1345,6 +1345,10 @@ import 'package:web3dart/web3dart.dart';
""";
const evmCWHeaders = """
+import 'package:cake_wallet/core/fiat_conversion_service.dart';
+import 'package:cake_wallet/di.dart';
+import 'package:cake_wallet/entities/fiat_api_mode.dart';
+import 'package:cake_wallet/store/settings_store.dart';
import 'package:cw_evm/utils/evm_chain_formatter.dart';
import 'package:cw_evm/evm_chain_mnemonics.dart';
import 'package:cw_evm/evm_chain_registry.dart';
@@ -1518,6 +1522,9 @@ abstract class EVM {
String? getExplorerUrlForChainId(int chainId, {bool showProtocol = true});
bool hasPriorityFee(int chainId);
+
+ Future<bool> checkTokenFiatPrice(WalletBase wallet, Erc20Token token);
+ Future<void> discoverAndAddWalletTokens(WalletBase wallet);
}
class ChainInfo {
Why this scored 50/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.