fix(spl): handle missing Ethereum token wallet in shared components
What changed, and why it matters
This commit is a defensive bug-fix in the desktop wallet UI. It prevents the app from crashing when a Solana token wallet is selected, because the shared UI components were originally written only for Ethereum-style token wallets. The changes replace hard assumptions (that a token wallet always has an Ethereum contract object) with null checks, try/catch guards, and placeholder labels. There is no evidence of an exploitable vulnerability; it is a robustness improvement.
No immediate security action required. Treat as a normal stability fix. Continue the Solana token integration work tracked by the TODOs so that token symbols, fee estimation, and price lookups are properly implemented rather than silently degraded.
Security signals we found
Null-dereference / type-cast crash prevented in token wallet UI
Defensive try/catch added around provider reads that previously assumed Ethereum token wallet
UI placeholders used when token contract metadata is unavailable
TODO comments indicate incomplete Solana token support
Evidence from the diff
The patch modifies three desktop wallet sub-widgets. desktop_receive.dart now uses a generic ‘token’ label instead of dereferencing tokenContract.symbol. desktop_send_fee_form.dart wraps token fee estimation in try/catch and logs when the token wallet is unavailable. desktop_wallet_summary.dart imports EthContract, makes tokenContract dynamic/nullable, guards price and balance lookups behind tokenContract != null, and casts only when non-null. The commit message frames this as ‘handle missing Ethereum token wallet in shared components’ and adds TODOs for proper Solana support later.
Changed components
lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dartlib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send_fee_form.dartlib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dartInspect captured patch +38 / −23
diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart
index 607bf3f..76a86d2 100644
--- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart
+++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart
@@ -621,7 +621,9 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
Row(
children: [
Text(
- "Your ${widget.contractAddress == null ? coin.ticker : ref.watch(pCurrentTokenWallet.select((value) => value!.tokenContract.symbol))} address",
+ // "Your ${widget.contractAddress == null ? coin.ticker : ref.watch(pCurrentTokenWallet.select((value) => value!.tokenContract.symbol))} address",
+ // TODO [prio=high]: Make the above work for Sol tokens instead of the placeholder below.
+ "Your ${widget.contractAddress == null ? coin.ticker : "token"} address",
style: STextStyles.itemSubtitle(context),
),
const Spacer(),
diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send_fee_form.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send_fee_form.dart
index e0dfd7f..9779f40 100644
--- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send_fee_form.dart
+++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send_fee_form.dart
@@ -211,15 +211,21 @@ class _DesktopSendFeeFormState extends ConsumerState<DesktopSendFeeForm> {
.estimateFeeFor(amount, feeRate);
}
} else {
- final tokenWallet = ref.read(
- pCurrentTokenWallet,
- )!;
- final fee = await tokenWallet
- .estimateFeeFor(amount, feeRate);
- ref
- .read(tokenFeeSessionCacheProvider)
- .average[amount] =
- fee;
+ // TODO: Implement fee estimation for Solana tokens.
+ try {
+ final tokenWallet = ref.read(
+ pCurrentTokenWallet,
+ )!;
+ final fee = await tokenWallet
+ .estimateFeeFor(amount, feeRate);
+ ref
+ .read(tokenFeeSessionCacheProvider)
+ .average[amount] =
+ fee;
+ } catch (_) {
+ // Token wallet not available (Solana).
+ debugPrint("Token fee estimation not available");
+ }
}
}
return ref
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 c6ad269..ce9134e 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
@@ -12,6 +12,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../models/balance.dart';
+import '../../../../models/isar/models/ethereum/eth_contract.dart';
import '../../../../pages/wallet_view/sub_widgets/wallet_refresh_button.dart';
import '../../../../providers/providers.dart';
import '../../../../providers/wallet/public_private_balance_state_provider.dart';
@@ -77,18 +78,24 @@ class _WDesktopWalletSummaryState extends ConsumerState<DesktopWalletSummary> {
prefsChangeNotifierProvider.select((value) => value.currency),
);
- final tokenContract =
- widget.isToken
- ? ref.watch(
- pCurrentTokenWallet.select((value) => value!.tokenContract),
- )
- : null;
+ // For Ethereum tokens, get the token contract; for Solana tokens, show placeholder.
+ dynamic tokenContract;
+ if (widget.isToken) {
+ try {
+ tokenContract = ref.watch(
+ pCurrentTokenWallet.select((value) => value!.tokenContract),
+ );
+ } catch (_) {
+ // Solana token or token wallet not yet loaded.
+ tokenContract = null;
+ }
+ }
final price =
- widget.isToken
+ widget.isToken && tokenContract != null
? ref.watch(
priceAnd24hChangeNotifierProvider.select(
- (value) => value.getTokenPrice(tokenContract!.address),
+ (value) => value.getTokenPrice((tokenContract as dynamic).address as String),
),
)
: ref.watch(
@@ -116,11 +123,11 @@ class _WDesktopWalletSummaryState extends ConsumerState<DesktopWalletSummary> {
}
} else {
final Balance balance =
- widget.isToken
+ widget.isToken && tokenContract != null
? ref.watch(
pTokenBalance((
walletId: walletId,
- contractAddress: tokenContract!.address,
+ contractAddress: (tokenContract as dynamic).address as String,
)),
)
: ref.watch(pWalletBalance(walletId));
@@ -141,7 +148,7 @@ class _WDesktopWalletSummaryState extends ConsumerState<DesktopWalletSummary> {
child: SelectableText(
ref
.watch(pAmountFormatter(coin))
- .format(balanceToShow, ethContract: tokenContract),
+ .format(balanceToShow, ethContract: tokenContract != null ? tokenContract as EthContract? : null),
style: STextStyles.desktopH3(context),
),
),
@@ -174,8 +181,8 @@ class _WDesktopWalletSummaryState extends ConsumerState<DesktopWalletSummary> {
walletId: walletId,
initialSyncStatus: widget.initialSyncStatus,
tokenContractAddress:
- widget.isToken
- ? ref.watch(pCurrentTokenWallet)!.tokenContract.address
+ widget.isToken && tokenContract != null
+ ? (tokenContract as EthContract).address
: null,
),
Why this scored 22/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.