Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cw_core/lib/spl_token.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin {

@override
@HiveField(8, defaultValue: false)
final bool isPotentialScam;
bool isPotentialScam;

SPLToken({
required this.name,
Expand Down
92 changes: 88 additions & 4 deletions cw_solana/lib/solana_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -422,8 +422,7 @@ class SolanaWalletClient {
}
}

final bool isSplToSplSwap =
decreasedMintForWallet != null &&
final bool isSplToSplSwap = decreasedMintForWallet != null &&
increasedMintForWallet != null &&
decreasedMintForWallet != increasedMintForWallet;

Expand Down Expand Up @@ -1437,7 +1436,7 @@ class SolanaWalletClient {
// For transaction history loading (shouldCreateATA: false), try standard token program first
// to avoid unnecessary RPC call. Only fetch token program ID when creating accounts.
SolAddress tokenProgramId = SPLTokenProgramConst.tokenProgramId;

if (shouldCreateATA) {
// Only fetch token program ID when we need to create an account
tokenProgramId = await _getTokenProgramId(mintAddress);
Expand Down Expand Up @@ -1475,7 +1474,7 @@ class SolanaWalletClient {
owner: ownerAddress,
tokenProgramId: token2022ProgramId,
);

try {
accountInfo = await _provider!.request(
SolanaRPCGetAccountInfo(
Expand Down Expand Up @@ -1784,4 +1783,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,
});
}
80 changes: 79 additions & 1 deletion cw_solana/lib/solana_wallet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,8 @@ abstract class SolanaWalletBase
if (!hasKeysFile) rethrow;
}

final balance = SolanaBalance.fromJSON(data?['balance'] as String?, false) ?? SolanaBalance(0.0, false);
final balance =
SolanaBalance.fromJSON(data?['balance'] as String?, false) ?? SolanaBalance(0.0, false);

final WalletKeysData keysData;
// Migrate wallet from the old scheme to then new .keys file scheme
Expand Down Expand Up @@ -630,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);

Expand Down Expand Up @@ -765,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: []);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import 'package:cake_wallet/new-ui/pages/send_page.dart';
import 'package:cake_wallet/new-ui/pages/swap_page.dart';
import 'package:cake_wallet/new-ui/widgets/modern_button.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/unspent_coin_type.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';

enum AssetDetailsModalModes { normal, ltcTransparent, ltcPrivate }
Expand All @@ -29,7 +29,9 @@ class AssetDetailsModal extends StatelessWidget {
required this.iconPath,
required this.chainIconPath,
required this.mode,
required this.wallet, required this.showSwap, this.asset});
required this.wallet,
required this.showSwap,
this.asset});

final String title;
final CryptoCurrency? asset;
Expand Down Expand Up @@ -70,38 +72,39 @@ class AssetDetailsModal extends StatelessWidget {
height: 75,
child: Stack(
children: [
if(iconPath.isNotEmpty)
Image.asset(iconPath, width: 75, height: 75)
if (iconPath.isNotEmpty)
CakeImageWidget(imageUrl: iconPath, width: 75, height: 75)
else
Container(
width: 75,
height: 75,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(99999)),
child: Center(
child: Text(
title.substring(0, 2),
style: TextStyle(
fontSize: 28, color: Theme.of(context).colorScheme.onPrimary),
)),
),
Container(
width: 75,
height: 75,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(99999)),
child: Center(
child: Text(
title.substring(0, 2),
style: TextStyle(
fontSize: 28, color: Theme.of(context).colorScheme.onPrimary),
)),
),
if (chainIconPath.isNotEmpty)
Align(
alignment: Alignment.bottomRight,
child: Container(
decoration: ShapeDecoration(
shape: RoundedSuperellipseBorder(
borderRadius: BorderRadius.circular(8),side: BorderSide(color: Colors.black)),
borderRadius: BorderRadius.circular(8),
side: BorderSide(color: Colors.black)),
color: Colors.white),
child: Padding(
padding: const EdgeInsets.all(4.0),
child: SvgPicture.asset(
chainIconPath,
child: CakeImageWidget(
imageUrl: chainIconPath,
width: 18,
height: 18,
colorFilter:
ColorFilter.mode(Colors.black, BlendMode.srcIn),
ColorFilter.mode(Colors.black, BlendMode.srcIn),
),
)))
],
Expand Down Expand Up @@ -214,10 +217,10 @@ class AssetDetailsModal extends StatelessWidget {
}
openPage<NewReceivePage>(context, param2: asset);
}),
if(showSwap)
AssetDetailsModalBottomButton(
iconPath: "assets/new-ui/exchange.svg",
title: S.of(context).swap,
if (showSwap)
AssetDetailsModalBottomButton(
iconPath: "assets/new-ui/exchange.svg",
title: S.of(context).swap,
onPressed: () => openPage<NewSwapPage>(context, param2: asset)),
],
),
Expand Down
Loading
Loading