mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-17 05:41:29 +02:00
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:
@@ -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('');
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
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),
|
||||
);
|
||||
|
||||
// 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);
|
||||
|
||||
return hasSuspiciousData || (hasSuspiciousSymbol && !isTokenWhitelisted);
|
||||
}
|
||||
|
||||
Future<void> _checkForExistingScamTokens() async {
|
||||
for (var token in erc20Currencies) {
|
||||
bool isPotentialScam = false;
|
||||
|
||||
bool isWhitelisted = getDefaultTokenContractAddresses
|
||||
.any((element) => element.toLowerCase() == token.contractAddress.toLowerCase());
|
||||
|
||||
final tokenSymbol = token.title.toUpperCase();
|
||||
|
||||
// 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) {
|
||||
if (isTokenPropertiesSuspicious(token)) {
|
||||
isPotentialScam = true;
|
||||
}
|
||||
|
||||
if (isPotentialScam) {
|
||||
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;
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user