What changed, and why it matters
This commit is a code-quality refactor for Solana token handling in the Stack Wallet app. It replaces loose 'dynamic' types with concrete SplToken/EthContract types, removes unsafe casts, and makes the Solana token wallet constructor carry a full token object instead of separate fields. There is no direct evidence of a security vulnerability being fixed, but the changes remove patterns (dynamic casts and type confusion between Ethereum and Solana tokens) that can lead to crashes or incorrect balance/price display.
Treat as a hardening/refactor commit. Review whether the previous dynamic casts could have been triggered by malformed or attacker-controlled token metadata, and consider adding input validation for custom SPL token records loaded from the database. No immediate security response is indicated by the diff alone.
Security signals we found
Removal of `dynamic tokenInfo` and `tokenInfo.decimals as int` unsafe cast in Solana token wallet creation
Replacement of try/catch dynamic token lookup with typed switch on wallet coin type
Removal of multiple `as dynamic` casts and `as EthContract` casts in desktop wallet summary
Constructor change centralizes token metadata in a typed SplToken object
No explicit security claim, CVE reference, or attribution in commit message
Evidence from the diff
The patch refactors Solana token wallet initialization and desktop wallet summary rendering. Key changes: (1) SolanaTokenWallet now takes a typed SplToken object and derives mint/name/symbol/decimals from it, instead of accepting separate fields with a dynamic source and an unsafe tokenInfo.decimals as int cast. (2) DesktopWalletSummary replaces try/catch dynamic dispatch with an explicit switch on the wallet coin type (Ethereum vs Solana), eliminating as dynamic casts and making the token contract/token wallet typed. (3) UI formatting now passes the SplToken to the amount formatter instead of pulling decimals/symbol via dynamic casts. These are defensive improvements that reduce type-confusion and cast-related runtime failures, but the commit message gives no indication a security bug was identified or fixed.
Changed components
lib/pages/token_view/sol_token_view.dartlib/pages_desktop_specific/my_stack_view/wallet_view/desktop_sol_token_view.dartlib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dartlib/wallets/wallet/impl/sub_wallets/solana_token_wallet.dartInspect captured patch +56 / −76
diff --git a/lib/pages/token_view/sol_token_view.dart b/lib/pages/token_view/sol_token_view.dart
index 7efe2dc..2673404 100644
--- a/lib/pages/token_view/sol_token_view.dart
+++ b/lib/pages/token_view/sol_token_view.dart
@@ -12,6 +12,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
+import '../../models/isar/models/isar_models.dart';
import '../../providers/db/main_db_provider.dart';
import '../../providers/providers.dart';
import '../../services/event_bus/events/global/wallet_sync_status_changed_event.dart';
@@ -63,7 +64,7 @@ class _SolTokenViewState extends ConsumerState<SolTokenView> {
: WalletSyncStatus.synced;
// Initialize the Solana token wallet provider with mock data.
- //
+ //
// This sets up the pCurrentSolanaTokenWallet provider so that
// SolanaTokenSummary can access the token wallet information.
WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -76,15 +77,15 @@ class _SolTokenViewState extends ConsumerState<SolTokenView> {
}
/// Initialize the Solana token wallet for this token view.
- ///
+ ///
/// Creates a SolanaTokenWallet with token data from DefaultSplTokens or the database.
/// First looks in DefaultSplTokens, then checks the database for custom tokens.
/// Sets it as the current token wallet in the provider so that UI widgets can access it.
- ///
+ ///
/// If the token is not found anywhere, sets the token wallet to null
/// so the UI can display an error message.
void _initializeSolanaTokenWallet() {
- dynamic tokenInfo;
+ SplToken? tokenInfo;
// First try to find in default tokens.
try {
@@ -119,19 +120,11 @@ class _SolTokenViewState extends ConsumerState<SolTokenView> {
if (parentWallet == null) {
ref.read(solanaTokenServiceStateProvider.state).state = null;
- debugPrint(
- 'ERROR: Wallet is not a SolanaWallet: ${widget.walletId}',
- );
+ debugPrint('ERROR: Wallet is not a SolanaWallet: ${widget.walletId}');
return;
}
- final solanaTokenWallet = SolanaTokenWallet(
- parentSolanaWallet: parentWallet,
- tokenMint: widget.tokenMint,
- tokenName: "${tokenInfo.name}",
- tokenSymbol: "${tokenInfo.symbol}",
- tokenDecimals: tokenInfo.decimals as int,
- );
+ final solanaTokenWallet = SolanaTokenWallet(parentWallet, tokenInfo);
ref.read(solanaTokenServiceStateProvider.state).state = solanaTokenWallet;
diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_sol_token_view.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_sol_token_view.dart
index f17c37a..7f96315 100644
--- a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_sol_token_view.dart
+++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_sol_token_view.dart
@@ -12,6 +12,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
+import '../../../models/isar/models/isar_models.dart';
import '../../../pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart';
import '../../../pages/token_view/sub_widgets/token_transaction_list_widget_sol.dart';
import '../../../providers/db/main_db_provider.dart';
@@ -84,7 +85,7 @@ class _DesktopTokenViewState extends ConsumerState<DesktopSolTokenView> {
/// so the UI can display an error message.
void _initializeSolanaTokenWallet() {
// First try to find in default tokens
- dynamic tokenInfo;
+ SplToken? tokenInfo;
try {
tokenInfo = DefaultSplTokens.list.firstWhere(
(token) => token.address == widget.tokenMint,
@@ -117,19 +118,11 @@ class _DesktopTokenViewState extends ConsumerState<DesktopSolTokenView> {
if (parentWallet == null) {
ref.read(solanaTokenServiceStateProvider.state).state = null;
- debugPrint(
- 'ERROR: Wallet is not a SolanaWallet: ${widget.walletId}',
- );
+ debugPrint('ERROR: Wallet is not a SolanaWallet: ${widget.walletId}');
return;
}
- final solanaTokenWallet = SolanaTokenWallet(
- parentSolanaWallet: parentWallet,
- tokenMint: widget.tokenMint,
- tokenName: "${tokenInfo.name}",
- tokenSymbol: "${tokenInfo.symbol}",
- tokenDecimals: tokenInfo.decimals as int,
- );
+ final solanaTokenWallet = SolanaTokenWallet(parentWallet, tokenInfo);
ref.read(solanaTokenServiceStateProvider.state).state = solanaTokenWallet;
diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart
index 9071a9e..99c671d 100644
--- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart
+++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart
@@ -23,7 +23,9 @@ import '../../../../utilities/amount/amount.dart';
import '../../../../utilities/amount/amount_formatter.dart';
import '../../../../utilities/enums/wallet_balance_toggle_state.dart';
import '../../../../utilities/text_styles.dart';
+import '../../../../wallets/crypto_currency/coins/ethereum.dart';
import '../../../../wallets/crypto_currency/coins/firo.dart';
+import '../../../../wallets/crypto_currency/coins/solana.dart';
import '../../../../wallets/crypto_currency/crypto_currency.dart'
show CryptoCurrency;
import '../../../../wallets/isar/providers/eth/current_token_wallet_provider.dart';
@@ -31,6 +33,7 @@ import '../../../../wallets/isar/providers/eth/token_balance_provider.dart';
import '../../../../wallets/isar/providers/solana/current_sol_token_wallet_provider.dart';
import '../../../../wallets/isar/providers/solana/sol_token_balance_provider.dart';
import '../../../../wallets/isar/providers/wallet_info_provider.dart';
+import '../../../../wallets/wallet/impl/sub_wallets/solana_token_wallet.dart';
import 'desktop_balance_toggle_button.dart';
class DesktopWalletSummary extends ConsumerStatefulWidget {
@@ -81,42 +84,43 @@ class _WDesktopWalletSummaryState extends ConsumerState<DesktopWalletSummary> {
);
// For Ethereum tokens, get the token contract; for Solana tokens, get the token wallet.
- dynamic tokenContract;
- dynamic solanaTokenWallet;
+ final EthContract? tokenContract;
+ final SolanaTokenWallet? solanaTokenWallet;
if (widget.isToken) {
- try {
- tokenContract = ref.watch(
- pCurrentTokenWallet.select((value) => value!.tokenContract),
- );
- } catch (_) {
- // Ethereum token not found, check for Solana.
- tokenContract = null;
- }
+ switch (ref.watch(pWalletCoin(walletId))) {
+ case Ethereum():
+ tokenContract = ref.watch(
+ pCurrentTokenWallet.select((value) => value!.tokenContract),
+ );
+ solanaTokenWallet = null;
+ break;
+
+ case Solana():
+ tokenContract = null;
+ // this cannot be null if coin is sol and isToken is true.
+ // if it is null, then there is a bug somewhere else.
+ solanaTokenWallet = ref.watch(pCurrentSolanaTokenWallet)!;
+ break;
- // Check for Solana token wallet if Ethereum token not found.
- if (tokenContract == null) {
- try {
- solanaTokenWallet = ref.watch(pCurrentSolanaTokenWallet);
- } catch (_) {
+ default:
+ tokenContract = null;
solanaTokenWallet = null;
- }
}
+ } else {
+ tokenContract = null;
+ solanaTokenWallet = null;
}
final price = widget.isToken && tokenContract != null
? ref.watch(
priceAnd24hChangeNotifierProvider.select(
- (value) => value.getTokenPrice(
- (tokenContract as dynamic).address as String,
- ),
+ (value) => value.getTokenPrice(tokenContract!.address),
),
)
: widget.isToken && solanaTokenWallet != null
? ref.watch(
priceAnd24hChangeNotifierProvider.select(
- (value) => value.getTokenPrice(
- "${(solanaTokenWallet as dynamic).tokenMint}",
- ),
+ (value) => value.getTokenPrice(solanaTokenWallet!.tokenMint),
),
)
: ref.watch(
@@ -149,7 +153,7 @@ class _WDesktopWalletSummaryState extends ConsumerState<DesktopWalletSummary> {
balance = ref.watch(
pTokenBalance((
walletId: walletId,
- contractAddress: (tokenContract as dynamic).address as String,
+ contractAddress: tokenContract.address,
)),
);
} else if (widget.isToken && solanaTokenWallet != null) {
@@ -157,7 +161,7 @@ class _WDesktopWalletSummaryState extends ConsumerState<DesktopWalletSummary> {
balance = ref.watch(
pSolanaTokenBalance((
walletId: walletId,
- tokenMint: (solanaTokenWallet as dynamic).tokenMint,
+ tokenMint: solanaTokenWallet.tokenMint,
)),
);
} else {
@@ -179,18 +183,13 @@ class _WDesktopWalletSummaryState extends ConsumerState<DesktopWalletSummary> {
FittedBox(
fit: BoxFit.scaleDown,
child: SelectableText(
- widget.isToken && solanaTokenWallet != null
- ? "${balanceToShow.decimal.toStringAsFixed(
- (solanaTokenWallet as dynamic).tokenDecimals as int,
- )} ${(solanaTokenWallet as dynamic).tokenSymbol}"
- : ref
- .watch(pAmountFormatter(coin))
- .format(
- balanceToShow,
- ethContract: tokenContract != null
- ? tokenContract as EthContract?
- : null,
- ),
+ ref
+ .watch(pAmountFormatter(coin))
+ .format(
+ balanceToShow,
+ ethContract: tokenContract,
+ splToken: solanaTokenWallet?.splToken,
+ ),
style: STextStyles.desktopH3(context),
),
),
@@ -222,7 +221,7 @@ class _WDesktopWalletSummaryState extends ConsumerState<DesktopWalletSummary> {
walletId: walletId,
initialSyncStatus: widget.initialSyncStatus,
tokenContractAddress: widget.isToken && tokenContract != null
- ? (tokenContract as EthContract).address
+ ? tokenContract.address
: null,
),
diff --git a/lib/wallets/wallet/impl/sub_wallets/solana_token_wallet.dart b/lib/wallets/wallet/impl/sub_wallets/solana_token_wallet.dart
index c41ca6c..69883f5 100644
--- a/lib/wallets/wallet/impl/sub_wallets/solana_token_wallet.dart
+++ b/lib/wallets/wallet/impl/sub_wallets/solana_token_wallet.dart
@@ -15,16 +15,14 @@ import 'package:solana/solana.dart' hide Wallet;
import '../../../../db/isar/main_db.dart';
import '../../../../models/balance.dart';
-import '../../../../models/isar/models/blockchain_data/transaction.dart';
import '../../../../models/isar/models/blockchain_data/v2/input_v2.dart';
import '../../../../models/isar/models/blockchain_data/v2/output_v2.dart';
import '../../../../models/isar/models/blockchain_data/v2/transaction_v2.dart';
+import '../../../../models/isar/models/isar_models.dart';
import '../../../../models/paymint/fee_object_model.dart';
import '../../../../services/solana/solana_token_api.dart';
import '../../../../utilities/amount/amount.dart';
import '../../../../utilities/logger.dart';
-import '../../../crypto_currency/crypto_currency.dart';
-import '../../../isar/models/wallet_solana_token_info.dart';
import '../../../models/tx_data.dart';
import '../../wallet.dart';
import '../solana_wallet.dart';
@@ -37,21 +35,18 @@ class SolanaTokenWallet extends Wallet {
/// Create a new Solana Token Wallet.
///
/// Requires a parent SolanaWallet to provide RPC client and key management.
- SolanaTokenWallet({
- required this.parentSolanaWallet,
- required this.tokenMint,
- required this.tokenName,
- required this.tokenSymbol,
- required this.tokenDecimals,
- }) : super(Solana(CryptoCurrencyNetwork.main)); // TODO: make testnet-capable.
+ SolanaTokenWallet(this.parentSolanaWallet, this.splToken)
+ : super(parentSolanaWallet.cryptoCurrency);
/// Parent Solana wallet (provides RPC client and keypair access).
final SolanaWallet parentSolanaWallet;
- final String tokenMint;
- final String tokenName;
- final String tokenSymbol;
- final int tokenDecimals;
+ final SplToken splToken;
+
+ String get tokenMint => splToken.address;
+ String get tokenName => splToken.name;
+ String get tokenSymbol => splToken.symbol;
+ int get tokenDecimals => splToken.decimals;
/// Override walletId to delegate to parent wallet
@override
Why this scored 21/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.