mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-20 06:08:34 +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:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user