spark view only wallet electrumx cache clear option fix
What changed, and why it matters
This commit is a small UI fix for the Stack Wallet app. It changes when the 'Clear electrumx cache' button is shown for Spark (Firo) wallets so that it also appears for certain 'view-only' wallets. The change mostly reorganizes on-screen settings and updates a few warning messages. There is no direct evidence in the commit of a security vulnerability being fixed.
Treat as a routine UI/UX fix. No immediate security action is required. If reviewing for release, verify that showing the cache-clear option to Spark view-only wallets does not allow unauthorized deletion of data in shared/multi-user scenarios.
Security signals we found
UI visibility change for a cache-clear action
No cryptographic, authentication, or network-layer changes observed
No input validation, parsing, or serialization changes observed
No privilege escalation or data-exposure patterns in diff
Evidence from the diff
The patch refactors wallet_settings_view.dart to extract the settings body into a private _WalletSettingsViewBody widget and introduces a shouldShowClearSparkCache guard: wallet is SparkInterface && (!wallet.isViewOnly || (wallet.isViewOnly && wallet.viewOnlyType == .spark)). The same guard is applied in desktop_wallet_features.dart. It also removes an empty wallet_navigation_bar.dart file and makes minor mounted/context.mounted and string-formatting cleanups elsewhere. The functional change is exposing the cache-clear action to Spark view-only wallets of type .spark.
Changed components
lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dartlib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dartlib/pages/wallet_view/sub_widgets/wallet_navigation_bar.dartlib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dartlib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dartInspect captured patch +275 / −278
diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart
index 57808de..58ffaad 100644
--- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart
+++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart
@@ -352,6 +352,11 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> {
canBackup = false;
}
+ final shouldShowClearSparkCache =
+ wallet is SparkInterface &&
+ (!wallet.isViewOnly ||
+ (wallet.isViewOnly && wallet.viewOnlyType == .spark));
+
return Background(
child: Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
@@ -363,258 +368,251 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> {
),
title: Text("Settings", style: STextStyles.navBarTitle(context)),
),
- body: SafeArea(
- child: LayoutBuilder(
- builder: (builderContext, constraints) {
- return Padding(
- padding: const EdgeInsets.only(left: 12, top: 12, right: 12),
- child: SingleChildScrollView(
- child: ConstrainedBox(
- constraints: BoxConstraints(
- minHeight: constraints.maxHeight - 24,
- ),
- child: IntrinsicHeight(
- child: Padding(
- padding: const EdgeInsets.all(4),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- RoundedWhiteContainer(
- padding: const EdgeInsets.all(4),
- child: Column(
- children: [
- SettingsListButton(
- iconAssetName: Assets.svg.addressBook,
- iconSize: 16,
- title: "Address book",
- onPressed: () {
- Navigator.of(context).pushNamed(
- AddressBookView.routeName,
- arguments: coin,
- );
- },
- ),
- if (coin is FrostCurrency)
- const SizedBox(height: 8),
- if (coin is FrostCurrency)
- SettingsListButton(
- iconAssetName: Assets.svg.addressBook2,
- iconSize: 16,
- title: "FROST Multisig settings",
- onPressed: () {
- Navigator.of(context).pushNamed(
- FrostMSWalletOptionsView.routeName,
- arguments: walletId,
- );
- },
- ),
- const SizedBox(height: 8),
- SettingsListButton(
- iconAssetName: Assets.svg.node,
- iconSize: 16,
- title: "Network",
- onPressed: () {
- Navigator.of(context).pushNamed(
- WalletNetworkSettingsView.routeName,
- arguments: Tuple3(
- walletId,
- _currentSyncStatus,
- widget.initialNodeStatus,
- ),
- );
- },
- ),
- if (canBackup) const SizedBox(height: 8),
- if (canBackup)
- Consumer(
- builder: (_, ref, __) {
- return SettingsListButton(
- iconAssetName: Assets.svg.lock,
- iconSize: 16,
- title: "Wallet backup",
- onPressed: _walletBackupPressed,
- );
- },
- ),
- const SizedBox(height: 8),
- SettingsListButton(
- iconAssetName: Assets.svg.downloadFolder,
- title: "Wallet settings",
- iconSize: 16,
- onPressed: () {
- Navigator.of(context).pushNamed(
- WalletSettingsWalletSettingsView
- .routeName,
- arguments: walletId,
- );
- },
- ),
- const SizedBox(height: 8),
- SettingsListButton(
- iconAssetName: Assets.svg.arrowRotate,
- title: "Syncing preferences",
- onPressed: () {
- Navigator.of(context).pushNamed(
- SyncingPreferencesView.routeName,
- );
- },
- ),
- if (xPubEnabled) const SizedBox(height: 8),
- if (xPubEnabled)
- Consumer(
- builder: (_, ref, __) {
- return SettingsListButton(
- iconAssetName: Assets.svg.eye,
- title: "Wallet xPub",
- onPressed: _walletXPubPressed,
- );
- },
- ),
- if (sparkViewKeyEnabled)
- const SizedBox(height: 8),
- if (sparkViewKeyEnabled)
- Consumer(
- builder: (_, ref, __) {
- return SettingsListButton(
- iconAssetName: Assets.svg.eye,
- title: "Spark view key",
- onPressed: _walletSparkViewKeyPressed,
- );
- },
- ),
- if (coin is Firo) const SizedBox(height: 8),
- if (coin is Firo)
- Consumer(
- builder: (_, ref, __) {
- return SettingsListButton(
- iconAssetName: Assets.svg.eye,
- title: "Clear electrumx cache",
- onPressed: () async {
- String? result;
- await showDialog<void>(
- useSafeArea: false,
- barrierDismissible: true,
- context: context,
- builder: (_) => StackOkDialog(
- title:
- "Are you sure you want to clear "
- "${coin.prettyName} electrumx cache?",
- onOkPressed: (value) {
- result = value;
- },
- leftButton: SecondaryButton(
- label: "Cancel",
- onPressed: () {
- Navigator.of(context).pop();
- },
- ),
- ),
- );
-
- if (result == "OK" &&
- context.mounted) {
- await showLoading(
- whileFuture: Future.wait<void>([
- Future.delayed(
- const Duration(
- milliseconds: 1500,
- ),
- ),
- DB.instance
- .clearSharedTransactionCache(
- currency: coin,
- ),
- if (coin is Firo)
- FiroCacheCoordinator.clearSharedCache(
- coin.network,
- ),
- ]),
- context: context,
- message: "Clearing cache...",
- );
- }
- },
- );
- },
- ),
- if (coin is NanoCurrency)
- const SizedBox(height: 8),
- if (coin is NanoCurrency)
- Consumer(
- builder: (_, ref, __) {
- return SettingsListButton(
- iconAssetName: Assets.svg.eye,
- title: "Change representative",
- onPressed: () {
- Navigator.of(context).pushNamed(
- ChangeRepresentativeView
- .routeName,
- arguments: widget.walletId,
- );
- },
- );
- },
- ),
- // const SizedBox(
- // height: 8,
- // ),
- // SettingsListButton(
- // iconAssetName: Assets.svg.ellipsis,
- // title: "Debug Info",
- // onPressed: () {
- // Navigator.of(context)
- // .pushNamed(DebugView.routeName);
- // },
- // ),
- ],
- ),
+ body: _WalletSettingsViewBody(
+ children: [
+ SettingsListButton(
+ iconAssetName: Assets.svg.addressBook,
+ iconSize: 16,
+ title: "Address book",
+ onPressed: () {
+ Navigator.of(
+ context,
+ ).pushNamed(AddressBookView.routeName, arguments: coin);
+ },
+ ),
+ if (coin is FrostCurrency) const SizedBox(height: 8),
+ if (coin is FrostCurrency)
+ SettingsListButton(
+ iconAssetName: Assets.svg.addressBook2,
+ iconSize: 16,
+ title: "FROST Multisig settings",
+ onPressed: () {
+ Navigator.of(context).pushNamed(
+ FrostMSWalletOptionsView.routeName,
+ arguments: walletId,
+ );
+ },
+ ),
+ const SizedBox(height: 8),
+ SettingsListButton(
+ iconAssetName: Assets.svg.node,
+ iconSize: 16,
+ title: "Network",
+ onPressed: () {
+ Navigator.of(context).pushNamed(
+ WalletNetworkSettingsView.routeName,
+ arguments: Tuple3(
+ walletId,
+ _currentSyncStatus,
+ widget.initialNodeStatus,
+ ),
+ );
+ },
+ ),
+ if (canBackup) const SizedBox(height: 8),
+ if (canBackup)
+ Consumer(
+ builder: (_, ref, __) {
+ return SettingsListButton(
+ iconAssetName: Assets.svg.lock,
+ iconSize: 16,
+ title: "Wallet backup",
+ onPressed: _walletBackupPressed,
+ );
+ },
+ ),
+ const SizedBox(height: 8),
+ SettingsListButton(
+ iconAssetName: Assets.svg.downloadFolder,
+ title: "Wallet settings",
+ iconSize: 16,
+ onPressed: () {
+ Navigator.of(context).pushNamed(
+ WalletSettingsWalletSettingsView.routeName,
+ arguments: walletId,
+ );
+ },
+ ),
+ const SizedBox(height: 8),
+ SettingsListButton(
+ iconAssetName: Assets.svg.arrowRotate,
+ title: "Syncing preferences",
+ onPressed: () {
+ Navigator.of(
+ context,
+ ).pushNamed(SyncingPreferencesView.routeName);
+ },
+ ),
+ if (xPubEnabled) const SizedBox(height: 8),
+ if (xPubEnabled)
+ Consumer(
+ builder: (_, ref, __) {
+ return SettingsListButton(
+ iconAssetName: Assets.svg.eye,
+ title: "Wallet xPub",
+ onPressed: _walletXPubPressed,
+ );
+ },
+ ),
+ if (sparkViewKeyEnabled) const SizedBox(height: 8),
+ if (sparkViewKeyEnabled)
+ Consumer(
+ builder: (_, ref, __) {
+ return SettingsListButton(
+ iconAssetName: Assets.svg.eye,
+ title: "Spark view key",
+ onPressed: _walletSparkViewKeyPressed,
+ );
+ },
+ ),
+ if (shouldShowClearSparkCache) const SizedBox(height: 8),
+ if (shouldShowClearSparkCache)
+ Consumer(
+ builder: (_, ref, __) {
+ return SettingsListButton(
+ iconAssetName: Assets.svg.eye,
+ title: "Clear electrumx cache",
+ onPressed: () async {
+ String? result;
+ await showDialog<void>(
+ useSafeArea: false,
+ barrierDismissible: true,
+ context: context,
+ builder: (_) => StackOkDialog(
+ title:
+ "Are you sure you want to clear "
+ "${coin.prettyName} electrumx cache?",
+ onOkPressed: (value) {
+ result = value;
+ },
+ leftButton: SecondaryButton(
+ label: "Cancel",
+ onPressed: () {
+ Navigator.of(context).pop();
+ },
+ ),
+ ),
+ );
+
+ if (result == "OK" && context.mounted) {
+ await showLoading(
+ whileFuture: Future.wait<void>([
+ Future.delayed(const Duration(milliseconds: 1500)),
+ DB.instance.clearSharedTransactionCache(
+ currency: coin,
),
- const SizedBox(height: 12),
- const Spacer(),
- Consumer(
- builder: (_, ref, __) {
- return TextButton(
- onPressed: () {
- // TODO: [prio=med] needs more thought if this is still required
- // ref
- // .read(pWallets)
- // .getWallet(walletId)
- // .isActiveWallet = false;
- ref
- .read(
- transactionFilterProvider.state,
- )
- .state =
- null;
-
- Navigator.of(context).popUntil(
- ModalRoute.withName(HomeView.routeName),
- );
- },
- style: Theme.of(context)
- .extension<StackColors>()!
- .getSecondaryEnabledButtonStyle(context),
- child: Text(
- "Log out",
- style: STextStyles.button(context).copyWith(
- color: Theme.of(context)
- .extension<StackColors>()!
- .accentColorDark,
- ),
- ),
+ if (coin is Firo)
+ FiroCacheCoordinator.clearSharedCache(
+ coin.network,
+ ),
+ ]),
+ context: context,
+ message: "Clearing cache...",
+ );
+ }
+ },
+ );
+ },
+ ),
+ if (coin is NanoCurrency) const SizedBox(height: 8),
+ if (coin is NanoCurrency)
+ Consumer(
+ builder: (_, ref, __) {
+ return SettingsListButton(
+ iconAssetName: Assets.svg.eye,
+ title: "Change representative",
+ onPressed: () {
+ Navigator.of(context).pushNamed(
+ ChangeRepresentativeView.routeName,
+ arguments: widget.walletId,
+ );
+ },
+ );
+ },
+ ),
+ // const SizedBox(
+ // height: 8,
+ // ),
+ // SettingsListButton(
+ // iconAssetName: Assets.svg.ellipsis,
+ // title: "Debug Info",
+ // onPressed: () {
+ // Navigator.of(context)
+ // .pushNamed(DebugView.routeName);
+ // },
+ // ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class _WalletSettingsViewBody extends StatelessWidget {
+ const _WalletSettingsViewBody({super.key, required this.children});
+
+ final List<Widget> children;
+
+ @override
+ Widget build(BuildContext context) {
+ return SafeArea(
+ child: LayoutBuilder(
+ builder: (builderContext, constraints) {
+ return Padding(
+ padding: const EdgeInsets.only(left: 12, top: 12, right: 12),
+ child: SingleChildScrollView(
+ child: ConstrainedBox(
+ constraints: BoxConstraints(
+ minHeight: constraints.maxHeight - 24,
+ ),
+ child: IntrinsicHeight(
+ child: Padding(
+ padding: const EdgeInsets.all(4),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ RoundedWhiteContainer(
+ padding: const EdgeInsets.all(4),
+ child: Column(children: children),
+ ),
+
+ const SizedBox(height: 12),
+ const Spacer(),
+ Consumer(
+ builder: (_, ref, __) {
+ return TextButton(
+ onPressed: () {
+ ref
+ .read(transactionFilterProvider.state)
+ .state =
+ null;
+
+ Navigator.of(context).popUntil(
+ ModalRoute.withName(HomeView.routeName),
);
},
- ),
- ],
+ style: Theme.of(context)
+ .extension<StackColors>()!
+ .getSecondaryEnabledButtonStyle(context),
+ child: Text(
+ "Log out",
+ style: STextStyles.button(context).copyWith(
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.accentColorDark,
+ ),
+ ),
+ );
+ },
),
- ),
+ ],
),
),
),
- );
- },
- ),
- ),
+ ),
+ ),
+ );
+ },
),
);
}
@@ -683,7 +681,7 @@ class _EpiBoxInfoFormState extends ConsumerState<EpicBoxInfoForm> {
hostController.text,
int.parse(portController.text),
);
- if (mounted) {
+ if (context.mounted) {
await showFloatingFlushBar(
context: context,
message: "Epicbox info saved!",
@@ -692,11 +690,13 @@ class _EpiBoxInfoFormState extends ConsumerState<EpicBoxInfoForm> {
}
unawaited(wallet.refresh());
} catch (e) {
- await showFloatingFlushBar(
- context: context,
- message: "Failed to save epicbox info: $e",
- type: FlushBarType.warning,
- );
+ if (context.mounted) {
+ await showFloatingFlushBar(
+ context: context,
+ message: "Failed to save epicbox info: $e",
+ type: FlushBarType.warning,
+ );
+ }
}
},
child: Text(
@@ -778,7 +778,7 @@ class _MwcmqsInfoFormState extends ConsumerState<MwcMqsInfoForm> {
hostController.text,
int.parse(portController.text),
);
- if (mounted) {
+ if (context.mounted) {
await showFloatingFlushBar(
context: context,
message: "Mwcmqs info saved!",
@@ -787,11 +787,13 @@ class _MwcmqsInfoFormState extends ConsumerState<MwcMqsInfoForm> {
}
unawaited(wallet.refresh());
} catch (e) {
- await showFloatingFlushBar(
- context: context,
- message: "Failed to save mwcmqs info: $e",
- type: FlushBarType.warning,
- );
+ if (context.mounted) {
+ await showFloatingFlushBar(
+ context: context,
+ message: "Failed to save mwcmqs info: $e",
+ type: FlushBarType.warning,
+ );
+ }
}
},
child: Text(
diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart
index 0490795..a5464cd 100644
--- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart
+++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart
@@ -134,7 +134,9 @@ class _WalletSettingsWalletSettingsViewState
return StackDialog(
title: "Warning!",
message:
- "Reusing addresses reduces your privacy and security. Are you sure you want to reuse addresses by default?",
+ "Reusing addresses reduces your privacy and "
+ "security. Are you sure you want to reuse "
+ "addresses by default?",
leftButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
@@ -187,8 +189,9 @@ class _WalletSettingsWalletSettingsViewState
return StackDialog(
title: "Notice",
message:
- "Activating MWEB requires synchronizing on-chain MWEB related data. "
- "This currently requires about 800 MB of storage.",
+ "Activating MWEB requires synchronizing on-chain MWEB "
+ "related data. This currently requires about "
+ "800 MB of storage.",
leftButton: SecondaryButton(
onPressed: () {
Navigator.of(context).pop(false);
@@ -292,7 +295,6 @@ class _WalletSettingsWalletSettingsViewState
RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: RawMaterialButton(
- // splashColor: Theme.of(context).extension<StackColors>()!.highlight,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
@@ -362,7 +364,6 @@ class _WalletSettingsWalletSettingsViewState
RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: RawMaterialButton(
- // splashColor: Theme.of(context).extension<StackColors>()!.highlight,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
@@ -412,7 +413,6 @@ class _WalletSettingsWalletSettingsViewState
RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: RawMaterialButton(
- // splashColor: Theme.of(context).extension<StackColors>()!.highlight,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
@@ -628,7 +628,6 @@ class _WalletSettingsWalletSettingsViewState
RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: RawMaterialButton(
- // splashColor: Theme.of(context).extension<StackColors>()!.highlight,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
@@ -642,7 +641,8 @@ class _WalletSettingsWalletSettingsViewState
context: context,
builder: (_) => StackDialog(
title:
- "Do you want to delete ${ref.read(pWalletName(widget.walletId))}?",
+ "Do you want to delete "
+ "${ref.read(pWalletName(widget.walletId))}?",
leftButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
diff --git a/lib/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart b/lib/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart
deleted file mode 100644
index a022be5..0000000
--- a/lib/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart
+++ /dev/null
@@ -1,9 +0,0 @@
-/*
- * This file is part of Stack Wallet.
- *
- * Copyright (c) 2023 Cypher Stack
- * All Rights Reserved.
- * The code is distributed under GPLv3 license, see LICENSE file for details.
- * Generated by Cypher Stack on 2023-05-26
- *
- */
diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart
index 63366f2..e052df5 100644
--- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart
+++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart
@@ -580,7 +580,8 @@ class _DesktopWalletFeaturesState extends ConsumerState<DesktopWalletFeatures> {
final showMwebOption = wallet is MwebInterface && !wallet.isViewOnly;
final extraOptions = [
- if (wallet is SparkInterface && !isViewOnly)
+ if (wallet is SparkInterface &&
+ (!isViewOnly || (isViewOnly && wallet.viewOnlyType == .spark)))
(WalletFeature.clearSparkCache, Assets.svg.key, () => ()),
if (wallet is RbfInterface) (WalletFeature.rbf, Assets.svg.key, () => ()),
diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart
index 74be058..2a635e5 100644
--- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart
+++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart
@@ -134,7 +134,9 @@ class _MoreFeaturesDialogState extends ConsumerState<MoreFeaturesDialog> {
mainAxisSize: MainAxisSize.min,
children: [
Text(
- "Reusing addresses reduces your privacy and security. Are you sure you want to reuse addresses by default?",
+ "Reusing addresses reduces your privacy and "
+ "security. Are you sure you want to reuse "
+ "addresses by default?",
style: STextStyles.desktopTextSmall(context),
),
const SizedBox(height: 43),
@@ -238,8 +240,9 @@ class _MoreFeaturesDialogState extends ConsumerState<MoreFeaturesDialog> {
mainAxisSize: MainAxisSize.min,
children: [
Text(
- "Activating MWEB requires synchronizing on-chain MWEB related data. "
- "This currently requires about 800 MB of storage.",
+ "Activating MWEB requires synchronizing on-chain "
+ "MWEB related data. This currently requires about "
+ "800 MB of storage.",
style: STextStyles.desktopTextSmall(context),
),
const SizedBox(height: 43),
Why this scored 17/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.