fix(spl): Solana token specific transaction list widget
What changed, and why it matters
This commit adds a Solana-specific token transaction list widget and wires up the SolanaTokenAPI to use the wallet's real RPC client instead of a placeholder. It is a feature/fix for displaying SPL token transactions and checking token ownership. There is no direct evidence in the commit of a security vulnerability being patched.
No immediate security action required. Treat as routine feature/fix. If reviewing for defense-in-depth, verify that SolanaTokenAPI.ownsToken() and transactionFilterOperation cannot be manipulated by untrusted data, and that RPC credentials are not exposed through getRpcClient().
Security signals we found
RPC client accessor added to wallet implementation
Token ownership check now uses live RPC instead of placeholder
New transaction list widget filters by walletId and optional token-specific filter
No explicit security fix or vulnerability description in commit message or diff
Evidence from the diff
The change introduces a new SolanaTokenTransactionsList widget that queries TransactionV2 records filtered by a Solana token wallet’s transactionFilterOperation. It also exposes SolanaWallet._rpcClient via getRpcClient() and updates EditWalletTokensView to initialize SolanaTokenAPI with that RPC client before calling ownsToken(). The previous ownsToken() call was a placeholder returning false; now it uses a live RPC if available. No input validation, cryptographic, or authorization changes are visible in the diff.
Changed components
lib/pages/token_view/sub_widgets/token_transaction_list_widget_sol.dartlib/pages/token_view/sol_token_view.dartlib/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dartlib/wallets/wallet/impl/solana_wallet.dartInspect captured patch +208 / −15
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 c5391be..1602aa4 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
@@ -163,24 +163,31 @@ class _EditWalletTokensViewState extends ConsumerState<EditWalletTokensView> {
// Check if wallet owns this token using the API.
try {
- // Note: ownsToken() is currently a placeholder returning false.
- // Once Solana RPC integration is complete, this will check real ownership.
+ // Initialize the RPC client for the SolanaTokenAPI.
final tokenApi = SolanaTokenAPI();
- final ownershipResult = await tokenApi.ownsToken(
- receivingAddress,
- mintAddress,
- );
+ 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');
+ 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('DOES NOT own token - no token account found');
+ debugPrint(
+ 'Error checking ownership: ${ownershipResult.exception}',
+ );
}
} else {
- debugPrint(
- 'Error checking ownership: ${ownershipResult.exception}',
- );
+ debugPrint('Warning: RPC client not initialized for wallet');
}
} catch (e) {
debugPrint('Exception checking ownership: $e');
diff --git a/lib/pages/token_view/sol_token_view.dart b/lib/pages/token_view/sol_token_view.dart
index 94e2bd8..44ce450 100644
--- a/lib/pages/token_view/sol_token_view.dart
+++ b/lib/pages/token_view/sol_token_view.dart
@@ -24,7 +24,7 @@ import '../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../widgets/custom_buttons/blue_text_button.dart';
import '../../widgets/icon_widgets/sol_token_icon.dart';
import 'sub_widgets/token_summary_sol.dart';
-import 'sub_widgets/token_transaction_list_widget.dart';
+import 'sub_widgets/token_transaction_list_widget_sol.dart';
/// [eventBus] should only be set during testing.
class SolTokenView extends ConsumerStatefulWidget {
@@ -242,7 +242,7 @@ class _SolTokenViewState extends ConsumerState<SolTokenView> {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
- child: TokenTransactionsList(
+ child: SolanaTokenTransactionsList(
walletId: widget.walletId,
),
),
diff --git a/lib/pages/token_view/sub_widgets/token_transaction_list_widget_sol.dart b/lib/pages/token_view/sub_widgets/token_transaction_list_widget_sol.dart
new file mode 100644
index 0000000..d3640e8
--- /dev/null
+++ b/lib/pages/token_view/sub_widgets/token_transaction_list_widget_sol.dart
@@ -0,0 +1,179 @@
+/*
+ * 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.
+ *
+ */
+
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:isar_community/isar.dart';
+
+import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart';
+import '../../wallet_view/sub_widgets/no_transactions_found.dart';
+import '../../wallet_view/transaction_views/tx_v2/transaction_v2_list_item.dart';
+import '../../../providers/db/main_db_provider.dart';
+import '../../../providers/global/wallets_provider.dart';
+import '../../../themes/stack_colors.dart';
+import '../../../utilities/constants.dart';
+import '../../../wallets/isar/providers/solana/current_sol_token_wallet_provider.dart';
+import '../../../widgets/loading_indicator.dart';
+
+/// Solana-specific transaction list widget.
+///
+/// Displays transactions for a Solana token using the Solana token wallet provider.
+class SolanaTokenTransactionsList extends ConsumerStatefulWidget {
+ const SolanaTokenTransactionsList({
+ super.key,
+ required this.walletId,
+ });
+
+ final String walletId;
+
+ @override
+ ConsumerState<SolanaTokenTransactionsList> createState() =>
+ _SolanaTransactionsListState();
+}
+
+class _SolanaTransactionsListState extends ConsumerState<SolanaTokenTransactionsList> {
+ late final int minConfirms;
+
+ bool _hasLoaded = false;
+ List<TransactionV2> _transactions = [];
+
+ late final StreamSubscription<List<TransactionV2>> _subscription;
+ late final Query<TransactionV2> _query;
+
+ BorderRadius get _borderRadiusFirst {
+ return BorderRadius.only(
+ topLeft: Radius.circular(
+ Constants.size.circularBorderRadius,
+ ),
+ topRight: Radius.circular(
+ Constants.size.circularBorderRadius,
+ ),
+ );
+ }
+
+ BorderRadius get _borderRadiusLast {
+ return BorderRadius.only(
+ bottomLeft: Radius.circular(
+ Constants.size.circularBorderRadius,
+ ),
+ bottomRight: Radius.circular(
+ Constants.size.circularBorderRadius,
+ ),
+ );
+ }
+
+ @override
+ void initState() {
+ minConfirms = ref
+ .read(pWallets)
+ .getWallet(widget.walletId)
+ .cryptoCurrency
+ .minConfirms;
+
+ // Get transaction filter from Solana token wallet if available.
+ final solanaTokenWallet = ref.read(pCurrentSolanaTokenWallet);
+ FilterOperation? transactionFilter;
+
+ if (solanaTokenWallet != null) {
+ transactionFilter = solanaTokenWallet.transactionFilterOperation;
+ }
+
+ _query = ref.read(mainDBProvider).isar.transactionV2s.buildQuery<TransactionV2>(
+ whereClauses: [
+ IndexWhereClause.equalTo(
+ indexName: 'walletId',
+ value: [widget.walletId],
+ ),
+ ],
+ filter: transactionFilter,
+ sortBy: [
+ const SortProperty(
+ property: "timestamp",
+ sort: Sort.desc,
+ ),
+ ],
+ );
+
+ _subscription = _query.watch().listen((event) {
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (mounted) {
+ setState(() {
+ _transactions = event;
+ });
+ }
+ });
+ });
+ super.initState();
+ }
+
+ @override
+ void dispose() {
+ _subscription.cancel();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final wallet =
+ ref.watch(pWallets.select((value) => value.getWallet(widget.walletId)));
+
+ return FutureBuilder(
+ future: _query.findAll(),
+ builder: (fbContext, AsyncSnapshot<List<TransactionV2>> snapshot) {
+ if (snapshot.connectionState == ConnectionState.done &&
+ snapshot.hasData) {
+ if (!_hasLoaded) {
+ _hasLoaded = true;
+ _transactions = snapshot.data ?? [];
+ }
+
+ if (_transactions.isEmpty) {
+ return const NoTransActionsFound();
+ }
+
+ return CustomScrollView(
+ slivers: [
+ SliverList(
+ delegate: SliverChildBuilderDelegate(
+ (context, index) {
+ return TxListItem(
+ key: Key(
+ "solanaTokenTransactionV2ListItemKey_${_transactions[index].txid}",
+ ),
+ tx: _transactions[index],
+ coin: wallet.cryptoCurrency,
+ radius: index == 0
+ ? _borderRadiusFirst
+ : index == _transactions.length - 1
+ ? _borderRadiusLast
+ : null,
+ );
+ },
+ childCount: _transactions.length,
+ ),
+ ),
+ ],
+ );
+ }
+
+ return Center(
+ child: Container(
+ color: Theme.of(context).extension<StackColors>()!.background,
+ child: const LoadingIndicator(
+ width: 100,
+ height: 100,
+ ),
+ ),
+ );
+ },
+ );
+ }
+}
diff --git a/lib/wallets/wallet/impl/solana_wallet.dart b/lib/wallets/wallet/impl/solana_wallet.dart
index c88d995..db96018 100644
--- a/lib/wallets/wallet/impl/solana_wallet.dart
+++ b/lib/wallets/wallet/impl/solana_wallet.dart
@@ -35,6 +35,13 @@ class SolanaWallet extends Bip39Wallet<Solana> {
RpcClient? _rpcClient; // The Solana RpcClient.
+ /// Get the RPC client for this wallet.
+ ///
+ /// This is used by services like SolanaTokenAPI that need to make RPC calls.
+ RpcClient? getRpcClient() {
+ return _rpcClient;
+ }
+
Future<Ed25519HDKeyPair> _getKeyPair() async {
return Ed25519HDKeyPair.fromMnemonic(
await getMnemonic(),
Why this scored 19/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.