CW-1103:Token Validation Issues (#2327)

* feat(token_validation): Improve flow for adding new tokens across wallets This change: - Implements check to see if a token is already added, preventing duplicates - Triggers dialog warning if its a duplicate token - Takes EVM Chains contract adddress case-insensitivity when making checks for potential scams. * refactor(token_validation): Modify token management flow This change: - Removes duplicate token check during token addition in EVMChainWalletBase. - Introduces a flag to indicate if a token is being edited - Adjusts token addition validation to bypass checks when editing an existing token. * Update lib/src/screens/dashboard/edit_token_page.dart --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

David Adegoke committed Jun 24, 2025 at 03:47 UTC af89603b81c5c62b39c44def92ff58fe34791a71
37 files changed +127 -5
cw_evm/lib/evm_chain_wallet.dart
+12 -2
@@ -198,8 +198,8 @@ abstract class EVMChainWalletBase
198 for (var token in erc20Currencies) {
199 bool isPotentialScam = false;
200
201 - bool isWhitelisted =
202 - getDefaultTokenContractAddresses.any((element) => element == token.contractAddress);
201 + bool isWhitelisted = getDefaultTokenContractAddresses
202 + .any((element) => element.toLowerCase() == token.contractAddress.toLowerCase());
203
204 final tokenSymbol = token.title.toUpperCase();
205
@@ -214,6 +214,16 @@ abstract class EVMChainWalletBase
214 token.iconPath = null;
215 await token.save();
216 }
217 +
218 + // For fixing wrongly classified tokens
219 + if (!isPotentialScam && token.isPotentialScam) {
220 + token.isPotentialScam = false;
221 + final iconPath = CryptoCurrency.all
222 + .firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase())
223 + .iconPath;
224 + token.iconPath = iconPath;
225 + await token.save();
226 + }
227 }
228 }
229
lib/ethereum/cw_ethereum.dart
+10 -2
@@ -199,8 +199,16 @@ class CWEthereum extends Ethereum {
199 }
200
201 @override
202 - List<String> getDefaultTokenContractAddresses() =>
203 - DefaultEthereumErc20Tokens().initialErc20Tokens.map((e) => e.contractAddress).toList();
202 + List<String> getDefaultTokenContractAddresses() {
203 + return DefaultEthereumErc20Tokens().initialErc20Tokens.map((e) => e.contractAddress).toList();
204 + }
205 +
206 +
207 + @override
208 + bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress) {
209 + final ethereumWallet = wallet as EthereumWallet;
210 + return ethereumWallet.erc20Currencies.any((element) => element.contractAddress.toLowerCase() == contractAddress.toLowerCase());
211 + }
212
213 Future<PendingTransaction> createTokenApproval(WalletBase wallet, BigInt amount, String spender,
214 CryptoCurrency token, TransactionPriority priority) =>
lib/polygon/cw_polygon.dart
+6
@@ -216,4 +216,10 @@ class CWPolygon extends Polygon {
216 .initialPolygonErc20Tokens
217 .map((e) => e.contractAddress)
218 .toList();
219 +
220 + @override
221 + bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress) {
222 + final polygonWallet = wallet as PolygonWallet;
223 + return polygonWallet.erc20Currencies.any((element) => element.contractAddress.toLowerCase() == contractAddress.toLowerCase());
224 + }
225 }
lib/solana/cw_solana.dart
+6
@@ -155,4 +155,10 @@ class CWSolana extends Solana {
155 List<String> getDefaultTokenContractAddresses() {
156 return DefaultSPLTokens().initialSPLTokens.map((e) => e.mintAddress).toList();
157 }
158 +
159 + @override
160 + bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress) {
161 + final solanaWallet = wallet as SolanaWallet;
162 + return solanaWallet.splTokenCurrencies.any((element) => element.mintAddress == contractAddress);
163 + }
164 }
lib/src/screens/dashboard/edit_token_page.dart
+22
@@ -73,6 +73,7 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
73
74 bool _showDisclaimer = false;
75 bool _disclaimerChecked = false;
76 + bool isEditingToken = false;
77
78 @override
79 void initState() {
@@ -88,6 +89,8 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
89 _tokenSymbolController.text = widget.token!.title;
90 _tokenDecimalController.text = widget.token!.decimals.toString();
91 _tokenIconPathController.text = widget.token?.iconPath ?? '';
92 +
93 + isEditingToken = true;
94 }
95
96 if (widget.initialContractAddress != null) {
@@ -200,6 +203,25 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
203 onPressed: () async {
204 if (_formKey.currentState!.validate() &&
205 (!_showDisclaimer || _disclaimerChecked)) {
206 + final isTokenAlreadyAdded = isEditingToken
207 + ? false
208 + : await widget.homeSettingsViewModel
209 + .checkIfTokenIsAlreadyAdded(_contractAddressController.text);
210 + if (isTokenAlreadyAdded) {
211 + showPopUp<void>(
212 + context: context,
213 + builder: (dialogContext) {
214 + return AlertWithOneAction(
215 + alertTitle: S.current.warning,
216 + alertContent: S.of(context).token_already_exists,
217 + buttonText: S.of(context).ok,
218 + buttonAction: () => Navigator.of(dialogContext).pop(),
219 + );
220 + },
221 + );
222 + return;
223 + }
224 +
225 final isWhitelisted = await widget.homeSettingsViewModel
226 .checkIfTokenIsWhitelisted(_contractAddressController.text);
227
lib/tron/cw_tron.dart
+6
@@ -138,4 +138,10 @@ class CWTron extends Tron {
138 List<String> getDefaultTokenContractAddresses() {
139 return DefaultTronTokens().initialTronTokens.map((e) => e.contractAddress).toList();
140 }
141 +
142 + @override
143 + bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress) {
144 + final tronWallet = wallet as TronWallet;
145 + return tronWallet.tronTokenCurrencies.any((element) => element.contractAddress == contractAddress);
146 + }
147 }
lib/view_model/dashboard/home_settings_view_model.dart
+26 -1
@@ -126,6 +126,31 @@ abstract class HomeSettingsViewModelBase with Store {
126 }
127 }
128
129 + @action
130 + bool checkIfTokenIsAlreadyAdded(String contractAddress) {
131 + if (_balanceViewModel.wallet.type == WalletType.ethereum) {
132 + return ethereum!.isTokenAlreadyAdded(_balanceViewModel.wallet, contractAddress);
133 + }
134 +
135 + if (_balanceViewModel.wallet.type == WalletType.polygon) {
136 + return polygon!.isTokenAlreadyAdded(_balanceViewModel.wallet, contractAddress);
137 + }
138 +
139 + if (_balanceViewModel.wallet.type == WalletType.solana) {
140 + return solana!.isTokenAlreadyAdded(_balanceViewModel.wallet, contractAddress);
141 + }
142 +
143 + if (_balanceViewModel.wallet.type == WalletType.tron) {
144 + return tron!.isTokenAlreadyAdded(_balanceViewModel.wallet, contractAddress);
145 + }
146 +
147 + if (_balanceViewModel.wallet.type == WalletType.zano) {
148 + return zano!.isTokenAlreadyAdded(_balanceViewModel.wallet, contractAddress);
149 + }
150 +
151 + return false;
152 + }
153 +
154 @action
155 Future<void> deleteToken(CryptoCurrency token) async {
156 try {
@@ -297,7 +322,7 @@ abstract class HomeSettingsViewModelBase with Store {
322 required bool isEthereum,
323 }) async {
324 final uri = Uri.https(
300 - "api.etherscan.io",
325 + "api.etherscan.io",
326 "/v2/api",
327 {
328 "chainid": isEthereum ? "1" : "137",
lib/zano/cw_zano.dart
+6
@@ -136,4 +136,10 @@ class CWZano extends Zano {
136 Map<String, List<int>> debugCallLength() {
137 return api.debugCallLength();
138 }
139 +
140 + @override
141 + bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress) {
142 + final zanoWallet = wallet as ZanoWallet;
143 + return zanoWallet.zanoAssets.values.any((element) => element?.assetId == contractAddress);
144 + }
145 }
res/values/strings_ar.arb
+1
@@ -913,6 +913,7 @@
913 "tip": "بقشيش:",
914 "to": "ل",
915 "today": "اليوم",
916 + "token_already_exists": "رمز موجود بالفعل",
917 "token_contract_address": "عنوان عقد الرمز",
918 "token_decimal": "رمز عشري",
919 "token_name": "اسم الرمز ، على سبيل المثال: Tether",
res/values/strings_bg.arb
+1
@@ -913,6 +913,7 @@
913 "tip": "Tip:",
914 "to": "Да",
915 "today": "Днес",
916 + "token_already_exists": "Токена вече съществува",
917 "token_contract_address": "Адрес на токен договор",
918 "token_decimal": "Токен десетичен",
919 "token_name": "Име на токена, напр.: Tether",
res/values/strings_cs.arb
+1
@@ -913,6 +913,7 @@
913 "tip": "Spropitné:",
914 "to": "Na",
915 "today": "Dnes",
916 + "token_already_exists": "Token již existuje",
917 "token_contract_address": "Adresa tokenové smlouvy",
918 "token_decimal": "Token v desítkové soustavě",
919 "token_name": "Název tokenu např.: Tether",
res/values/strings_de.arb
+1
@@ -914,6 +914,7 @@
914 "tip": "Hinweis:",
915 "to": "Zu",
916 "today": "Heute",
917 + "token_already_exists": "Token existiert bereits",
918 "token_contract_address": "Token-Contract-Adresse",
919 "token_decimal": "Token-Dezimalzahl",
920 "token_name": "Token-Name, z. B.: Tether",
res/values/strings_en.arb
+1
@@ -914,6 +914,7 @@
914 "tip": "Tip:",
915 "to": "To",
916 "today": "Today",
917 + "token_already_exists": "Token already exists",
918 "token_contract_address": "Token contract address",
919 "token_decimal": "Token decimal",
920 "token_name": "Token name eg: Tether",
res/values/strings_es.arb
+1
@@ -914,6 +914,7 @@
914 "tip": "Consejo:",
915 "to": "A",
916 "today": "Hoy",
917 + "token_already_exists": "Token ya existe",
918 "token_contract_address": "Dirección de contrato de token",
919 "token_decimal": "Token decimal",
920 "token_name": "Nombre del token, por ejemplo: Tether",
res/values/strings_fr.arb
+1
@@ -913,6 +913,7 @@
913 "tip": "Pourboire:",
914 "to": "À",
915 "today": "Aujourd'hui",
916 + "token_already_exists": "Le jeton existe déjà",
917 "token_contract_address": "Adresse du contrat de token",
918 "token_decimal": "Décimales de token",
919 "token_name": "Nom du token, par exemple : Tether",
res/values/strings_ha.arb
+1
@@ -915,6 +915,7 @@
915 "tip": "Tukwici:",
916 "to": "Zuwa",
917 "today": "Yau",
918 + "token_already_exists": "Alama an riga an wanzu",
919 "token_contract_address": "Adireshin kwangilar Token",
920 "token_decimal": "Alamar ƙima",
921 "token_name": "Alamar sunan misali: Tether",
res/values/strings_hi.arb
+1
@@ -915,6 +915,7 @@
915 "tip": "टिप:",
916 "to": "को",
917 "today": "आज",
918 + "token_already_exists": "टोकन पहले से मौजूद है",
919 "token_contract_address": "टोकन अनुबंध पता",
920 "token_decimal": "सांकेतिक दशमलव",
921 "token_name": "टोकन नाम जैसे: टीथर",
res/values/strings_hr.arb
+1
@@ -913,6 +913,7 @@
913 "tip": "Savjet:",
914 "to": "Do",
915 "today": "Danas",
916 + "token_already_exists": "Token već postoji",
917 "token_contract_address": "Adresa ugovora tokena",
918 "token_decimal": "Token decimalni",
919 "token_name": "Naziv tokena npr.: Tether",
res/values/strings_hy.arb
+1
@@ -911,6 +911,7 @@
911 "tip": "Թեյավճար",
912 "to": "Դեպի",
913 "today": "Այսօր",
914 + "token_already_exists": "Նշանն արդեն գոյություն ունի",
915 "token_contract_address": "Token-ի պայմանագրի հասցե",
916 "token_decimal": "Token-ի տասանիշ",
917 "token_name": "Token-ի անուն, օրինակ՝ Tether",
res/values/strings_id.arb
+1
@@ -916,6 +916,7 @@
916 "tip": "Tip:",
917 "to": "Ke",
918 "today": "Hari ini",
919 + "token_already_exists": "Token sudah ada",
920 "token_contract_address": "Alamat kontrak token",
921 "token_decimal": "Desimal token",
922 "token_name": "Nama token misalnya: Tether",
res/values/strings_it.arb
+1
@@ -914,6 +914,7 @@
914 "tip": "Suggerimento:",
915 "to": "A",
916 "today": "Oggi",
917 + "token_already_exists": "Il token esiste già",
918 "token_contract_address": "Indirizzo del contratto token",
919 "token_decimal": "Decimale del token",
920 "token_name": "Nome del token, ad esempio: Tether",
res/values/strings_ja.arb
+1
@@ -914,6 +914,7 @@
914 "tip": "ヒント: ",
915 "to": "に",
916 "today": "今日",
917 + "token_already_exists": "トークンはすでに存在します",
918 "token_contract_address": "トークンコントラクトアドレス",
919 "token_decimal": "トークン10進数",
920 "token_name": "トークン名 例: Tether",
res/values/strings_ko.arb
+1
@@ -914,6 +914,7 @@
914 "tip": "팁:",
915 "to": "받는 통화",
916 "today": "오늘",
917 + "token_already_exists": "토큰이 이미 존재합니다",
918 "token_contract_address": "토큰 계약 주소",
919 "token_decimal": "토큰 소수 자릿수",
920 "token_name": "토큰 이름 (예: Tether)",
res/values/strings_my.arb
+1
@@ -913,6 +913,7 @@
913 "tip": "အကြံပြုချက်-",
914 "to": "သို့",
915 "today": "ဒီနေ့",
916 + "token_already_exists": "တိုကင်ရှိပြီးသား",
917 "token_contract_address": "တိုကင်စာချုပ်လိပ်စာ",
918 "token_decimal": "တိုကင်ဒဿမ",
919 "token_name": "တိုကင်အမည် ဥပမာ- Tether",
res/values/strings_nl.arb
+1
@@ -913,6 +913,7 @@
913 "tip": "Tip:",
914 "to": "Naar",
915 "today": "Vandaag",
916 + "token_already_exists": "Token bestaat al",
917 "token_contract_address": "Token contractadres",
918 "token_decimal": "Token decimaal",
919 "token_name": "Tokennaam bijv.: Tether",
res/values/strings_pl.arb
+1
@@ -913,6 +913,7 @@
913 "tip": "tip:",
914 "to": "Do",
915 "today": "Dzisiaj",
916 + "token_already_exists": "Token już istnieje",
917 "token_contract_address": "Adres kontraktu tokena",
918 "token_decimal": "Token dziesiętny",
919 "token_name": "Nazwa tokena, np.: Tether",
res/values/strings_pt.arb
+1
@@ -915,6 +915,7 @@
915 "tip": "Dica:",
916 "to": "Para",
917 "today": "Hoje",
918 + "token_already_exists": "Token já existe",
919 "token_contract_address": "Endereço do contrato de token",
920 "token_decimal": "Token decimal",
921 "token_name": "Nome do token, por exemplo: Tether",
res/values/strings_ru.arb
+1
@@ -914,6 +914,7 @@
914 "tip": "Совет:",
915 "to": "К",
916 "today": "Сегодня",
917 + "token_already_exists": "Токен уже существует",
918 "token_contract_address": "Адрес контракта токена",
919 "token_decimal": "Десятичный токен",
920 "token_name": "Имя токена, например: Tether",
res/values/strings_th.arb
+1
@@ -913,6 +913,7 @@
913 "tip": "เพิ่มค่าตอบแทน:",
914 "to": "ถึง",
915 "today": "วันนี้",
916 + "token_already_exists": "โทเค็นมีอยู่แล้ว",
917 "token_contract_address": "ที่อยู่สัญญาโทเค็น",
918 "token_decimal": "โทเค็นทศนิยม",
919 "token_name": "ชื่อโทเค็น เช่น Tether",
res/values/strings_tl.arb
+1
@@ -913,6 +913,7 @@
913 "tip": "Tip:",
914 "to": "Sa",
915 "today": "Ngayon",
916 + "token_already_exists": "Mayroon nang token",
917 "token_contract_address": "Address ng token contract",
918 "token_decimal": "Token decimal",
919 "token_name": "Pangalan ng token, halimbawa: Tether",
res/values/strings_tr.arb
+1
@@ -913,6 +913,7 @@
913 "tip": "Bahşiş:",
914 "to": "İle",
915 "today": "Bugün",
916 + "token_already_exists": "Token zaten var",
917 "token_contract_address": "Token sözleşme adresi",
918 "token_decimal": "Belirteç ondalık",
919 "token_name": "Belirteç adı, örneğin: Tether",
res/values/strings_uk.arb
+1
@@ -914,6 +914,7 @@
914 "tip": "Порада:",
915 "to": "До",
916 "today": "Сьогодні",
917 + "token_already_exists": "Маркер вже існує",
918 "token_contract_address": "Адреса договору маркера",
919 "token_decimal": "Токен десятковий",
920 "token_name": "Назва токена, наприклад: Tether",
res/values/strings_ur.arb
+1
@@ -915,6 +915,7 @@
915 "tip": "ٹپ:",
916 "to": "to",
917 "today": "آج",
918 + "token_already_exists": "ٹوکن پہلے ہی موجود ہے",
919 "token_contract_address": "ٹوکن کنٹریکٹ ایڈریس",
920 "token_decimal": "ٹوکن اعشاریہ",
921 "token_name": "ٹوکن کا نام جیسے: Tether",
res/values/strings_vi.arb
+1
@@ -910,6 +910,7 @@
910 "tip": "Mẹo:",
911 "to": "ĐẾN",
912 "today": "Hôm nay",
913 + "token_already_exists": "Mã thông báo đã tồn tại",
914 "token_contract_address": "Địa chỉ hợp đồng token",
915 "token_decimal": "Số thập phân của token",
916 "token_name": "Tên token ví dụ: Tether",
res/values/strings_yo.arb
+1
@@ -914,6 +914,7 @@
914 "tip": "Owó àfikún:",
915 "to": "Si",
916 "today": "Lénìí",
917 + "token_already_exists": "Token tẹlẹ wa",
918 "token_contract_address": "Àmi guide adirẹsi",
919 "token_decimal": "Àmi eleemewa",
920 "token_name": "Orukọ àmi fun apẹẹrẹ: Tether",
res/values/strings_zh.arb
+1
@@ -913,6 +913,7 @@
913 "tip": "提示:",
914 "to": "到",
915 "today": "今天",
916 + "token_already_exists": "令牌已经存在",
917 "token_contract_address": "代币合约地址",
918 "token_decimal": "令牌十进制",
919 "token_name": "代币名称例如:Tether",
tool/configure.dart
+5
@@ -760,6 +760,7 @@ abstract class Ethereum {
760 void setLedgerConnection(WalletBase wallet, ledger.LedgerConnection connection);
761 Future<List<HardwareAccountData>> getHardwareWalletAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5});
762 List<String> getDefaultTokenContractAddresses();
763 + bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress);
764 }
765 """;
766
@@ -870,6 +871,7 @@ abstract class Polygon {
871 void setLedgerConnection(WalletBase wallet, ledger.LedgerConnection connection);
872 Future<List<HardwareAccountData>> getHardwareWalletAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5});
873 List<String> getDefaultTokenContractAddresses();
874 + bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress);
875 }
876 """;
877
@@ -1156,6 +1158,7 @@ abstract class Solana {
1158 List<int>? getValidationLength(CryptoCurrency type);
1159 double? getEstimateFees(WalletBase wallet);
1160 List<String> getDefaultTokenContractAddresses();
1161 + bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress);
1162 }
1163
1164 """;
@@ -1234,6 +1237,7 @@ abstract class Tron {
1237
1238 void updateTronGridUsageState(WalletBase wallet, bool isEnabled);
1239 List<String> getDefaultTokenContractAddresses();
1240 + bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress);
1241 }
1242 """;
1243
@@ -1308,6 +1312,7 @@ abstract class Zano {
1312 String getAddress(WalletBase wallet);
1313 bool validateAddress(String address);
1314 Map<String, List<int>> debugCallLength();
1315 + bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress);
1316 }
1317 """;
1318 const zanoEmptyDefinition = 'Zano? zano;\n';