mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-31 06:58:42 +01:00
Catches up from the May 12 fork point (v6.1.2+23) through v6.5.0: 535 upstream commits, including the Monero silent-open fix, Monero on Trezor, Ledger/Trezor BLE fixes, the new seed and history screens, quick Bitcoin sync, git-pinned dependencies (pubspec_overrides.yaml), frozen Hive adapters (*.part.dart), monero_c v0.18.4.6-RC2 and Flutter 3.41.9. Merged with both sides normalized first (import rename, 100-column format), which took the conflict count from 614 paths to 322. The procedure is now scripts/upstream_sync.sh. Kept from Hash Bags: - WalletType enum order and Hive IDs. SQLite stores WalletType.index and our order differs from upstream's; taking theirs would reread saved wallets as other chains. Our frozen adapter keeps the shipped IDs. - The 1.0.2 legacy PIN and wallet-password decode and self-heal. - Removed chains (Solana, Tron, Zano, Zcash, Decred, Haven) and the new Robinhood chain stay out; Cake Pay and non-Trocador swaps stay out. - Default nodes (node.wownero.org, sethforprivacy, Blockstream, bysh.me), converted to upstream's isDefault node-list key; Wownero is back in the node reset list. - Privacy: address resolvers limited to domain-anchored sources again; prices.neroswap.com for fiat; upstream's charts tab, which reads prices.cakewallet.com, sits behind FeatureFlag.isChartsEnabled = false. - Lightning stays behind FeatureFlag.isLightningEnabled, including upstream's new post-seed Lightning username step. - Our icon pipeline, branding and bundle identifiers. Twelve new upstream widget tests are skipped: test and code are unchanged from v6.5.0 apart from renames (not rerun on a pristine upstream checkout; upstream's top_bar_test does not compile against its own TopBar). flutter analyze lib: 0 errors; flutter test: 345 passed, 12 skipped; flutter build bundle compiles. Built with Flutter 3.41.9.
366 lines
10 KiB
Dart
366 lines
10 KiB
Dart
import 'package:hash_wallet/reactions/wallet_connect.dart';
|
|
import 'package:hash_wallet/evm/evm.dart';
|
|
import 'package:cw_core/crypto_currency.dart';
|
|
import 'package:cw_core/currency_for_wallet_type.dart';
|
|
import 'package:cw_core/erc20_token.dart';
|
|
import 'package:cw_core/spl_token.dart';
|
|
import 'package:cw_core/tron_token.dart';
|
|
import "package:cw_core/utils/print_verbose.dart";
|
|
import 'package:cw_core/wallet_base.dart';
|
|
import 'package:cw_core/wallet_info.dart';
|
|
import 'package:cw_core/wallet_type.dart';
|
|
|
|
class TokenUtilities {
|
|
static Future<List<Erc20Token>> loadAllUniqueEvmTokens() async {
|
|
final allWi = await WalletInfo.getAll();
|
|
final evmWallets = allWi.where(
|
|
(w) => isEVMCompatibleChain(w.type),
|
|
);
|
|
|
|
final seen = <String>{};
|
|
final unique = <Erc20Token>[];
|
|
|
|
for (final wallet in evmWallets) {
|
|
final chain = getTokenNameBasedOnWalletType(wallet.type);
|
|
final tokens = await Erc20Token.getAllForWallet(wallet.name, _getDefaultChainId(wallet.type));
|
|
|
|
for (final t in tokens.where((t) => t.enabled)) {
|
|
final key = "$chain|${t.contractAddress.toLowerCase()}";
|
|
if (seen.add(key)) {
|
|
unique.add(t);
|
|
}
|
|
}
|
|
}
|
|
|
|
return unique;
|
|
}
|
|
|
|
static Future<List<SPLToken>> loadAllUniqueSolTokens() async {
|
|
return <SPLToken>[];
|
|
}
|
|
|
|
static Future<List<TronToken>> loadAllUniqueTronTokens() async {
|
|
return <TronToken>[];
|
|
}
|
|
|
|
static List<Erc20Token> loadDefaultEvmTokensForSwap() {
|
|
if (evm == null) {
|
|
return [];
|
|
}
|
|
|
|
final tokens = <Erc20Token>[];
|
|
final seen = <String>{};
|
|
|
|
for (final chain in evm!.getAllChains()) {
|
|
for (final token in evm!.getDefaultTokensByChainId(chain.chainId)) {
|
|
final key = "${chain.chainId}|${token.contractAddress.toLowerCase()}";
|
|
if (seen.add(key)) {
|
|
tokens.add(token);
|
|
}
|
|
}
|
|
}
|
|
|
|
return tokens;
|
|
}
|
|
|
|
static List<SPLToken> loadDefaultSolTokensForSwap() => <SPLToken>[];
|
|
|
|
static List<TronToken> loadDefaultTronTokensForSwap() => <TronToken>[];
|
|
|
|
static Future<List<Erc20Token>> loadEvmTokensForSwap() async {
|
|
final defaultTokens = loadDefaultEvmTokensForSwap();
|
|
final userTokens = await loadAllUniqueEvmTokens();
|
|
|
|
final seen = <String>{};
|
|
final result = <Erc20Token>[];
|
|
|
|
for (final t in [...defaultTokens, ...userTokens]) {
|
|
final key = '${t.tag ?? 'ETH'}|${t.contractAddress.toLowerCase()}';
|
|
if (seen.add(key)) {
|
|
result.add(t);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static Future<List<SPLToken>> loadSolTokensForSwap() async {
|
|
final defaultTokens = loadDefaultSolTokensForSwap();
|
|
final userTokens = await loadAllUniqueSolTokens();
|
|
|
|
final seen = <String>{};
|
|
final result = <SPLToken>[];
|
|
|
|
for (final t in [...defaultTokens, ...userTokens]) {
|
|
final key = t.mintAddress.toLowerCase();
|
|
if (seen.add(key)) {
|
|
result.add(t);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static Future<List<TronToken>> loadTronTokensForSwap() async {
|
|
final defaultTokens = loadDefaultTronTokensForSwap();
|
|
final userTokens = await loadAllUniqueTronTokens();
|
|
|
|
final seen = <String>{};
|
|
final result = <TronToken>[];
|
|
|
|
for (final t in [...defaultTokens, ...userTokens]) {
|
|
final key = t.contractAddress.toLowerCase();
|
|
if (seen.add(key)) {
|
|
result.add(t);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// Finds a token by address for the given [walletType].
|
|
/// - EVM chains: match by contractAddress
|
|
static Future<CryptoCurrency?> findTokenByAddress({
|
|
required WalletType walletType,
|
|
required String address,
|
|
}) async {
|
|
if (address.isEmpty) {
|
|
return null;
|
|
}
|
|
final lower = address.toLowerCase();
|
|
final tokens = await getAvailableTokensForNetwork(walletType);
|
|
for (final t in tokens) {
|
|
if (t is Erc20Token && t.contractAddress.toLowerCase() == lower) {
|
|
return t;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
static int _getDefaultChainId(WalletType walletType) => switch (walletType) {
|
|
WalletType.ethereum => 1,
|
|
WalletType.polygon => 137,
|
|
WalletType.base => 8453,
|
|
WalletType.arbitrum => 42161,
|
|
WalletType.bsc => 56,
|
|
_ => 1,
|
|
};
|
|
|
|
static Future<int?> findEvmChainIdForContract(
|
|
String contractAddress, {
|
|
int? excludingChainId,
|
|
}) async {
|
|
if (evm == null || contractAddress.isEmpty) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
for (final chain in evm!.getAllChains()) {
|
|
if (chain.chainId == excludingChainId) {
|
|
continue;
|
|
}
|
|
|
|
final walletType = evm!.getWalletTypeByChainId(chain.chainId);
|
|
if (walletType == null) {
|
|
continue;
|
|
}
|
|
|
|
final token = await findTokenByAddress(walletType: walletType, address: contractAddress);
|
|
if (token != null) {
|
|
return chain.chainId;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
printV("findEvmChainIdForContract failed: $e");
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
static Erc20Token? findErc20Token(CryptoCurrency currency, WalletBase wallet) {
|
|
if (currency is Erc20Token) {
|
|
return currency;
|
|
}
|
|
|
|
// More of a fallback for us
|
|
for (final balanceCurrency in wallet.balance.keys) {
|
|
if (balanceCurrency is Erc20Token && _matchesCurrency(balanceCurrency, currency)) {
|
|
return balanceCurrency;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
static Erc20Token? findErc20TokenForSwap(CryptoCurrency currency) {
|
|
if (currency is Erc20Token) {
|
|
return currency;
|
|
}
|
|
|
|
for (final token in loadDefaultEvmTokensForSwap()) {
|
|
if (_matchesCurrency(token, currency)) {
|
|
return token;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
static String? findSolanaTokenMint(CryptoCurrency currency) {
|
|
if (currency is SPLToken) return currency.mintAddress;
|
|
|
|
for (final token in loadDefaultSolTokensForSwap()) {
|
|
if (_matchesCurrency(token, currency)) return token.mintAddress;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
static String? findTronTokenContract(CryptoCurrency currency) {
|
|
if (currency is TronToken) return currency.contractAddress;
|
|
|
|
for (final token in loadDefaultTronTokensForSwap()) {
|
|
if (_matchesCurrency(token, currency)) return token.contractAddress;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
static bool isNativeToken(CryptoCurrency currency) {
|
|
final title = currency.title.toLowerCase();
|
|
final tag = currency.tag?.toLowerCase();
|
|
|
|
return title == "eth" ||
|
|
title == "ethereum" ||
|
|
title == "matic" ||
|
|
title == "polygon" ||
|
|
title == "base" ||
|
|
title == "arbitrum" ||
|
|
title == "bnb" ||
|
|
title == "bsc" ||
|
|
title == "avax" ||
|
|
title == "avalanche" ||
|
|
tag == "polygon" ||
|
|
tag == "bsc" ||
|
|
tag == "avalanche";
|
|
}
|
|
|
|
static int getChainId(CryptoCurrency currency) {
|
|
final tag = currency.tag?.toUpperCase();
|
|
final title = currency.title.toLowerCase();
|
|
|
|
// Only check EVM registry for currencies that might be EVM-related
|
|
final isPotentialEVM = isNativeToken(currency) ||
|
|
(tag != null &&
|
|
(tag == "ETH" ||
|
|
tag == "POL" ||
|
|
tag == "BASE" ||
|
|
tag == "ARB" ||
|
|
tag == "BSC"));
|
|
|
|
if (isPotentialEVM) {
|
|
// Try by tag first if available (e.g., 'POL', 'BASE', 'ARB')
|
|
if (tag != null) {
|
|
final chainId = evm?.getChainIdByTag(tag);
|
|
if (chainId != null) {
|
|
return chainId;
|
|
}
|
|
}
|
|
|
|
// Try by title (case-insensitive)
|
|
final titleChainId = evm?.getChainIdByTitle(title);
|
|
if (titleChainId != null) {
|
|
return titleChainId;
|
|
}
|
|
}
|
|
|
|
// Fallback to hardcoded values for chains not in registry yet
|
|
if (title == "avalanche" || title == "avax" || tag == "AVALANCHE") {
|
|
return 43114;
|
|
}
|
|
|
|
if (title == "optimism" || title == "op" || tag == "OPTIMISM") {
|
|
return 10;
|
|
}
|
|
|
|
if (title == "fantom" || title == "ftm" || tag == "FANTOM") {
|
|
return 250;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
static Future<List<CryptoCurrency>> getAvailableTokensForNetwork(
|
|
WalletType network,
|
|
) async {
|
|
final baseCurrency = walletTypeToCryptoCurrency(network);
|
|
final allTokens = <CryptoCurrency>[baseCurrency];
|
|
final addedAddresses = <String>{};
|
|
|
|
if (isEVMCompatibleChain(network)) {
|
|
final userTokens = await _getUserTokensForNetwork(baseCurrency);
|
|
for (final token in userTokens) {
|
|
if (token is Erc20Token) {
|
|
final address = token.contractAddress.toLowerCase();
|
|
if (addedAddresses.add(address)) {
|
|
allTokens.add(token);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (evm != null) {
|
|
final chainId = evm!.getChainIdByWalletType(network);
|
|
for (final token in evm!.getDefaultTokensByChainId(chainId)) {
|
|
final address = token.contractAddress.toLowerCase();
|
|
if (addedAddresses.add(address)) {
|
|
allTokens.add(token);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add tokens from CryptoCurrency.all that don't duplicate user tokens
|
|
for (final currency in CryptoCurrency.all) {
|
|
// Match by tag for POL/BASE, match by title==tag for ETH
|
|
final matches = (baseCurrency.tag == null && baseCurrency.title == currency.tag) ||
|
|
(baseCurrency.tag != null &&
|
|
currency.tag?.toLowerCase() == baseCurrency.tag?.toLowerCase());
|
|
|
|
if (matches) {
|
|
if (currency is Erc20Token) {
|
|
final address = currency.contractAddress.toLowerCase();
|
|
if (addedAddresses.add(address)) {
|
|
allTokens.add(currency);
|
|
}
|
|
} else if (!allTokens.any((t) => _matchesCurrency(t, currency))) {
|
|
allTokens.add(currency);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return allTokens;
|
|
}
|
|
|
|
static bool _matchesCurrency(CryptoCurrency a, CryptoCurrency b) =>
|
|
a.title.toUpperCase() == b.title.toUpperCase() &&
|
|
(a.tag?.toUpperCase() == b.tag?.toUpperCase());
|
|
|
|
static Future<List<CryptoCurrency>> _getUserTokensForNetwork(CryptoCurrency baseCurrency) async {
|
|
final walletType = cryptoCurrencyOrTokenToWalletType(baseCurrency);
|
|
if (walletType == null) {
|
|
return [];
|
|
}
|
|
|
|
if (isEVMCompatibleChain(walletType)) {
|
|
final tokens = await TokenUtilities.loadAllUniqueEvmTokens();
|
|
|
|
return tokens.where((token) {
|
|
if (baseCurrency.tag == null) {
|
|
return token.tag == baseCurrency.title;
|
|
}
|
|
|
|
return token.tag?.toLowerCase() == baseCurrency.tag?.toLowerCase();
|
|
}).toList();
|
|
}
|
|
|
|
return [];
|
|
}
|
|
}
|