Files
hash-wallet/lib/utils/token_utilities.dart
T
jwinterm dd449f2594 Merge upstream Cake Wallet v6.5.0
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.
2026-10-24 23:49:29 -04:00

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 [];
}
}