mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-27 06:18:50 +01:00
Merge branch 'cake-tech:dev' into dev
This commit is contained in:
@@ -35,7 +35,7 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin {
|
||||
|
||||
@override
|
||||
@HiveField(8, defaultValue: false)
|
||||
final bool isPotentialScam;
|
||||
bool isPotentialScam;
|
||||
|
||||
SPLToken({
|
||||
required this.name,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user