fix(spl): handle Sol tokens the same way Eth tokens are
What changed, and why it matters
This commit refactors how Solana (SPL) tokens are added and managed in Stack Wallet so they follow the same pattern already used for Ethereum tokens. It removes a large block of debug-only ownership-check code and replaces direct database updates with a new wallet method, `updateSolanaTokens`. It also makes sure the default Solana token list is seeded into the database if it is empty, mirroring existing Ethereum behavior. There is no direct evidence in the commit that this fixes an active security vulnerability, but it removes code that performed RPC calls and ownership checks from the UI layer, which reduces the attack surface and potential for mistakes.
Review the implementation of `wallet.updateSolanaTokens` to confirm it correctly validates and persists default and custom SPL token mint addresses, and that it does not reintroduce the removed ownership RPC calls in a less visible location. Verify that seeding `DefaultSplTokens.list` into the database does not overwrite user-defined custom tokens or allow injection of malicious default entries. Consider whether this change warrants a changelog note for users who previously saw ownership-check debug output.
Security signals we found
Removal of RPC client initialization and ownership-check calls from UI code
Consolidation of token-update logic behind a single wallet method
Alignment of Solana token seeding with existing Ethereum token seeding
No explicit security claim, CVE, or attribution in commit message or diff
Evidence from the diff
The change unifies Solana token handling with Ethereum token handling in two UI files. In edit_wallet_tokens_view.dart, the previous Solana-specific branch that called walletInfo.updateSolanaTokenMintAddresses, updateSolanaCustomTokenMintAddresses, and ran an ownsToken RPC check via SolanaTokenAPI is replaced by a single call to wallet.updateSolanaTokens(selectedTokens). The import of solana_token_api.dart is removed. In both files, SPL token loading now seeds DefaultSplTokens.list into the database when empty, matching the existing Ethereum default-token seeding logic. The diff shows a net reduction of 93 lines and removes debug logging and RPC initialization from the UI.
Changed components
lib/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dartlib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dartSolanaWallet.updateSolanaTokens (new method referenced but not shown)SolanaTokenAPI (usage removed)Inspect captured patch +34 / −127
diff --git a/lib/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart b/lib/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart
index bace01e..1460f18 100644
--- a/lib/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart
+++ b/lib/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart
@@ -19,7 +19,6 @@ import '../../../db/isar/main_db.dart';
import '../../../models/isar/models/ethereum/eth_contract.dart';
import '../../../models/isar/models/solana/spl_token.dart';
import '../../../notifications/show_flush_bar.dart';
-import '../../../services/solana/solana_token_api.dart';
import '../../../pages_desktop_specific/desktop_home_view.dart';
import '../../../providers/global/price_provider.dart';
import '../../../providers/global/wallets_provider.dart';
@@ -109,112 +108,11 @@ class _EditWalletTokensViewState extends ConsumerState<EditWalletTokensView> {
final wallet = ref.read(pWallets).getWallet(widget.walletId);
- // Handle Ethereum tokens.
+ // Handle tokens.
if (wallet is EthereumWallet) {
await wallet.updateTokenContracts(selectedTokens);
- }
- // Handle Solana tokens.
- else if (wallet is SolanaWallet) {
- // Get WalletInfo and update Solana token mint addresses.
- final walletInfo = wallet.info;
-
- // Separate selected tokens into default and custom.
- final defaultTokenMints = DefaultSplTokens.list.map((e) => e.address).toSet();
- final selectedDefaultTokens = selectedTokens.where(
- (mint) => defaultTokenMints.contains(mint),
- ).toSet();
- final selectedCustomTokens = selectedTokens.where(
- (mint) => !defaultTokenMints.contains(mint),
- ).toSet();
-
- // Update default token mint addresses.
- await walletInfo.updateSolanaTokenMintAddresses(
- newMintAddresses: selectedDefaultTokens,
- isar: MainDB.instance.isar,
- );
-
- // Update custom token mint addresses.
- await walletInfo.updateSolanaCustomTokenMintAddresses(
- newMintAddresses: selectedCustomTokens,
- isar: MainDB.instance.isar,
- );
-
- // Log selected tokens and verify ownership.
- debugPrint('===== SOLANA TOKEN OWNERSHIP CHECK =====');
- debugPrint('Wallet: ${walletInfo.name}');
- debugPrint('Selected token mint addresses: $selectedTokens');
-
- // Get wallet's receiving address for ownership checks.
- try {
- final receivingAddressObj = await wallet.getCurrentReceivingAddress();
- if (receivingAddressObj == null) {
- debugPrint('Error: Could not get wallet receiving address');
- return;
- }
- final receivingAddress = receivingAddressObj.value;
- debugPrint('Wallet address: $receivingAddress');
- debugPrint('');
-
- // Check ownership of each selected token.
- for (final mintAddress in selectedTokens) {
- // Find the token entity to get token details.
- final tokenEntity = tokenEntities.firstWhere(
- (e) => e.token.address == mintAddress,
- orElse: () => AddTokenListElementData(
- // Fallback contract with just the address.
- EthContract(
- address: mintAddress,
- name: 'Unknown Token',
- symbol: mintAddress,
- decimals: 0,
- type: EthContractType.erc20,
- ),
- ),
- );
-
- final tokenName = tokenEntity.token.name;
- final tokenSymbol = tokenEntity.token.symbol;
-
- debugPrint('Token: $tokenName ($tokenSymbol)');
- debugPrint(' Mint: $mintAddress');
-
- // Check if wallet owns this token using the API.
- try {
- // Initialize the RPC client for the SolanaTokenAPI.
- final tokenApi = SolanaTokenAPI();
- final rpcClient = wallet.getRpcClient();
-
- if (rpcClient != null) {
- tokenApi.initializeRpcClient(rpcClient);
-
- final ownershipResult = await tokenApi.ownsToken(
- receivingAddress,
- mintAddress,
- );
-
- if (ownershipResult.isSuccess) {
- if (ownershipResult.value == true) {
- debugPrint('OWNS token - token account found');
- } else {
- debugPrint('DOES NOT own token - no token account found');
- }
- } else {
- debugPrint(
- 'Error checking ownership: ${ownershipResult.exception}',
- );
- }
- } else {
- debugPrint('Warning: RPC client not initialized for wallet');
- }
- } catch (e) {
- debugPrint('Exception checking ownership: $e');
- }
- }
-
- debugPrint('========================================');
- } catch (e) {
- debugPrint('Error getting wallet address: $e');
- }
+ } else if (wallet is SolanaWallet) {
+ await wallet.updateSolanaTokens(selectedTokens);
}
if (mounted) {
if (widget.contractsToMarkSelected == null) {
@@ -347,29 +245,23 @@ class _EditWalletTokensViewState extends ConsumerState<EditWalletTokensView> {
final wallet = ref.read(pWallets).getWallet(widget.walletId);
- // Load appropriate tokens based on wallet type.
if (wallet is SolanaWallet) {
- // Load both default and custom Solana tokens.
- final defaultSplTokens = DefaultSplTokens.list;
- tokenEntities.addAll(defaultSplTokens.map((e) => AddTokenListElementData(e)));
-
- // Load custom tokens from database
- final customSplTokens = MainDB.instance.getSplTokens().findAllSync();
-
- // Deduplicate: only add custom tokens that aren't already in defaults.
- final seenAddresses = <String>{
- ...defaultSplTokens.map((e) => e.address),
- ...tokenEntities.map((e) => e.token.address),
- };
-
- for (final token in customSplTokens) {
- if (!seenAddresses.contains(token.address)) {
- tokenEntities.add(AddTokenListElementData(token));
- seenAddresses.add(token.address);
- }
+ final contracts = MainDB.instance
+ .getSplTokens()
+ .sortByName()
+ .findAllSync();
+
+ if (contracts.isEmpty) {
+ contracts.addAll(DefaultSplTokens.list);
+ MainDB.instance
+ .putSplTokens(contracts)
+ .then(
+ (_) => ref.read(priceAnd24hChangeNotifierProvider).updatePrice(),
+ );
}
+
+ tokenEntities.addAll(contracts.map((e) => AddTokenListElementData(e)));
} else {
- // Load Ethereum tokens (default behavior for Ethereum wallets).
final contracts = MainDB.instance
.getEthContracts()
.sortByName()
diff --git a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart
index c1db3b5..4f94e05 100644
--- a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart
+++ b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart
@@ -29,6 +29,7 @@ import '../../../themes/stack_colors.dart';
import '../../../utilities/assets.dart';
import '../../../utilities/constants.dart';
import '../../../utilities/default_eth_tokens.dart';
+import '../../../utilities/default_spl_tokens.dart';
import '../../../utilities/text_styles.dart';
import '../../../utilities/util.dart';
import '../../../wallets/crypto_currency/crypto_currency.dart';
@@ -192,8 +193,22 @@ class _AddWalletViewState extends ConsumerState<AddWalletView> {
}
if (AppConfig.coins.whereType<Solana>().isNotEmpty) {
- final tokens = MainDB.instance.getSplTokens().findAllSync();
- solTokenEntities.addAll(tokens.map((e) => SolTokenEntity(e)));
+ final contracts = MainDB.instance
+ .getSplTokens()
+ .sortByName()
+ .findAllSync();
+
+ if (contracts.isEmpty) {
+ contracts.addAll(DefaultSplTokens.list);
+ MainDB.instance
+ .putSplTokens(contracts)
+ .then(
+ (value) =>
+ ref.read(priceAnd24hChangeNotifierProvider).updatePrice(),
+ );
+ }
+
+ solTokenEntities.addAll(contracts.map((e) => SolTokenEntity(e)));
}
WidgetsBinding.instance.addPostFrameCallback((_) {
Why this scored 27/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.