What changed, and why it matters
This commit cleans up Solana wallet handling in Stack Wallet. It removes a duplicate desktop Solana card widget, makes Solana token wallets delegate more operations to the parent Solana wallet (such as address filters and node updates), and fixes balance display formatting so Solana token balances use the correct token contract details. There is no clear security vulnerability in the diff, but the changes correct consistency issues that could have led to stale or incorrect wallet state.
Review the Solana token wallet delegation changes for correctness, especially the new TransactionSubType.ethToken filter condition, to ensure it matches the intended Solana token transaction subtype. Verify that removing the dedicated Solana desktop card does not lose any Solana-specific UI behavior. No immediate security patch appears necessary from this diff alone.
Security signals we found
Solana token wallet previously returned null for change/receiving address filters and no-op for updateNode/checkSaveInitialReceivingAddress, which could cause incomplete wallet state handling
Transaction filter now adds an additional TransactionSubType.ethToken condition, potentially narrowing query results to correct token transactions
Removal of duplicated Solana-specific desktop UI path reduces maintenance surface and chance of inconsistent behavior
Balance formatting now passes solContract to the formatter, fixing possible display of wrong decimal places or units
Evidence from the diff
The patch consolidates Solana token wallet behavior: SolanaTokenWallet now delegates change/receiving address filters, node updates, and initial receiving address checks to parentSolanaWallet instead of returning null or no-op. Its transaction filter now also requires TransactionSubType.ethToken alongside contractAddress. The UI removes a Solana-specific desktop expanding card in favor of the generic DesktopExpandingWalletCard. Balance and info row widgets are refactored to treat contracts generically and pass SolContract to the amount formatter. These are correctness/consistency fixes rather than a clear exploit.
Changed components
lib/wallets/wallet/impl/sub_wallets/solana_token_wallet.dartlib/pages/wallets_view/wallets_overview.dartlib/pages_desktop_specific/my_stack_view/dialogs/desktop_expanding_solana_wallet_card.dartlib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance.dartlib/widgets/wallet_info_row/wallet_info_row.dartInspect captured patch +50 / −263
diff --git a/lib/pages/wallets_view/wallets_overview.dart b/lib/pages/wallets_view/wallets_overview.dart
index 29aaa3b..c1adb8d 100644
--- a/lib/pages/wallets_view/wallets_overview.dart
+++ b/lib/pages/wallets_view/wallets_overview.dart
@@ -16,7 +16,6 @@ import 'package:isar_community/isar.dart';
import '../../app_config.dart';
import '../../models/add_wallet_list_entity/sub_classes/coin_entity.dart';
import '../../models/isar/models/contract.dart';
-import '../../pages_desktop_specific/my_stack_view/dialogs/desktop_expanding_solana_wallet_card.dart';
import '../../pages_desktop_specific/my_stack_view/dialogs/desktop_expanding_wallet_card.dart';
import '../../providers/providers.dart';
import '../../services/event_bus/events/wallet_added_event.dart';
@@ -343,23 +342,14 @@ class _EthWalletsOverviewState extends ConsumerState<WalletsOverview> {
if (wallet.cryptoCurrency.hasTokenSupport) {
if (isDesktop) {
- if (wallet.cryptoCurrency is Solana) {
- return DesktopExpandingSolanaWalletCard(
- key: Key(
- "${wallet.walletId}_${entry.contracts.map((e) => e.address).join()}",
- ),
- data: entry,
- navigatorState: widget.navigatorState!,
- );
- } else {
- return DesktopExpandingWalletCard(
- key: Key(
- "${wallet.walletId}_${entry.contracts.map((e) => e.address).join()}",
- ),
- data: entry,
- navigatorState: widget.navigatorState!,
- );
- }
+ return DesktopExpandingWalletCard(
+ key: Key(
+ "${wallet.walletId}_${entry.contracts.map((e) => e.address).join()}",
+ ),
+ data: entry,
+ navigatorState: widget.navigatorState!,
+ );
+ // }
} else {
return MasterWalletCard(
key: Key(wallet.walletId),
diff --git a/lib/pages_desktop_specific/my_stack_view/dialogs/desktop_expanding_solana_wallet_card.dart b/lib/pages_desktop_specific/my_stack_view/dialogs/desktop_expanding_solana_wallet_card.dart
deleted file mode 100644
index db0afd6..0000000
--- a/lib/pages_desktop_specific/my_stack_view/dialogs/desktop_expanding_solana_wallet_card.dart
+++ /dev/null
@@ -1,201 +0,0 @@
-/*
- * This file is part of Stack Wallet.
- *
- * Copyright (c) 2025 Cypher Stack
- * All Rights Reserved.
- * The code is distributed under GPLv3 license, see LICENSE file for details.
- * Generated by Cypher Stack on 2025-11-20
- *
- */
-
-import 'package:flutter/material.dart';
-import 'package:flutter_svg/flutter_svg.dart';
-import 'package:flutter_svg/svg.dart';
-
-import '../../../pages/wallets_view/wallets_overview.dart';
-import '../../../themes/stack_colors.dart';
-import '../../../utilities/assets.dart';
-import '../../../utilities/constants.dart';
-import '../../../utilities/text_styles.dart';
-import '../../../widgets/animated_widgets/rotate_icon.dart';
-import '../../../widgets/expandable.dart';
-import '../../../widgets/rounded_white_container.dart';
-import '../../../widgets/wallet_card.dart';
-import '../../../widgets/wallet_info_row/sub_widgets/wallet_info_row_balance.dart';
-import '../../../widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart';
-
-class DesktopExpandingSolanaWalletCard extends StatefulWidget {
- const DesktopExpandingSolanaWalletCard({
- super.key,
- required this.data,
- required this.navigatorState,
- });
-
- final WalletListItemData data;
- final NavigatorState navigatorState;
-
- @override
- State<DesktopExpandingSolanaWalletCard> createState() =>
- _DesktopExpandingSolanaWalletCardState();
-}
-
-class _DesktopExpandingSolanaWalletCardState
- extends State<DesktopExpandingSolanaWalletCard> {
- final expandableController = ExpandableController();
- final rotateIconController = RotateIconController();
- final List<String> tokenMintAddresses = [];
-
- @override
- void initState() {
- if (widget.data.wallet.cryptoCurrency.hasTokenSupport) {
- tokenMintAddresses.addAll(
- widget.data.contracts.map((e) => e.address),
- );
- }
-
- super.initState();
- }
-
- @override
- Widget build(BuildContext context) {
- return RoundedWhiteContainer(
- padding: EdgeInsets.zero,
- borderColor: Theme.of(context).extension<StackColors>()!.backgroundAppBar,
- child: Expandable(
- initialState: widget.data.wallet.cryptoCurrency.hasTokenSupport
- ? ExpandableState.expanded
- : ExpandableState.collapsed,
- controller: expandableController,
- onExpandWillChange: (toState) {
- if (toState == ExpandableState.expanded) {
- rotateIconController.forward?.call();
- } else {
- rotateIconController.reverse?.call();
- }
- },
- header: Padding(
- padding: const EdgeInsets.symmetric(
- horizontal: 20,
- vertical: 14,
- ),
- child: Row(
- children: [
- Expanded(
- child: Row(
- children: [
- Expanded(
- flex: 2,
- child: Row(
- children: [
- WalletInfoCoinIcon(
- coin: widget.data.wallet.info.coin,
- ),
- const SizedBox(
- width: 12,
- ),
- Text(
- widget.data.wallet.info.name,
- style: STextStyles.desktopTextExtraSmall(context)
- .copyWith(
- color: Theme.of(context)
- .extension<StackColors>()!
- .textDark,
- ),
- ),
- ],
- ),
- ),
- Expanded(
- flex: 4,
- child: WalletInfoRowBalance(
- walletId: widget.data.wallet.walletId,
- ),
- ),
- ],
- ),
- ),
- MaterialButton(
- padding: const EdgeInsets.all(5),
- materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
- minWidth: 32,
- height: 32,
- color: Theme.of(context)
- .extension<StackColors>()!
- .textFieldDefaultBG,
- elevation: 0,
- hoverElevation: 0,
- disabledElevation: 0,
- highlightElevation: 0,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(
- Constants.size.circularBorderRadius,
- ),
- ),
- onPressed: () {
- if (expandableController.state == ExpandableState.collapsed) {
- rotateIconController.forward?.call();
- } else {
- rotateIconController.reverse?.call();
- }
- expandableController.toggle?.call();
- },
- child: RotateIcon(
- controller: rotateIconController,
- icon: RotatedBox(
- quarterTurns: 2,
- child: SvgPicture.asset(
- Assets.svg.chevronDown,
- width: 14,
- ),
- ),
- curve: Curves.easeInOut,
- ),
- ),
- ],
- ),
- ),
- body: ListView(
- shrinkWrap: true,
- primary: false,
- children: [
- Container(
- width: double.infinity,
- height: 1,
- color:
- Theme.of(context).extension<StackColors>()!.backgroundAppBar,
- ),
- Padding(
- padding: const EdgeInsets.only(
- left: 32,
- right: 14,
- top: 14,
- bottom: 14,
- ),
- child: SimpleWalletCard(
- walletId: widget.data.wallet.walletId,
- popPrevious: true,
- desktopNavigatorState: widget.navigatorState,
- ),
- ),
- ...tokenMintAddresses.map(
- (e) => Padding(
- padding: const EdgeInsets.only(
- left: 32,
- right: 14,
- top: 14,
- bottom: 14,
- ),
- child: SimpleWalletCard(
- walletId: widget.data.wallet.walletId,
- contractAddress: e,
- popPrevious: true,
- desktopNavigatorState: widget.navigatorState,
- ),
- ),
- ),
- ],
- ),
- ),
- );
- }
-}
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 fc7c050..bf0e4b1 100644
--- a/lib/wallets/wallet/impl/sub_wallets/solana_token_wallet.dart
+++ b/lib/wallets/wallet/impl/sub_wallets/solana_token_wallet.dart
@@ -13,7 +13,6 @@ import 'package:isar_community/isar.dart';
import 'package:solana/dto.dart';
import 'package:solana/solana.dart' hide Wallet;
-import '../../../../db/isar/main_db.dart';
import '../../../../models/balance.dart';
import '../../../../models/isar/models/blockchain_data/v2/input_v2.dart';
import '../../../../models/isar/models/blockchain_data/v2/output_v2.dart';
@@ -44,20 +43,21 @@ class SolanaTokenWallet extends Wallet {
int get tokenDecimals => solContract.decimals;
@override
- String get walletId => parentSolanaWallet.walletId;
+ FilterOperation? get changeAddressFilterOperation =>
+ parentSolanaWallet.changeAddressFilterOperation;
@override
- MainDB get mainDB => parentSolanaWallet.mainDB;
+ FilterOperation? get receivingAddressFilterOperation =>
+ parentSolanaWallet.receivingAddressFilterOperation;
@override
- FilterOperation? get changeAddressFilterOperation => null;
-
- @override
- FilterOperation? get receivingAddressFilterOperation => null;
-
- @override
- FilterOperation? get transactionFilterOperation =>
- FilterCondition.equalTo(property: r"contractAddress", value: tokenMint);
+ FilterOperation? get transactionFilterOperation => FilterGroup.and([
+ FilterCondition.equalTo(property: r"contractAddress", value: tokenMint),
+ const FilterCondition.equalTo(
+ property: r"subType",
+ value: TransactionSubType.ethToken,
+ ),
+ ]);
@override
Future<void> init() async {
@@ -472,7 +472,7 @@ class SolanaTokenWallet extends Wallet {
@override
Future<void> updateNode() async {
- // No-op for token wallet.
+ await parentSolanaWallet.updateNode();
}
@override
@@ -748,7 +748,9 @@ class SolanaTokenWallet extends Wallet {
}
@override
- Future<void> checkSaveInitialReceivingAddress() async {}
+ Future<void> checkSaveInitialReceivingAddress() async {
+ await parentSolanaWallet.checkSaveInitialReceivingAddress();
+ }
Future<String?> _findTokenAccount({
required String ownerAddress,
diff --git a/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance.dart b/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance.dart
index 096428c..298304c 100644
--- a/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance.dart
+++ b/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance.dart
@@ -10,7 +10,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
+
import '../../../db/isar/main_db.dart';
+import '../../../models/isar/models/contract.dart';
import '../../../models/isar/models/ethereum/eth_contract.dart';
import '../../../models/isar/models/solana/sol_contract.dart';
import '../../../themes/stack_colors.dart';
@@ -38,56 +40,57 @@ class WalletInfoRowBalance extends ConsumerWidget {
final info = ref.watch(pWalletInfo(walletId));
final Amount totalBalance;
- EthContract? contract;
- SolContract? splToken;
+ Contract? contract;
if (contractAddress == null) {
- totalBalance = info.cachedBalance.total +
+ totalBalance =
+ info.cachedBalance.total +
info.cachedBalanceSecondary.total +
info.cachedBalanceTertiary.total;
contract = null;
- splToken = null;
} else {
// Check if it's a Solana wallet.
if (info.coin is Solana) {
- splToken = MainDB.instance.getSolContractSync(contractAddress!);
- if (splToken != null) {
- final solanaTokenInfo = ref
- .watch(
- pSolanaTokenWalletInfo(
- (walletId: walletId, tokenMint: contractAddress!),
- ),
- );
+ contract = MainDB.instance.getSolContractSync(contractAddress!);
+ if (contract != null) {
+ final solanaTokenInfo = ref.watch(
+ pSolanaTokenWalletInfo((
+ walletId: walletId,
+ tokenMint: contractAddress!,
+ )),
+ );
totalBalance = solanaTokenInfo.getCachedBalance().total;
} else {
// Token not yet in database, show zero balance.
totalBalance = Amount(rawValue: BigInt.zero, fractionDigits: 0);
}
- contract = null;
} else {
// Ethereum token.
contract = MainDB.instance.getEthContractSync(contractAddress!);
if (contract != null) {
totalBalance = ref
.watch(
- pTokenBalance(
- (walletId: walletId, contractAddress: contractAddress!),
- ),
+ pTokenBalance((
+ walletId: walletId,
+ contractAddress: contractAddress!,
+ )),
)
.total;
} else {
// Contract not yet in database, show zero balance.
totalBalance = Amount(rawValue: BigInt.zero, fractionDigits: 0);
}
- splToken = null;
}
}
return Text(
- ref.watch(pAmountFormatter(info.coin)).format(
+ ref
+ .watch(pAmountFormatter(info.coin))
+ .format(
totalBalance,
- ethContract: contract,
+ ethContract: contract is EthContract ? contract : null,
+ solContract: contract is SolContract ? contract : null,
),
style: Util.isDesktop
? STextStyles.desktopTextExtraSmall(context).copyWith(
diff --git a/lib/widgets/wallet_info_row/wallet_info_row.dart b/lib/widgets/wallet_info_row/wallet_info_row.dart
index ba2dfa7..835a853 100644
--- a/lib/widgets/wallet_info_row/wallet_info_row.dart
+++ b/lib/widgets/wallet_info_row/wallet_info_row.dart
@@ -12,7 +12,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../models/isar/models/contract.dart';
-import '../../models/isar/models/ethereum/eth_contract.dart';
import '../../providers/providers.dart';
import '../../themes/stack_colors.dart';
import '../../utilities/text_styles.dart';
@@ -41,13 +40,9 @@ class WalletInfoRow extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final wallet = ref.watch(pWallets).getWallet(walletId);
- final walletInfo = ref.watch(pWalletInfo(walletId));
-
Contract? contract;
- String? contractName;
-
if (contractAddress != null) {
- if (walletInfo.coin is Solana) {
+ if (wallet.info.coin is Solana) {
// Solana token.
final solContract = ref.watch(
mainDBProvider.select(
@@ -55,7 +50,6 @@ class WalletInfoRow extends ConsumerWidget {
),
);
contract = solContract;
- contractName = solContract?.name;
} else {
// Ethereum token.
final ethContract = ref.watch(
@@ -64,7 +58,6 @@ class WalletInfoRow extends ConsumerWidget {
),
);
contract = ethContract;
- contractName = ethContract?.name;
}
}
@@ -84,11 +77,11 @@ class WalletInfoRow extends ConsumerWidget {
contractAddress: contractAddress,
),
const SizedBox(width: 12),
- contractName != null
+ contract != null
? Row(
children: [
Text(
- contractName!,
+ contract.name,
style:
STextStyles.desktopTextExtraSmall(
context,
@@ -157,11 +150,11 @@ class WalletInfoRow extends ConsumerWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- if (contractName != null)
+ if (contract != null)
Row(
children: [
Text(
- contractName!,
+ contract.name,
style: STextStyles.titleBold12(context),
),
const SizedBox(width: 4),
Why this scored 23/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.