Merge branch 'cake-tech:dev' into dev

This commit is contained in:
BSN ∞/21M
2026-05-05 12:52:49 +02:00
committed by GitHub
7 changed files with 236 additions and 4 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin {
@override
@HiveField(8, defaultValue: false)
final bool isPotentialScam;
bool isPotentialScam;
SPLToken({
required this.name,
+85
View File
@@ -1858,4 +1858,89 @@ class SolanaWalletClient {
return null;
}
}
Future<List<MoralisSolanaTokenBalance>> fetchWalletTokensFromMoralis(
String address,
) async {
try {
if (secrets.moralisApiKey.isEmpty) {
printV('Moralis API key is empty, cannot fetch wallet tokens');
return [];
}
final uri = Uri.https(
'solana-gateway.moralis.io',
'/account/mainnet/$address/tokens',
);
final response = await client.get(
uri,
headers: {
"Accept": "application/json",
"X-API-Key": secrets.moralisApiKey,
},
);
if (response.statusCode < 200 || response.statusCode >= 300) {
printV(
'Moralis Solana API returned status: '
'${response.statusCode}',
);
return [];
}
final decodedResponse = jsonDecode(response.body) as List;
final List<MoralisSolanaTokenBalance> tokens = [];
for (final item in decodedResponse) {
final tokenData = item as Map<String, dynamic>;
final amountStr = tokenData['amount'] as String? ?? '0';
final amount = double.tryParse(amountStr) ?? 0.0;
if (amount <= 0) continue;
final mint = tokenData['mint'] as String? ?? '';
if (mint.isEmpty) continue;
final amountRaw = tokenData['amountRaw'] as String? ?? '0';
final decimals = tokenData['decimals'] as int? ?? 0;
final associatedTokenAddress = tokenData['associatedTokenAddress'] as String? ?? '';
tokens.add(
MoralisSolanaTokenBalance(
mint: mint,
amount: amount,
amountRaw: amountRaw,
decimals: decimals,
associatedTokenAddress: associatedTokenAddress,
),
);
}
return tokens;
} catch (e) {
printV('Error fetching wallet tokens from Moralis: ${e.toString()}');
return [];
}
}
}
class MoralisSolanaTokenBalance {
final String mint;
final double amount;
final String amountRaw;
final int decimals;
final String associatedTokenAddress;
const MoralisSolanaTokenBalance({
required this.mint,
required this.amount,
required this.amountRaw,
required this.decimals,
required this.associatedTokenAddress,
});
}
+77
View File
@@ -631,6 +631,65 @@ abstract class SolanaWalletBase
}
}
Future<SolanaMoralisDiscoveryResult> discoverTokensFromMoralis() async {
try {
if (!splTokensBox.isOpen) return SolanaMoralisDiscoveryResult.empty;
final address = walletAddresses.address;
if (address.isEmpty) return SolanaMoralisDiscoveryResult.empty;
final walletTokens = await _client.fetchWalletTokensFromMoralis(address);
if (walletTokens.isEmpty) return SolanaMoralisDiscoveryResult.empty;
final existingMints = {
for (final token in splTokensBox.values) token.mintAddress: token,
};
final defaultMints = DefaultSPLTokens().initialSPLTokens.map((t) => t.mintAddress).toSet();
final newTokens = <DiscoveredSPLToken>[];
for (final moralisToken in walletTokens) {
final mint = moralisToken.mint;
final existingToken = existingMints[mint];
if (existingToken != null) {
if (defaultMints.contains(mint) && !existingToken.enabled) {
existingToken.enabled = true;
await existingToken.save();
await addSPLToken(existingToken);
}
continue;
}
final tokenInfo = await _client.fetchSPLTokenInfo(mint);
if (tokenInfo == null) continue;
final discoveredToken = SPLToken(
name: tokenInfo.name,
symbol: tokenInfo.symbol,
mintAddress: mint,
decimal: moralisToken.decimals,
mint: tokenInfo.mint,
iconPath: tokenInfo.iconPath,
tag: 'SOL',
);
newTokens.add(
DiscoveredSPLToken(
token: discoveredToken,
balance: moralisToken.amount,
),
);
}
return SolanaMoralisDiscoveryResult(newTokens: newTokens);
} catch (e) {
printV('Error discovering SPL tokens from Moralis: ${e.toString()}');
return SolanaMoralisDiscoveryResult.empty;
}
}
Future<void> addSPLToken(SPLToken token) async {
await splTokensBox.put(token.mintAddress, token);
@@ -766,3 +825,21 @@ abstract class SolanaWalletBase
@override
final String? passphrase;
}
class DiscoveredSPLToken {
final SPLToken token;
final double balance;
const DiscoveredSPLToken({
required this.token,
required this.balance,
});
}
class SolanaMoralisDiscoveryResult {
final List<DiscoveredSPLToken> newTokens;
const SolanaMoralisDiscoveryResult({required this.newTokens});
static const SolanaMoralisDiscoveryResult empty = SolanaMoralisDiscoveryResult(newTokens: []);
}
@@ -111,6 +111,11 @@ void startCurrentWalletChangeReaction(
if (isEVMCompatibleChain(wallet.type)) {
await evm!.discoverAndAddWalletTokens(wallet);
}
if (wallet.type == WalletType.solana) {
await solana!
.discoverAndAddWalletTokens(wallet);
}
} catch (e) {
printV(e.toString());
}
+55
View File
@@ -354,4 +354,59 @@ class CWSolana extends Solana {
final solanaWallet = wallet as SolanaWallet;
await solanaWallet.updateTokenBalance(tokenMints: tokenMints);
}
static const _minTokenUsdValue = 0.1;
Future<({double usdValue, bool hasValidFiatPrice})> _getTokenUsdValueAndFiatCheck(
SPLToken token,
double balance,
) async {
try {
final settingsStore = getIt.get<SettingsStore>();
final torOnly = settingsStore.fiatApiMode == FiatApiMode.torOnly;
final price = await FiatConversionService.fetchPrice(
crypto: token,
fiat: FiatCurrency.usd,
torOnly: torOnly,
);
final hasValidFiatPrice = price > 0;
final usdValue = balance * price;
return (usdValue: usdValue, hasValidFiatPrice: hasValidFiatPrice);
} catch (e) {
return (usdValue: 0.0, hasValidFiatPrice: false);
}
}
@override
Future<void> discoverAndAddWalletTokens(WalletBase wallet) async {
if (wallet is! SolanaWallet) return;
try {
final result = await wallet.discoverTokensFromMoralis();
if (result.newTokens.isEmpty) return;
final List<Future<void>> tokenChecks = [];
for (final item in result.newTokens) {
tokenChecks.add((() async {
final token = item.token;
final fiatResult = await _getTokenUsdValueAndFiatCheck(token, item.balance);
final isSpam = !fiatResult.hasValidFiatPrice;
token.isPotentialScam = isSpam;
token.enabled = (fiatResult.usdValue >= _minTokenUsdValue) && !isSpam;
await wallet.addSPLToken(token);
})());
}
await Future.wait(tokenChecks);
} catch (_) {}
}
}
@@ -19,6 +19,7 @@ import 'package:cake_wallet/store/settings_store.dart';
import 'package:cw_core/balance.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/erc20_token.dart';
import 'package:cw_core/spl_token.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:mobx/mobx.dart';
@@ -382,9 +383,11 @@ abstract class BalanceViewModelBase with Store {
if (a.asset == wallet.currency) return -1;
}
if (isEVMCompatibleChain(wallet.type)) {
final aIsToken = a.asset is Erc20Token;
final bIsToken = b.asset is Erc20Token;
final isTokenWallet = isEVMCompatibleChain(wallet.type) || wallet.type == WalletType.solana;
if (isTokenWallet) {
final aIsToken = a.asset is Erc20Token || a.asset is SPLToken;
final bIsToken = b.asset is Erc20Token || b.asset is SPLToken;
final aHasBalance = (double.tryParse(a.availableBalance) ?? 0) > 0;
final bHasBalance = (double.tryParse(b.availableBalance) ?? 0) > 0;
+7
View File
@@ -942,6 +942,11 @@ import 'package:cw_solana/pending_solana_transaction.dart';
import 'package:cw_solana/solana_transaction_credentials.dart';
import 'package:cw_solana/solana_wallet_creation_credentials.dart';
import 'package:cw_solana/default_spl_tokens.dart';
import 'package:cake_wallet/core/fiat_conversion_service.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/fiat_api_mode.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'dart:convert';
import 'dart:typed_data';
@@ -1019,6 +1024,8 @@ abstract class Solana {
WalletBase wallet, {
List<String>? tokenMints,
});
Future<void> discoverAndAddWalletTokens(WalletBase wallet);
}
class JupiterSwapFailedException implements Exception {