mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-17 05:41:29 +02:00
* feat(zcash): initial commit - initial work on cw_zcash - use single .db for everything zcash to speed up sync and scanning - implement silent autoshielding of non-orchard funds - use FFI for everything to fetch the data - add support for passphrase by adding extra word to seed phrase - enforce lints on CI for cw_zcash directory - use correct zcash name, and call transparent zcash tZEC - minor cleanups outsite of cw_zcash - gitmodules, gitignore - db debug - fix broken addressPageType in WalletInfo - enable debug options in kDebugMode and kProfileMode * feat: T address rotation (and cache) wip: minor fixes * wip: shield tx list * fix fiat amounts fix derrivation of rotation account addresses screen for zcash block height fix T address shields improvements Amount in disposable T shields UI improvements show pending outgoing tx in tx history * fix null check on passphrase page fix tx showing as pending rename auto shield txs fixed send all to include fee use rotating address in addressForExchange wrap all calls of WarpApi.getLatestHeight in try catch to prevent leaking errors other minor fixes from notion * wallet group debug cache sent tx address wallet T address rotation fixes automatically pick the fee for auto-shield * fix T address cache deserialization fix autoshield icon fix autoshield text getCachedDestinationAddress fix send address cache * address comments from review * wip: iOS fix * fix swap * Allow cakepay payment from zcash wallet * fix: ios (I'm stupid) fix: reorder addresses to make orchard default fix: send all balance * 5.6.6 bump fix: ios apple review add transport error to ignored exceptions * fix: leaving dust behind when sweeping all * Change wallets order * fix: reflect balance of pending out txs fix: restore height support * linux support (disabled) * fix: date picker for zcash on restore screen fix: build script for linux match WarpApi.warpSync args with upstream * fix: linter script * ensure all exchange providers support ZEC (#2802) * ensure all exchange providers support ZEC * remove zaddr and tzec currencies and refactor providers * Update lib/exchange/provider/near_Intents_exchange_provider.dart * add back old zcash currencies for backward compatibility, thanks Czarek * minor fix [skip ci] --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> * Update android/app/src/main/AndroidManifestBase.xml [skip ci] * Update ios/Runner/InfoBase.plist [skip ci] * fix: remove unused code fix: macos fix: disable on linux and macos * update build numbers [skip ci] * Add zcash to birdpay --------- Co-authored-by: Czarek Nakamoto <cyjan@mrcyjanek.net> Co-authored-by: Serhii <borodenko.sv@gmail.com>
500 lines
16 KiB
Dart
500 lines
16 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:cake_wallet/core/fiat_conversion_service.dart';
|
|
import 'package:cake_wallet/entities/fiat_api_mode.dart';
|
|
import 'package:cake_wallet/entities/erc20_token_info_moralis.dart';
|
|
import 'package:cake_wallet/entities/sort_balance_types.dart';
|
|
import 'package:cake_wallet/evm/evm.dart';
|
|
import 'package:cake_wallet/reactions/wallet_connect.dart';
|
|
import 'package:cake_wallet/solana/solana.dart';
|
|
import 'package:cake_wallet/store/settings_store.dart';
|
|
import 'package:cake_wallet/tron/tron.dart';
|
|
import 'package:cw_core/utils/proxy_wrapper.dart';
|
|
import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
|
|
import 'package:cake_wallet/zano/zano.dart';
|
|
import 'package:cw_core/crypto_currency.dart';
|
|
import 'package:cw_core/erc20_token.dart';
|
|
import 'package:cw_core/utils/print_verbose.dart';
|
|
import 'package:cw_core/wallet_type.dart';
|
|
import 'package:mobx/mobx.dart';
|
|
import 'package:cake_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 {
|
|
_updateTokensList();
|
|
|
|
// 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;
|
|
|
|
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);
|
|
}
|
|
|
|
if (_balanceViewModel.wallet.type == WalletType.solana) {
|
|
final splToken = token.copyWith(enabled: true);
|
|
await solana!.addSPLToken(
|
|
_balanceViewModel.wallet,
|
|
splToken,
|
|
contractAddress,
|
|
);
|
|
}
|
|
|
|
if (_balanceViewModel.wallet.type == WalletType.tron) {
|
|
final tronToken = token.copyWith(enabled: true);
|
|
await tron!.addTronToken(_balanceViewModel.wallet, tronToken, contractAddress);
|
|
}
|
|
|
|
if (_balanceViewModel.wallet.type == WalletType.zano) {
|
|
await zano!.addZanoAssetById(_balanceViewModel.wallet, contractAddress);
|
|
}
|
|
|
|
_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);
|
|
}
|
|
|
|
if (_balanceViewModel.wallet.type == WalletType.solana) {
|
|
return solana!.isTokenAlreadyAdded(_balanceViewModel.wallet, contractAddress);
|
|
}
|
|
|
|
if (_balanceViewModel.wallet.type == WalletType.tron) {
|
|
return tron!.isTokenAlreadyAdded(_balanceViewModel.wallet, contractAddress);
|
|
}
|
|
|
|
if (_balanceViewModel.wallet.type == WalletType.zano) {
|
|
return zano!.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);
|
|
}
|
|
|
|
if (_balanceViewModel.wallet.type == WalletType.solana) {
|
|
await solana!.deleteSPLToken(_balanceViewModel.wallet, token);
|
|
}
|
|
|
|
if (_balanceViewModel.wallet.type == WalletType.tron) {
|
|
await tron!.deleteTronToken(_balanceViewModel.wallet, token);
|
|
}
|
|
if (_balanceViewModel.wallet.type == WalletType.zano) {
|
|
await zano!.deleteZanoAsset(_balanceViewModel.wallet, token);
|
|
}
|
|
_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:
|
|
defaultTokenAddresses = evm!.getDefaultTokenContractAddresses(_balanceViewModel.wallet);
|
|
break;
|
|
case WalletType.solana:
|
|
defaultTokenAddresses = solana!.getDefaultTokenContractAddresses();
|
|
break;
|
|
case WalletType.tron:
|
|
defaultTokenAddresses = tron!.getDefaultTokenContractAddresses();
|
|
break;
|
|
case WalletType.zano:
|
|
case WalletType.banano:
|
|
case WalletType.monero:
|
|
case WalletType.none:
|
|
case WalletType.bitcoin:
|
|
case WalletType.litecoin:
|
|
case WalletType.haven:
|
|
case WalletType.nano:
|
|
case WalletType.wownero:
|
|
case WalletType.bitcoinCash:
|
|
case WalletType.decred:
|
|
case WalletType.dogecoin:
|
|
case WalletType.zcash:
|
|
return false;
|
|
}
|
|
|
|
// check if the contractAddress is in the defaultTokenAddresses
|
|
bool isInWhitelist = defaultTokenAddresses
|
|
.any((element) => element.toLowerCase() == contractAddress.toLowerCase());
|
|
return isInWhitelist;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
if (_balanceViewModel.wallet.type == WalletType.solana) {
|
|
return await solana!.getSPLToken(_balanceViewModel.wallet, contractAddress);
|
|
}
|
|
|
|
if (_balanceViewModel.wallet.type == WalletType.tron) {
|
|
return await tron!.getTronToken(_balanceViewModel.wallet, contractAddress);
|
|
}
|
|
|
|
if (_balanceViewModel.wallet.type == WalletType.zano) {
|
|
return await zano!.getZanoAsset(_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.fiatConvertationStore.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);
|
|
}
|
|
|
|
if (_balanceViewModel.wallet.type == WalletType.solana) {
|
|
final address = solana!.getTokenAddress(token);
|
|
solana!.addSPLToken(_balanceViewModel.wallet, token, address);
|
|
}
|
|
|
|
if (_balanceViewModel.wallet.type == WalletType.tron) {
|
|
final address = tron!.getTokenAddress(token);
|
|
tron!.addTronToken(_balanceViewModel.wallet, token, address);
|
|
}
|
|
|
|
if (_balanceViewModel.wallet.type == WalletType.zano) {
|
|
await zano!.changeZanoAssetAvailability(_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));
|
|
}
|
|
|
|
if (_balanceViewModel.wallet.type == WalletType.solana) {
|
|
tokens.addAll(solana!
|
|
.getSPLTokenCurrencies(_balanceViewModel.wallet)
|
|
.where((element) => _matchesSearchText(element))
|
|
.toList()
|
|
..sort(_sortFunc));
|
|
}
|
|
|
|
if (_balanceViewModel.wallet.type == WalletType.tron) {
|
|
tokens.addAll(tron!
|
|
.getTronTokenCurrencies(_balanceViewModel.wallet)
|
|
.where((element) => _matchesSearchText(element))
|
|
.toList()
|
|
..sort(_sortFunc));
|
|
}
|
|
|
|
if (_balanceViewModel.wallet.type == WalletType.zano) {
|
|
tokens.addAll(zano!
|
|
.getZanoAssets(_balanceViewModel.wallet)
|
|
.where((element) => _matchesSearchText(element))
|
|
.toList()
|
|
..sort(_sortFunc));
|
|
}
|
|
}
|
|
|
|
@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 (_balanceViewModel.wallet.type == WalletType.tron) {
|
|
return tron!.getTokenAddress(asset);
|
|
}
|
|
|
|
if (_balanceViewModel.wallet.type == WalletType.solana) {
|
|
return solana!.getTokenAddress(asset);
|
|
}
|
|
|
|
if (isEVMCompatibleChain(_balanceViewModel.wallet.type)) {
|
|
return evm!.getTokenAddress(asset);
|
|
}
|
|
|
|
if (_balanceViewModel.wallet.type == WalletType.zano) {
|
|
return zano!.getZanoAssetAddress(asset);
|
|
}
|
|
|
|
// We return null if it's neither Tron, EVM or Solana wallet (which is actually impossible because we only display home settings for either of these four wallets).
|
|
return null;
|
|
}
|
|
}
|