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';