mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-26 06:08:40 +01:00
feat: harden spam detection for solana and evm (#3322)
* feat: harden spam detection for solana and evm * fix: guard scam/impersonator tokens from auto-enabling on EVM and Solana * feat: enhance token property validation for scam detection
This commit is contained in:
@@ -618,74 +618,117 @@ class EVMChainClient {
|
||||
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;
|
||||
|
||||
const maxPages = 3;
|
||||
String? cursor;
|
||||
int pageCount = 0;
|
||||
final List<MoralisWalletTokenBalance> tokens = [];
|
||||
|
||||
for (final item in decodedResponse) {
|
||||
final tokenData = item as Map<String, dynamic>;
|
||||
do {
|
||||
final params = <String, String>{
|
||||
"chain": chainName,
|
||||
if (cursor != null && cursor.isNotEmpty) "cursor": cursor,
|
||||
};
|
||||
|
||||
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,
|
||||
),
|
||||
final uri = Uri.https(
|
||||
'deep-index.moralis.io',
|
||||
'/api/v2.2/wallets/$address/tokens',
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
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 tokens;
|
||||
}
|
||||
|
||||
final decoded = jsonDecode(response.body);
|
||||
if (decoded is! Map<String, dynamic>) return tokens;
|
||||
|
||||
final result = decoded['result'];
|
||||
if (result is! List) return tokens;
|
||||
|
||||
for (final item in result) {
|
||||
if (item is! Map<String, dynamic>) continue;
|
||||
final tokenData = item;
|
||||
|
||||
final nativeRaw = tokenData['native_token'];
|
||||
final nativeToken = nativeRaw is bool
|
||||
? nativeRaw
|
||||
: (nativeRaw?.toString().toLowerCase() == 'true');
|
||||
if (nativeToken) continue;
|
||||
|
||||
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');
|
||||
|
||||
final usdPriceRaw = tokenData['usd_price'];
|
||||
final double? usdPrice = usdPriceRaw is num
|
||||
? usdPriceRaw.toDouble()
|
||||
: (usdPriceRaw is String ? double.tryParse(usdPriceRaw) : null);
|
||||
|
||||
final usdValueRaw = tokenData['usd_value'];
|
||||
final double? usdValue = usdValueRaw is num
|
||||
? usdValueRaw.toDouble()
|
||||
: (usdValueRaw is String ? double.tryParse(usdValueRaw) : null);
|
||||
|
||||
final securityRaw = tokenData['security_score'];
|
||||
final int? securityScore = securityRaw is int
|
||||
? securityRaw
|
||||
: (securityRaw is num
|
||||
? securityRaw.toInt()
|
||||
: (securityRaw is String ? int.tryParse(securityRaw) : null));
|
||||
|
||||
tokens.add(
|
||||
MoralisWalletTokenBalance(
|
||||
contractAddress: contractAddress,
|
||||
name: name,
|
||||
symbol: symbolFiltered,
|
||||
decimals: decimals,
|
||||
iconUrl: iconUrl,
|
||||
balanceWei: balanceWei,
|
||||
possibleSpam: possibleSpam,
|
||||
verifiedContract: verifiedContract,
|
||||
usdPrice: usdPrice,
|
||||
usdValue: usdValue,
|
||||
securityScore: securityScore,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final nextCursor = decoded['cursor'];
|
||||
cursor = nextCursor is String && nextCursor.isNotEmpty ? nextCursor : null;
|
||||
pageCount++;
|
||||
} while (cursor != null && pageCount < maxPages);
|
||||
|
||||
return tokens;
|
||||
} catch (e, stackTrace) {
|
||||
@@ -737,6 +780,9 @@ class MoralisWalletTokenBalance {
|
||||
final BigInt balanceWei;
|
||||
final bool possibleSpam;
|
||||
final bool verifiedContract;
|
||||
final double? usdPrice;
|
||||
final double? usdValue;
|
||||
final int? securityScore;
|
||||
|
||||
MoralisWalletTokenBalance({
|
||||
required this.contractAddress,
|
||||
@@ -747,5 +793,8 @@ class MoralisWalletTokenBalance {
|
||||
required this.balanceWei,
|
||||
required this.possibleSpam,
|
||||
required this.verifiedContract,
|
||||
this.usdPrice,
|
||||
this.usdValue,
|
||||
this.securityScore,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -545,67 +545,112 @@ abstract class EVMChainWalletBase
|
||||
await save();
|
||||
}
|
||||
|
||||
bool isTokenPropertiesSuspicious(Erc20Token token) {
|
||||
bool isTokenWhitelisted = getDefaultTokenContractAddresses
|
||||
.any((element) => element.toLowerCase() == token.contractAddress.toLowerCase());
|
||||
static const _urlLikeSuspiciousMarkers = [
|
||||
't.me',
|
||||
'.me',
|
||||
'telegram',
|
||||
'http',
|
||||
'https',
|
||||
'.com',
|
||||
'.org',
|
||||
'.top',
|
||||
'.live',
|
||||
'.xyz',
|
||||
'www',
|
||||
'🎁',
|
||||
'airdrop',
|
||||
'distribution',
|
||||
];
|
||||
|
||||
final defaultTokenSymbols = EVMChainDefaultTokens.getDefaultTokenSymbols(selectedChainId);
|
||||
static final _suspiciousWordPattern =
|
||||
RegExp(r'\b(bot|claim|reward)\b', caseSensitive: false);
|
||||
|
||||
static const _knownNonEvmNativeSymbols = {
|
||||
'ICP',
|
||||
'SOL',
|
||||
'TRX',
|
||||
'ATOM',
|
||||
'DOT',
|
||||
'ADA',
|
||||
'XRP',
|
||||
'XLM',
|
||||
'XMR',
|
||||
'ALGO',
|
||||
'NEAR',
|
||||
'TON',
|
||||
'HBAR',
|
||||
'APT',
|
||||
'SUI',
|
||||
'KAS',
|
||||
};
|
||||
|
||||
static bool _hasSuspiciousData(String normalized) {
|
||||
final lower = normalized.toLowerCase();
|
||||
if (_urlLikeSuspiciousMarkers.any(lower.contains)) return true;
|
||||
return _suspiciousWordPattern.hasMatch(lower);
|
||||
}
|
||||
|
||||
bool isTokenPropertiesSuspicious(
|
||||
Erc20Token token, {
|
||||
Set<String>? cachedWhitelistLower,
|
||||
Set<String>? cachedDefaultSymbolsUpper,
|
||||
}) {
|
||||
final whitelistLower = cachedWhitelistLower ??
|
||||
getDefaultTokenContractAddresses.map((a) => a.toLowerCase()).toSet();
|
||||
final defaultSymbolsUpper = cachedDefaultSymbolsUpper ??
|
||||
EVMChainDefaultTokens.getDefaultTokenSymbols(selectedChainId).toSet();
|
||||
|
||||
final isTokenWhitelisted = whitelistLower.contains(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',
|
||||
'.org',
|
||||
'.top',
|
||||
'.live',
|
||||
'airdrop',
|
||||
'reward',
|
||||
'distribution',
|
||||
'www',
|
||||
'.xyz',
|
||||
'🎁',
|
||||
'bot',
|
||||
'claim',
|
||||
'reward',
|
||||
];
|
||||
|
||||
final hasSuspiciousData = suspiciousStrings.any(
|
||||
(element) =>
|
||||
normalizedName.toLowerCase().contains(element) ||
|
||||
normalizedSymbol.toLowerCase().contains(element) ||
|
||||
normalizedTitle.toLowerCase().contains(element),
|
||||
);
|
||||
final hasSuspiciousData = _hasSuspiciousData(normalizedName) ||
|
||||
_hasSuspiciousData(normalizedSymbol) ||
|
||||
_hasSuspiciousData(normalizedTitle);
|
||||
|
||||
final nativeSymbol = currency.title.toUpperCase();
|
||||
final hasSuspiciousNativeSymbol = normalizedSymbol == nativeSymbol && !isTokenWhitelisted;
|
||||
|
||||
final hasSuspiciousDefaultTokenSymbol =
|
||||
defaultTokenSymbols.contains(normalizedSymbol) && !isTokenWhitelisted;
|
||||
defaultSymbolsUpper.contains(normalizedSymbol) && !isTokenWhitelisted;
|
||||
|
||||
return hasSuspiciousData || hasSuspiciousNativeSymbol || hasSuspiciousDefaultTokenSymbol;
|
||||
final hasSuspiciousNonEvmNativeSymbol =
|
||||
_knownNonEvmNativeSymbols.contains(normalizedSymbol) && !isTokenWhitelisted;
|
||||
|
||||
return hasSuspiciousData ||
|
||||
hasSuspiciousNativeSymbol ||
|
||||
hasSuspiciousDefaultTokenSymbol ||
|
||||
hasSuspiciousNonEvmNativeSymbol;
|
||||
}
|
||||
|
||||
Future<void> _checkForExistingScamTokens() async {
|
||||
for (var token in erc20Currencies) {
|
||||
bool isPotentialScam = false;
|
||||
String get _scamCheckDoneKey => 'evm_scam_check_v2_done_${walletInfo.name}';
|
||||
|
||||
if (isTokenPropertiesSuspicious(token)) {
|
||||
isPotentialScam = true;
|
||||
Future<void> _checkForExistingScamTokens() async {
|
||||
final prefs = await sharedPrefs.future;
|
||||
if (prefs.getBool(_scamCheckDoneKey) == true) return;
|
||||
|
||||
final whitelistLower =
|
||||
getDefaultTokenContractAddresses.map((a) => a.toLowerCase()).toSet();
|
||||
final defaultSymbolsUpper =
|
||||
EVMChainDefaultTokens.getDefaultTokenSymbols(selectedChainId).toSet();
|
||||
|
||||
for (var token in erc20Currencies) {
|
||||
final suspicious = isTokenPropertiesSuspicious(
|
||||
token,
|
||||
cachedWhitelistLower: whitelistLower,
|
||||
cachedDefaultSymbolsUpper: defaultSymbolsUpper,
|
||||
);
|
||||
|
||||
if (suspicious && !token.isPotentialScam) {
|
||||
token.isPotentialScam = true;
|
||||
token.iconPath = null;
|
||||
await token.save();
|
||||
continue;
|
||||
}
|
||||
|
||||
// For fixing wrongly classified tokens
|
||||
if (!isPotentialScam && token.isPotentialScam) {
|
||||
if (!suspicious && token.isPotentialScam) {
|
||||
token.isPotentialScam = false;
|
||||
|
||||
if (token.iconPath == null || token.iconPath!.isEmpty) {
|
||||
@@ -621,6 +666,8 @@ abstract class EVMChainWalletBase
|
||||
await token.save();
|
||||
}
|
||||
}
|
||||
|
||||
await prefs.setBool(_scamCheckDoneKey, true);
|
||||
}
|
||||
|
||||
Future<MoralisDiscoveryResult> discoverTokensFromMoralis() async {
|
||||
@@ -672,6 +719,9 @@ abstract class EVMChainWalletBase
|
||||
DiscoveredToken(
|
||||
token: newToken,
|
||||
balanceWei: token.balanceWei,
|
||||
verifiedContract: token.verifiedContract,
|
||||
moralisUsdPrice: token.usdPrice,
|
||||
moralisUsdValue: token.usdValue,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1757,10 +1807,16 @@ class GasParamsHandler {
|
||||
class DiscoveredToken {
|
||||
final Erc20Token token;
|
||||
final BigInt balanceWei;
|
||||
final bool verifiedContract;
|
||||
final double? moralisUsdPrice;
|
||||
final double? moralisUsdValue;
|
||||
|
||||
const DiscoveredToken({
|
||||
required this.token,
|
||||
required this.balanceWei,
|
||||
required this.verifiedContract,
|
||||
this.moralisUsdPrice,
|
||||
this.moralisUsdValue,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,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';
|
||||
@@ -135,6 +136,8 @@ abstract class SolanaWalletBase
|
||||
|
||||
splTokensBox = await CakeHive.openBox<SPLToken>(boxName);
|
||||
|
||||
await _checkForExistingScamTokens();
|
||||
|
||||
// Create the privatekey using either the mnemonic or the privateKey
|
||||
_solanaPrivateKey = await getPrivateKey(
|
||||
mnemonic: _mnemonic,
|
||||
@@ -153,6 +156,36 @@ abstract class SolanaWalletBase
|
||||
await save();
|
||||
}
|
||||
|
||||
String get _scamCheckDoneKey => 'solana_scam_check_v2_done_${walletInfo.name}';
|
||||
|
||||
Future<void> _checkForExistingScamTokens() async {
|
||||
if (!splTokensBox.isOpen) return;
|
||||
|
||||
final prefs = await _sharedPrefs.future;
|
||||
if (prefs.getBool(_scamCheckDoneKey) == true) return;
|
||||
|
||||
final defaultMints =
|
||||
DefaultSPLTokens().initialSPLTokens.map((t) => t.mintAddress).toSet();
|
||||
final defaultSymbolsUpper = DefaultSPLTokens()
|
||||
.initialSPLTokens
|
||||
.map((t) => t.symbol.toUpperCase())
|
||||
.toSet();
|
||||
|
||||
for (final token in splTokensBox.values) {
|
||||
final suspicious = isTokenPropertiesSuspicious(
|
||||
token,
|
||||
cachedDefaultMints: defaultMints,
|
||||
cachedDefaultSymbolsUpper: defaultSymbolsUpper,
|
||||
);
|
||||
if (suspicious && !token.isPotentialScam) {
|
||||
token.isPotentialScam = true;
|
||||
await token.save();
|
||||
}
|
||||
}
|
||||
|
||||
await prefs.setBool(_scamCheckDoneKey, true);
|
||||
}
|
||||
|
||||
Future<SolanaPrivateKey> getPrivateKey({
|
||||
String? mnemonic,
|
||||
String? privateKey,
|
||||
@@ -671,7 +704,98 @@ abstract class SolanaWalletBase
|
||||
}
|
||||
}
|
||||
|
||||
static const _urlLikeSuspiciousMarkers = [
|
||||
't.me',
|
||||
'.me',
|
||||
'telegram',
|
||||
'http',
|
||||
'https',
|
||||
'.com',
|
||||
'.org',
|
||||
'.top',
|
||||
'.live',
|
||||
'.xyz',
|
||||
'www',
|
||||
'🎁',
|
||||
'airdrop',
|
||||
'distribution',
|
||||
];
|
||||
|
||||
static final _suspiciousWordPattern =
|
||||
RegExp(r'\b(bot|claim|reward)\b', caseSensitive: false);
|
||||
|
||||
static const _knownNonSolanaNativeSymbols = {
|
||||
'BTC',
|
||||
'ETH',
|
||||
'BNB',
|
||||
'AVAX',
|
||||
'MATIC',
|
||||
'POL',
|
||||
'ICP',
|
||||
'TRX',
|
||||
'ATOM',
|
||||
'DOT',
|
||||
'ADA',
|
||||
'XRP',
|
||||
'XLM',
|
||||
'XMR',
|
||||
'ALGO',
|
||||
'NEAR',
|
||||
'TON',
|
||||
'HBAR',
|
||||
'APT',
|
||||
'SUI',
|
||||
'KAS',
|
||||
};
|
||||
|
||||
static bool _hasSuspiciousData(String normalized) {
|
||||
final lower = normalized.toLowerCase();
|
||||
if (_urlLikeSuspiciousMarkers.any(lower.contains)) return true;
|
||||
return _suspiciousWordPattern.hasMatch(lower);
|
||||
}
|
||||
|
||||
bool isTokenPropertiesSuspicious(
|
||||
SPLToken token, {
|
||||
Set<String>? cachedDefaultMints,
|
||||
Set<String>? cachedDefaultSymbolsUpper,
|
||||
}) {
|
||||
final defaultMints = cachedDefaultMints ??
|
||||
DefaultSPLTokens().initialSPLTokens.map((t) => t.mintAddress).toSet();
|
||||
final defaultSymbolsUpper = cachedDefaultSymbolsUpper ??
|
||||
DefaultSPLTokens()
|
||||
.initialSPLTokens
|
||||
.map((t) => t.symbol.toUpperCase())
|
||||
.toSet();
|
||||
|
||||
final isTokenWhitelisted = defaultMints.contains(token.mintAddress);
|
||||
|
||||
final normalizedName = normalizeHomoglyphs(token.name.trim().toUpperCase());
|
||||
final normalizedSymbol = normalizeHomoglyphs(token.symbol.trim().toUpperCase());
|
||||
final normalizedTitle = normalizeHomoglyphs(token.title.trim().toUpperCase());
|
||||
|
||||
final hasSuspiciousData = _hasSuspiciousData(normalizedName) ||
|
||||
_hasSuspiciousData(normalizedSymbol) ||
|
||||
_hasSuspiciousData(normalizedTitle);
|
||||
|
||||
const nativeSymbol = 'SOL';
|
||||
final hasSuspiciousNativeSymbol = normalizedSymbol == nativeSymbol && !isTokenWhitelisted;
|
||||
|
||||
final hasSuspiciousDefaultTokenSymbol =
|
||||
defaultSymbolsUpper.contains(normalizedSymbol) && !isTokenWhitelisted;
|
||||
|
||||
final hasSuspiciousNonSolanaNativeSymbol =
|
||||
_knownNonSolanaNativeSymbols.contains(normalizedSymbol) && !isTokenWhitelisted;
|
||||
|
||||
return hasSuspiciousData ||
|
||||
hasSuspiciousNativeSymbol ||
|
||||
hasSuspiciousDefaultTokenSymbol ||
|
||||
hasSuspiciousNonSolanaNativeSymbol;
|
||||
}
|
||||
|
||||
Future<void> addSPLToken(SPLToken token) async {
|
||||
final isSuspicious = isTokenPropertiesSuspicious(token);
|
||||
token.isPotentialScam = token.isPotentialScam || isSuspicious;
|
||||
|
||||
await splTokensBox.put(token.mintAddress, token);
|
||||
|
||||
if (token.enabled) {
|
||||
|
||||
+24
-22
@@ -692,29 +692,18 @@ class CWEVM extends EVM {
|
||||
);
|
||||
}
|
||||
|
||||
Future<({double usdValue, bool hasValidFiatPrice})> _getTokenUsdValueAndFiatCheck(
|
||||
Erc20Token token,
|
||||
BigInt balanceWei,
|
||||
) async {
|
||||
Future<double> _fetchFiatApiPriceForToken(Erc20Token token) async {
|
||||
try {
|
||||
final settingsStore = getIt.get<SettingsStore>();
|
||||
final torOnly = settingsStore.fiatApiMode == FiatApiMode.torOnly;
|
||||
|
||||
final price = await FiatConversionService.fetchPrice(
|
||||
return await FiatConversionService.fetchPrice(
|
||||
crypto: token,
|
||||
fiat: FiatCurrency.usd,
|
||||
torOnly: torOnly,
|
||||
);
|
||||
|
||||
final hasValidFiatPrice = price > 0;
|
||||
|
||||
final decimals = token.decimal;
|
||||
final balance = balanceWei.toDouble() / math.pow(10, decimals);
|
||||
final usdValue = balance * price;
|
||||
|
||||
return (usdValue: usdValue, hasValidFiatPrice: hasValidFiatPrice);
|
||||
} catch (e) {
|
||||
return (usdValue: 0.0, hasValidFiatPrice: false);
|
||||
} catch (_) {
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -730,21 +719,34 @@ class CWEVM extends EVM {
|
||||
|
||||
final List<Future<void>> tokenChecks = [];
|
||||
|
||||
final whitelistedContracts =
|
||||
wallet.getDefaultTokenContractAddresses.map((a) => a.toLowerCase()).toSet();
|
||||
|
||||
for (final item in result.newTokens) {
|
||||
tokenChecks.add((() async {
|
||||
final token = item.token;
|
||||
|
||||
final isPropertiesSuspicious = wallet.isTokenPropertiesSuspicious(token);
|
||||
final isWhitelisted = whitelistedContracts.contains(token.contractAddress.toLowerCase());
|
||||
|
||||
final fiatResult = await _getTokenUsdValueAndFiatCheck(
|
||||
token,
|
||||
item.balanceWei,
|
||||
);
|
||||
final isSpam = isPropertiesSuspicious || !fiatResult.hasValidFiatPrice;
|
||||
final moralisPrice = item.moralisUsdPrice;
|
||||
final moralisValue = item.moralisUsdValue ?? 0.0;
|
||||
final hasMoralisPrice = moralisPrice != null && moralisPrice > 0;
|
||||
|
||||
final fiatApiPrice = await _fetchFiatApiPriceForToken(token);
|
||||
final hasFiatApiPrice = fiatApiPrice > 0;
|
||||
|
||||
final isImpersonator =
|
||||
hasFiatApiPrice && !hasMoralisPrice && !isWhitelisted && !item.verifiedContract;
|
||||
|
||||
final isSpam = isPropertiesSuspicious ||
|
||||
token.isPotentialScam ||
|
||||
isImpersonator ||
|
||||
(!hasMoralisPrice && !hasFiatApiPrice);
|
||||
|
||||
token.isPotentialScam = isSpam;
|
||||
|
||||
token.enabled = (fiatResult.usdValue >= _minTokenUsdValue) && !isSpam;
|
||||
token.enabled =
|
||||
hasMoralisPrice && hasFiatApiPrice && (moralisValue >= _minTokenUsdValue) && !isSpam;
|
||||
|
||||
await wallet.addErc20Token(token);
|
||||
})());
|
||||
|
||||
@@ -387,9 +387,11 @@ class CWSolana extends Solana {
|
||||
tokenChecks.add((() async {
|
||||
final token = item.token;
|
||||
|
||||
final isPropertiesSuspicious = wallet.isTokenPropertiesSuspicious(token);
|
||||
|
||||
final fiatResult = await _getTokenUsdValueAndFiatCheck(token, item.balance);
|
||||
|
||||
final isSpam = !fiatResult.hasValidFiatPrice;
|
||||
final isSpam = isPropertiesSuspicious || !fiatResult.hasValidFiatPrice;
|
||||
|
||||
token.isPotentialScam = isSpam;
|
||||
token.enabled = (fiatResult.usdValue >= _minTokenUsdValue) && !isSpam;
|
||||
|
||||
Reference in New Issue
Block a user