Label existing scam tokens (#2164)

* label existing scam tokens because users can get scammed twice ¯\_(ツ)_/¯ * minor ui fix [skip ci]

Omar Hatem committed Apr 7, 2025 at 18:12 UTC 88ebba9236f9efd47f679c255288d4833882f46c
5 files changed +89 -53
cw_core/lib/erc20_token.dart
+2 -2
@@ -17,11 +17,11 @@ class Erc20Token extends CryptoCurrency with HiveObjectMixin {
17 @HiveField(4, defaultValue: true)
18 bool _enabled;
19 @HiveField(5)
20 - final String? iconPath;
20 + String? iconPath;
21 @HiveField(6)
22 final String? tag;
23 @HiveField(7, defaultValue: false)
24 - final bool isPotentialScam;
24 + bool isPotentialScam;
25
26 bool get enabled => _enabled;
27
cw_ethereum/lib/ethereum_wallet.dart
+5 -1
@@ -76,9 +76,13 @@ class EthereumWallet extends EVMChainWallet {
76 await erc20TokensBox.deleteFromDisk();
77
78 // Add all the previous tokens with configs to the new box
79 - evmChainErc20TokensBox.addAll(allValues);
79 + await evmChainErc20TokensBox.addAll(allValues);
80 }
81
82 + @override
83 + List<String> get getDefaultTokenContractAddresses =>
84 + DefaultEthereumErc20Tokens().initialErc20Tokens.map((e) => e.contractAddress).toList();
85 +
86 @override
87 EVMChainTransactionInfo getTransactionInfo(
88 EVMChainTransactionModel transactionModel, String address) {
cw_evm/lib/evm_chain_wallet.dart
+30
@@ -144,6 +144,8 @@ abstract class EVMChainWalletBase
144 // required WalletInfo walletInfo,
145 // });
146
147 + List<String> get getDefaultTokenContractAddresses;
148 +
149 Future<void> initErc20TokensBox();
150
151 String getTransactionHistoryFileName();
@@ -171,6 +173,9 @@ abstract class EVMChainWalletBase
173 await walletAddresses.init();
174 await transactionHistory.init();
175
176 + // check for Already existing scam tokens, cuz users can get scammed twice ¯\_(ツ)_/¯
177 + await _checkForExistingScamTokens();
178 +
179 if (walletInfo.isHardwareWallet) {
180 _evmChainPrivateKey = EvmLedgerCredentials(walletInfo.address);
181 walletAddresses.address = walletInfo.address;
@@ -186,6 +191,31 @@ abstract class EVMChainWalletBase
191 await save();
192 }
193
194 + Future<void> _checkForExistingScamTokens() async {
195 + final baseCurrencySymbols = CryptoCurrency.all.map((e) => e.title.toUpperCase()).toList();
196 +
197 + for (var token in erc20Currencies) {
198 + bool isPotentialScam = false;
199 +
200 + bool isWhitelisted =
201 + getDefaultTokenContractAddresses.any((element) => element == token.contractAddress);
202 +
203 + final tokenSymbol = token.title.toUpperCase();
204 +
205 + // check if the token symbol is the same as any of the base currencies symbols (ETH, SOL, POL, TRX, etc):
206 + // if it is, then it's probably a scam unless it's in the whitelist
207 + if (baseCurrencySymbols.contains(tokenSymbol.trim().toUpperCase()) && !isWhitelisted) {
208 + isPotentialScam = true;
209 + }
210 +
211 + if (isPotentialScam) {
212 + token.isPotentialScam = true;
213 + token.iconPath = null;
214 + await token.save();
215 + }
216 + }
217 + }
218 +
219 @override
220 int calculateEstimatedFee(TransactionPriority priority, int? amount) {
221 {
cw_polygon/lib/polygon_wallet.dart
+4
@@ -49,6 +49,10 @@ class PolygonWallet extends EVMChainWallet {
49 }
50 }
51
52 + @override
53 + List<String> get getDefaultTokenContractAddresses =>
54 + DefaultPolygonErc20Tokens().initialPolygonErc20Tokens.map((e) => e.contractAddress).toList();
55 +
56 @override
57 Future<bool> checkIfScanProviderIsEnabled() async {
58 bool isPolygonScanEnabled = (await sharedPrefs.future).getBool("use_polygonscan") ?? true;
lib/src/screens/send/send_page.dart
+48 -50
@@ -98,7 +98,7 @@ class SendPage extends BasePage {
98 return MergeSemantics(
99 child: SizedBox(
100 height: isMobileView ? 37 : 45,
101 - width: isMobileView ? 47: 45,
101 + width: isMobileView ? 47 : 45,
102 child: ButtonTheme(
103 minWidth: double.minPositive,
104 child: Semantics(
@@ -397,7 +397,6 @@ class SendPage extends BasePage {
397 return LoadingPrimaryButton(
398 key: ValueKey('send_page_send_button_key'),
399 onPressed: () async {
400 -
400 //Request dummy node to get the focus out of the text fields
401 FocusScope.of(context).requestFocus(FocusNode());
402
@@ -507,7 +506,6 @@ class SendPage extends BasePage {
506 Navigator.of(loadingBottomSheetContext!).pop();
507 }
508
510 -
509 if (state is FailureState) {
510 WidgetsBinding.instance.addPostFrameCallback((_) {
511 showPopUp<void>(
@@ -525,7 +523,6 @@ class SendPage extends BasePage {
523 }
524
525 if (state is IsExecutingState) {
528 -
526 // wait a bit to avoid showing the loading dialog if transaction is failed
527 await Future.delayed(const Duration(milliseconds: 300));
528 final currentState = sendViewModel.state;
@@ -584,8 +581,6 @@ class SendPage extends BasePage {
581 });
582 }
583
587 -
588 -
584 if (state is TransactionCommitted) {
585 WidgetsBinding.instance.addPostFrameCallback((_) async {
586 if (!context.mounted) {
@@ -594,7 +589,8 @@ class SendPage extends BasePage {
589
590 newContactAddress = newContactAddress ?? sendViewModel.newContactAddress();
591
597 - if (newContactAddress?.address != null && isRegularElectrumAddress(newContactAddress!.address)) {
592 + if (newContactAddress?.address != null &&
593 + isRegularElectrumAddress(newContactAddress!.address)) {
594 newContactAddress = null;
595 }
596
@@ -606,47 +602,51 @@ class SendPage extends BasePage {
602 builder: (BuildContext bottomSheetContext) {
603 return showContactSheet
604 ? InfoBottomSheet(
609 - currentTheme: currentTheme,
610 - showDontAskMeCheckbox: true,
611 - onCheckboxChanged: (value) => sendViewModel.setShowAddressBookPopup(!value),
612 - titleText: S.of(bottomSheetContext).transaction_sent,
613 - contentImage: 'assets/images/contact_icon.svg',
614 - contentImageColor: Theme.of(context)
615 - .extension<CakeTextTheme>()!
616 - .titleColor,
617 - content: S.of(bottomSheetContext).add_contact_to_address_book,
618 - isTwoAction: true,
619 - leftButtonText: 'No',
620 - rightButtonText: 'Yes',
621 - actionLeftButton: () {
622 - Navigator.of(bottomSheetContext).pop();
623 - Navigator.of(context)
624 - .pushNamedAndRemoveUntil(Routes.dashboard, (route) => false);
625 - RequestReviewHandler.requestReview();
626 - newContactAddress = null;
627 - },
628 - actionRightButton: () {
629 - Navigator.of(bottomSheetContext).pop();
630 - RequestReviewHandler.requestReview();
631 - Navigator.of(context)
632 - .pushNamed(Routes.addressBookAddContact, arguments: newContactAddress);
633 - newContactAddress = null;
634 - },
635 - )
605 + currentTheme: currentTheme,
606 + showDontAskMeCheckbox: true,
607 + onCheckboxChanged: (value) => sendViewModel.setShowAddressBookPopup(!value),
608 + titleText: S.of(bottomSheetContext).transaction_sent,
609 + contentImage: 'assets/images/contact_icon.svg',
610 + contentImageColor: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
611 + content: S.of(bottomSheetContext).add_contact_to_address_book,
612 + isTwoAction: true,
613 + leftButtonText: 'No',
614 + rightButtonText: 'Yes',
615 + actionLeftButton: () {
616 + Navigator.of(bottomSheetContext).pop();
617 + if (context.mounted) {
618 + Navigator.of(context)
619 + .pushNamedAndRemoveUntil(Routes.dashboard, (route) => false);
620 + }
621 + RequestReviewHandler.requestReview();
622 + newContactAddress = null;
623 + },
624 + actionRightButton: () {
625 + Navigator.of(bottomSheetContext).pop();
626 + RequestReviewHandler.requestReview();
627 + if (context.mounted) {
628 + Navigator.of(context).pushNamed(Routes.addressBookAddContact,
629 + arguments: newContactAddress);
630 + }
631 + newContactAddress = null;
632 + },
633 + )
634 : InfoBottomSheet(
637 - currentTheme: currentTheme,
638 - titleText: S.of(bottomSheetContext).transaction_sent,
639 - contentImage: 'assets/images/birthday_cake.svg',
640 - actionButtonText: S.of(bottomSheetContext).close,
641 - actionButtonKey: ValueKey('send_page_sent_dialog_ok_button_key'),
642 - actionButton: () {
643 - Navigator.of(bottomSheetContext).pop();
644 - Navigator.of(context)
645 - .pushNamedAndRemoveUntil(Routes.dashboard, (route) => false);
646 - RequestReviewHandler.requestReview();
647 - newContactAddress = null;
648 - },
649 - );
635 + currentTheme: currentTheme,
636 + titleText: S.of(bottomSheetContext).transaction_sent,
637 + contentImage: 'assets/images/birthday_cake.svg',
638 + actionButtonText: S.of(bottomSheetContext).close,
639 + actionButtonKey: ValueKey('send_page_sent_dialog_ok_button_key'),
640 + actionButton: () {
641 + Navigator.of(bottomSheetContext).pop();
642 + if (context.mounted) {
643 + Navigator.of(context)
644 + .pushNamedAndRemoveUntil(Routes.dashboard, (route) => false);
645 + }
646 + RequestReviewHandler.requestReview();
647 + newContactAddress = null;
648 + },
649 + );
650 },
651 );
652
@@ -678,8 +678,7 @@ class SendPage extends BasePage {
678 currentTheme: currentTheme,
679 titleText: S.of(bottomSheetContext).proceed_on_device,
680 contentImage: 'assets/images/hardware_wallet/ledger_nano_x.png',
681 - contentImageColor:
682 - Theme.of(context).extension<CakeTextTheme>()!.titleColor,
681 + contentImageColor: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
682 content: S.of(bottomSheetContext).proceed_on_device_description,
683 isTwoAction: false,
684 actionButtonText: S.of(context).cancel,
@@ -778,5 +777,4 @@ class SendPage extends BasePage {
777
778 return isValid;
779 }
781 -
780 }