Files
hash-wallet/lib/view_model/dashboard/home_settings_view_model.dart
jwinterm b57ab7cc06 Nuke Solana/Tron/Zano/Decred/Zcash/Haven from the codebase entirely
94 files changed, -5926 net lines. No stubs, no generators, no dead enum
values, no orphan switch branches. Removed for principled reasons (we
do not ship these chains) and to make the codebase smaller and friendlier
to contribute to.

What's gone:
- lib/{solana,tron,zano,decred,zcash,haven}/ directories
- lib/exchange/provider/jupiter_exchange_provider.dart (Solana-only)
- lib/entities/haven_seed_store.dart
- WalletType.{solana,tron,zano,decred,zcash,haven} enum values
  (HiveField IDs are pinned: Wownero stays at 12, all surviving types
  keep their IDs, so existing user-saved wallets deserialize unchanged)
- All ~70 lib/ files that imported the removed chain stubs or branched
  on the removed WalletType values
- backupSeeds(Box<HavenSeedStore>) abstract method + Wownero implementation
- Jupiter exchange branches in send/exchange view models
- TronGrid usage settings, Zano alias lookups, Zcash transparent address
  handling, Decred default-spv-nodes branches, Haven seed migration

What stays (intentional):
- CryptoCurrency.{xhv,sol,trx,dcr,zano,zec} enum entries in cw_core —
  they're still used as exchange/swap currency identifiers in
  address_validator.dart and elsewhere. The wallet types are gone but
  the *currencies* are still tradeable through Trocador.
- cw_core/spl_token.dart and cw_core/tron_token.dart Hive adapters in
  main.dart — the tokens still register as Hive types for backwards-
  compat with any old box data.
- lib/buy/{moonpay,robinhood,onramper,dfx,kryptonim,meld}/ provider
  source files (just stripped of solana/tron/etc references; providers
  themselves remain dormant per provider_types.dart).

Pinned-ID note: WalletType enum now has gaps in @HiveField IDs (4, 10,
11, 13, 14, 18 removed). This is deliberate — re-using a freed ID later
would mis-deserialize old hive boxes. Don't renumber.

Bundled config / CI hasn't changed; configure.dart was already free of
these chains in commit 4eb7db0b7.
2026-05-17 12:30:25 -04:00

461 lines
14 KiB
Dart

import 'dart:convert';
import 'package:hash_wallet/core/fiat_conversion_service.dart';
import 'package:hash_wallet/entities/fiat_api_mode.dart';
import 'package:hash_wallet/entities/erc20_token_info_moralis.dart';
import 'package:hash_wallet/entities/sort_balance_types.dart';
import 'package:hash_wallet/evm/evm.dart';
import 'package:hash_wallet/reactions/wallet_connect.dart';
import 'package:hash_wallet/store/settings_store.dart';
import 'package:hash_wallet/utils/token_utilities.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:hash_wallet/view_model/dashboard/balance_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/erc20_token.dart';
import 'package:cw_core/utils/homoglyph_normalizer.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:mobx/mobx.dart';
import 'package:hash_wallet/.secrets.g.dart' as secrets;
part 'home_settings_view_model.g.dart';
class HomeSettingsViewModel = HomeSettingsViewModelBase with _$HomeSettingsViewModel;
abstract class HomeSettingsViewModelBase with Store {
HomeSettingsViewModelBase(this._settingsStore, this._balanceViewModel)
: tokens = ObservableSet<CryptoCurrency>(),
isAddingToken = false,
isDeletingToken = false,
isValidatingContractAddress = false,
showCombinedBalance = _balanceViewModel.wallet.walletInfo.showCombinedBalance,
favoriteToken = _balanceViewModel.wallet.currency {
_updateTokensList();
_updateLocalFavoriteToken();
// React to wallet changes
reaction((_) => _balanceViewModel.wallet, (_) {
_updateTokensList();
});
reaction((_) {
final wallet = _balanceViewModel.wallet;
if (isEVMCompatibleChain(wallet.type)) {
final selectedChainId = evm!.getSelectedChainId(wallet);
final erc20Currencies = evm!.getERC20Currencies(wallet);
return '${wallet.currency.title}_${selectedChainId}_${erc20Currencies.length}';
}
return null;
}, (_) async {
await Future.delayed(const Duration(milliseconds: 200));
_updateTokensList();
});
}
final SettingsStore _settingsStore;
final BalanceViewModel _balanceViewModel;
final ObservableSet<CryptoCurrency> tokens;
@computed
List<CryptoCurrency> get enabledTokens => [_balanceViewModel.wallet.currency, ...tokens.where((item)=>item.enabled).toList()];
WalletType get walletType => _balanceViewModel.wallet.type;
@observable
bool isAddingToken;
@observable
bool isDeletingToken;
@observable
bool isValidatingContractAddress;
@observable
String searchText = '';
@computed
SortBalanceBy get sortBalanceBy => _settingsStore.sortBalanceBy;
@action
void setSortBalanceBy(SortBalanceBy value) {
_settingsStore.sortBalanceBy = value;
_updateTokensList();
}
@computed
bool get pinNativeToken => _settingsStore.pinNativeTokenAtTop;
@action
void setPinNativeToken(bool value) => _settingsStore.pinNativeTokenAtTop = value;
@action
Future<void> addToken({
required String contractAddress,
required CryptoCurrency token,
}) async {
try {
isAddingToken = true;
if (isEVMCompatibleChain(_balanceViewModel.wallet.type)) {
final evmToken = Erc20Token(
name: token.name,
symbol: token.title,
decimal: token.decimals,
contractAddress: contractAddress.toLowerCase(),
iconPath: token.iconPath,
isPotentialScam: token.isPotentialScam,
);
await evm!.addErc20Token(_balanceViewModel.wallet, evmToken);
}
_updateTokensList();
_updateFiatPrices(token);
} catch (e) {
throw e;
} finally {
isAddingToken = false;
}
}
@action
bool checkIfTokenIsAlreadyAdded(String contractAddress) {
if (isEVMCompatibleChain(_balanceViewModel.wallet.type)) {
return evm!.isTokenAlreadyAdded(_balanceViewModel.wallet, contractAddress);
}
return false;
}
@action
Future<void> deleteToken(CryptoCurrency token) async {
try {
isDeletingToken = true;
if (isEVMCompatibleChain(_balanceViewModel.wallet.type)) {
await evm!.deleteErc20Token(_balanceViewModel.wallet, token as Erc20Token);
}
_updateTokensList();
} finally {
isDeletingToken = false;
}
}
Future<bool> checkIfERC20TokenContractAddressIsAPotentialScamAddress(
String contractAddress,
) async {
try {
isValidatingContractAddress = true;
if (!isEVMCompatibleChain(_balanceViewModel.wallet.type)) {
return false;
}
bool isPotentialScamViaMoralis = await _isPotentialScamTokenViaMoralis(
contractAddress,
getChainNameBasedOnWalletType(_balanceViewModel.wallet.type),
);
bool isUnverifiedContract = await _isContractUnverified(
contractAddress,
chainId: evm!.getSelectedChainId(_balanceViewModel.wallet).toString(),
);
final showWarningForContractAddress = isPotentialScamViaMoralis || isUnverifiedContract;
return showWarningForContractAddress;
} finally {
isValidatingContractAddress = false;
}
}
bool checkIfTokenIsWhitelisted(String contractAddress) {
// get the default tokens for each currency type:
List<String> defaultTokenAddresses = [];
switch (_balanceViewModel.wallet.type) {
case WalletType.ethereum:
case WalletType.polygon:
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
defaultTokenAddresses = evm!.getDefaultTokenContractAddresses(_balanceViewModel.wallet);
break;
case WalletType.banano:
case WalletType.monero:
case WalletType.none:
case WalletType.bitcoin:
case WalletType.litecoin:
case WalletType.nano:
case WalletType.wownero:
case WalletType.bitcoinCash:
case WalletType.dogecoin:
return false;
}
// check if the contractAddress is in the defaultTokenAddresses
bool isInWhitelist = defaultTokenAddresses
.any((element) => element.toLowerCase() == contractAddress.toLowerCase());
return isInWhitelist;
}
bool checkIfTokenSymbolMatchesDefaultToken(String symbol) {
final normalizedSymbol = normalizeHomoglyphs(symbol.trim().toUpperCase());
if (normalizedSymbol.isEmpty) return false;
List<String> defaultTokenSymbols = [];
switch (_balanceViewModel.wallet.type) {
case WalletType.ethereum:
case WalletType.polygon:
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
defaultTokenSymbols = evm!.getDefaultTokenSymbols(_balanceViewModel.wallet);
break;
case WalletType.banano:
case WalletType.monero:
case WalletType.none:
case WalletType.bitcoin:
case WalletType.litecoin:
case WalletType.nano:
case WalletType.wownero:
case WalletType.bitcoinCash:
case WalletType.dogecoin:
return false;
}
return defaultTokenSymbols.any((s) => s.toUpperCase() == normalizedSymbol);
}
Future<bool> _isPotentialScamTokenViaMoralis(
String contractAddress,
String chainName,
) async {
final uri = Uri.https(
'deep-index.moralis.io',
'/api/v2.2/erc20/metadata',
{
"chain": chainName,
"addresses": contractAddress,
},
);
try {
final response = await ProxyWrapper().get(
clearnetUri: uri,
headers: {
"Accept": "application/json",
"X-API-Key": secrets.moralisApiKey,
},
);
final decodedResponse = jsonDecode(response.body);
final tokenInfo = Erc20TokenInfoMoralis.fromJson(decodedResponse[0] as Map<String, dynamic>);
// Based on analysis using Moralis internal metrics
if (tokenInfo.possibleSpam == true) {
return true;
}
// Tokens whose contract have not been verified are potentially risky tokens.
// if (tokenInfo.verifiedContract == false) {
// return true;
// }
// Tokens with a security score less than 40 are potentially risky, requiring caution when dealing with them.
if (tokenInfo.securityScore != null && tokenInfo.securityScore! < 40) {
return true;
}
// Having a Fully Diluted Valiuation of 0 is a significant red flag that could signify:
// - An abandoned/unlaunched project
// - Incorrect/missing token data
// - Suspicious manipulation of token data
/// commented out as it's failing a lot of legit tokens
// if (tokenInfo.fullyDilutedValuation == '0') {
// return true;
// }
return false;
} catch (e) {
printV('Error while checking scam via moralis: ${e.toString()}');
return true;
}
}
Future<bool> _isContractUnverified(
String contractAddress, {
required String chainId,
}) async {
final uri = Uri.https(
"api.etherscan.io",
"/v2/api",
{
"chainid": chainId,
"module": "contract",
"action": "getsourcecode",
"address": contractAddress,
"apikey": secrets.etherScanApiKey,
},
);
try {
final response = await ProxyWrapper().get(clearnetUri: uri);
final decodedResponse = jsonDecode(response.body) as Map<String, dynamic>;
if (decodedResponse['status'] == '0') {
printV('${response.body}\n');
printV('${decodedResponse['result']}\n');
return true;
}
if (decodedResponse['status'] == '1' &&
decodedResponse['result'][0]['ABI'] == 'Contract source code not verified') {
printV('Call is valid but contract is not verified');
return true; // Contract is not verified
} else {
printV('Call is valid and contract is verified');
return false; // Contract is verified
}
} catch (e) {
printV('Error while checking contract verification: ${e.toString()}');
return true;
}
}
Future<CryptoCurrency?> getToken(String contractAddress) async {
if (isEVMCompatibleChain(_balanceViewModel.wallet.type)) {
return await evm!.getErc20Token(_balanceViewModel.wallet, contractAddress);
}
return null;
}
CryptoCurrency get nativeToken => _balanceViewModel.wallet.currency;
void _updateFiatPrices(CryptoCurrency token) async {
if (token.isPotentialScam) return; // don't fetch price data for potential scam tokens
try {
_balanceViewModel.fiatConversionStore.prices[token] =
await FiatConversionService.fetchPrice(
crypto: token,
fiat: _settingsStore.fiatCurrency,
torOnly: _settingsStore.fiatApiMode == FiatApiMode.torOnly);
} catch (_) {}
}
void changeTokenAvailability(CryptoCurrency token, bool value) async {
token.enabled = value;
if (isEVMCompatibleChain(_balanceViewModel.wallet.type)) {
evm!.addErc20Token(_balanceViewModel.wallet, token as Erc20Token);
if (!value) evm!.removeTokenTransactionsInHistory(_balanceViewModel.wallet, token);
}
_refreshTokensList();
}
@action
void _updateTokensList() {
int _sortFunc(CryptoCurrency e1, CryptoCurrency e2) {
int index1 = _balanceViewModel.formattedBalances.indexWhere((element) => element.asset == e1);
int index2 = _balanceViewModel.formattedBalances.indexWhere((element) => element.asset == e2);
if (e1.enabled && !e2.enabled) {
return -1;
} else if (e2.enabled && !e1.enabled) {
return 1;
} else if (!e1.enabled && !e2.enabled) {
// if both are disabled then sort alphabetically
return e1.name.compareTo(e2.name);
}
return index1.compareTo(index2);
}
tokens.clear();
if (isEVMCompatibleChain(_balanceViewModel.wallet.type)) {
tokens.addAll(evm!
.getERC20Currencies(_balanceViewModel.wallet)
.where((element) => _matchesSearchText(element))
.toList()
..sort(_sortFunc));
}
}
// FIXME these two observables cause duplicated state and are needed because mobx. remove them as part of the refactor and replace with proper getters
@observable
bool showCombinedBalance;
@observable
CryptoCurrency favoriteToken;
@action
void setShowCombinedBalance(bool value) {
_balanceViewModel.wallet.walletInfo.showCombinedBalance = value;
showCombinedBalance = value;
_balanceViewModel.wallet.updateBalance();
_balanceViewModel.wallet.walletInfo.save();
}
@action
Future<void> setFavoriteToken(CryptoCurrency token) async {
final String? address;
if(token == _balanceViewModel.wallet.currency) {
address = null;
} else {
address = getTokenAddressBasedOnWallet(token);
}
_balanceViewModel.wallet.walletInfo.favoriteTokenAddress = address;
_balanceViewModel.wallet.walletInfo.save();
_updateLocalFavoriteToken();
}
@action
Future<void> _updateLocalFavoriteToken() async {
favoriteToken = await TokenUtilities.findTokenByAddress(
walletType: _balanceViewModel.wallet.type, address: _balanceViewModel.wallet.walletInfo.favoriteTokenAddress ?? "") ??
_balanceViewModel.wallet.currency;
}
@action
void _refreshTokensList() {
final _tokens = Set.of(tokens);
tokens.clear();
tokens.addAll(_tokens);
}
@action
void changeSearchText(String text) {
searchText = text;
_updateTokensList();
}
bool _matchesSearchText(CryptoCurrency asset) {
final address = getTokenAddressBasedOnWallet(asset);
// The homes settings would only be displayed for either of Tron, EVM or Solana Wallets.
if (address == null) return false;
return searchText.isEmpty ||
asset.fullName!.toLowerCase().contains(searchText.toLowerCase()) ||
asset.title.toLowerCase().contains(searchText.toLowerCase()) ||
address == searchText;
}
String? getTokenAddressBasedOnWallet(CryptoCurrency asset) {
if (isEVMCompatibleChain(_balanceViewModel.wallet.type)) {
return evm!.getTokenAddress(asset);
}
return null;
}
}