copy list to ensure underlying is not modified
What changed, and why it matters
This commit fixes a UI bug in the wallet overview screen. Previously, the code was modifying a shared list of wallet data directly (for example, filtering out wallets for other coins). Because that list came from a shared app state provider, the changes could unexpectedly affect other parts of the app. The fix makes a local copy of the list before filtering so the original shared data stays unchanged. This is a correctness/defensive-coding fix, not a direct security vulnerability.
Treat as a routine bug-fix / defensive-coding improvement. No urgent security response is indicated. Review other provider consumers for similar direct mutation patterns.
Security signals we found
shared mutable state mutation prevented by defensive copy
UI state corruption risk reduced
no input validation, injection, or cryptographic changes present
Evidence from the diff
In lib/pages/wallets_view/wallets_overview.dart, updateWallets() now calls .toList() on ref.read(pAllWalletsInfo) before applying removeWhere((e) => e.coin != widget.coin). Without the copy, removeWhere mutated the list returned by the Riverpod provider, potentially corrupting global wallet state and causing inconsistent UI or data elsewhere. The rest of the diff is formatting/refactoring only. There is no evidence of an exploit path, privilege escalation, or data leakage; the risk is state corruption / UI inconsistency.
Changed components
lib/pages/wallets_view/wallets_overview.dartupdateWallets() methodpAllWalletsInfo Riverpod provider consumerInspect captured patch +106 / −111
diff --git a/lib/pages/wallets_view/wallets_overview.dart b/lib/pages/wallets_view/wallets_overview.dart
index bd1dfe5..d0cc2d1 100644
--- a/lib/pages/wallets_view/wallets_overview.dart
+++ b/lib/pages/wallets_view/wallets_overview.dart
@@ -73,14 +73,13 @@ class _EthWalletsOverviewState extends ConsumerState<WalletsOverview> {
List<WalletListItemData> _filter(String searchTerm) {
// clean out deleted wallets
- final existingWalletIds =
- ref
- .read(mainDBProvider)
- .isar
- .walletInfo
- .where()
- .walletIdProperty()
- .findAllSync();
+ final existingWalletIds = ref
+ .read(mainDBProvider)
+ .isar
+ .walletInfo
+ .where()
+ .walletIdProperty()
+ .findAllSync();
wallets.removeWhere((k, v) => !existingWalletIds.contains(k));
if (searchTerm.isEmpty) {
@@ -128,7 +127,7 @@ class _EthWalletsOverviewState extends ConsumerState<WalletsOverview> {
}
void updateWallets() {
- final walletsData = ref.read(pAllWalletsInfo);
+ final walletsData = ref.read(pAllWalletsInfo).toList();
walletsData.removeWhere((e) => e.coin != widget.coin);
@@ -205,45 +204,44 @@ class _EthWalletsOverviewState extends ConsumerState<WalletsOverview> {
Widget build(BuildContext context) {
return ConditionalParent(
condition: !isDesktop && !AppConfig.isSingleCoinApp,
- builder:
- (child) => Background(
- child: Scaffold(
- backgroundColor:
- Theme.of(context).extension<StackColors>()!.background,
- appBar: AppBar(
- leading: const AppBarBackButton(),
- title: Text(
- "${widget.coin.prettyName} (${widget.coin.ticker}) wallets",
- style: STextStyles.navBarTitle(context),
- ),
- actions: [
- AspectRatio(
- aspectRatio: 1,
- child: AppBarIconButton(
- icon: SvgPicture.asset(
- Assets.svg.plus,
- width: 18,
- height: 18,
- color:
- Theme.of(
- context,
- ).extension<StackColors>()!.topNavIconPrimary,
- ),
- onPressed: () {
- Navigator.of(context).pushNamed(
- CreateOrRestoreWalletView.routeName,
- arguments: CoinEntity(widget.coin),
- );
- },
- ),
+ builder: (child) => Background(
+ child: Scaffold(
+ backgroundColor: Theme.of(
+ context,
+ ).extension<StackColors>()!.background,
+ appBar: AppBar(
+ leading: const AppBarBackButton(),
+ title: Text(
+ "${widget.coin.prettyName} (${widget.coin.ticker}) wallets",
+ style: STextStyles.navBarTitle(context),
+ ),
+ actions: [
+ AspectRatio(
+ aspectRatio: 1,
+ child: AppBarIconButton(
+ icon: SvgPicture.asset(
+ Assets.svg.plus,
+ width: 18,
+ height: 18,
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.topNavIconPrimary,
),
- ],
- ),
- body: SafeArea(
- child: Padding(padding: const EdgeInsets.all(16), child: child),
+ onPressed: () {
+ Navigator.of(context).pushNamed(
+ CreateOrRestoreWalletView.routeName,
+ arguments: CoinEntity(widget.coin),
+ );
+ },
+ ),
),
- ),
+ ],
+ ),
+ body: SafeArea(
+ child: Padding(padding: const EdgeInsets.all(16), child: child),
),
+ ),
+ ),
child: Column(
children: [
ClipRRect(
@@ -260,55 +258,53 @@ class _EthWalletsOverviewState extends ConsumerState<WalletsOverview> {
_searchString = value;
});
},
- style:
- isDesktop
- ? STextStyles.desktopTextExtraSmall(context).copyWith(
- color:
- Theme.of(
- context,
- ).extension<StackColors>()!.textFieldActiveText,
- height: 1.8,
- )
- : STextStyles.field(context),
- decoration: standardInputDecoration(
- "Search...",
- searchFieldFocusNode,
- context,
- desktopMed: isDesktop,
- ).copyWith(
- prefixIcon: Padding(
- padding: EdgeInsets.symmetric(
- horizontal: isDesktop ? 12 : 10,
- vertical: isDesktop ? 18 : 16,
- ),
- child: SvgPicture.asset(
- Assets.svg.search,
- width: isDesktop ? 20 : 16,
- height: isDesktop ? 20 : 16,
- ),
- ),
- suffixIcon:
- _searchController.text.isNotEmpty
+ style: isDesktop
+ ? STextStyles.desktopTextExtraSmall(context).copyWith(
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.textFieldActiveText,
+ height: 1.8,
+ )
+ : STextStyles.field(context),
+ decoration:
+ standardInputDecoration(
+ "Search...",
+ searchFieldFocusNode,
+ context,
+ desktopMed: isDesktop,
+ ).copyWith(
+ prefixIcon: Padding(
+ padding: EdgeInsets.symmetric(
+ horizontal: isDesktop ? 12 : 10,
+ vertical: isDesktop ? 18 : 16,
+ ),
+ child: SvgPicture.asset(
+ Assets.svg.search,
+ width: isDesktop ? 20 : 16,
+ height: isDesktop ? 20 : 16,
+ ),
+ ),
+ suffixIcon: _searchController.text.isNotEmpty
? Padding(
- padding: const EdgeInsets.only(right: 0),
- child: UnconstrainedBox(
- child: Row(
- children: [
- TextFieldIconButton(
- child: const XIcon(),
- onTap: () async {
- setState(() {
- _searchController.text = "";
- _searchString = "";
- });
- },
- ),
- ],
+ padding: const EdgeInsets.only(right: 0),
+ child: UnconstrainedBox(
+ child: Row(
+ children: [
+ TextFieldIconButton(
+ child: const XIcon(),
+ onTap: () async {
+ setState(() {
+ _searchController.text = "";
+ _searchString = "";
+ });
+ },
+ ),
+ ],
+ ),
),
- ),
- )
+ )
: null,
- ),
+ ),
),
),
const SizedBox(height: 16),
@@ -340,34 +336,33 @@ class _EthWalletsOverviewState extends ConsumerState<WalletsOverview> {
return ConditionalParent(
key: Key(wallet.walletId),
condition: isDesktop,
- builder:
- (child) => RoundedWhiteContainer(
- padding: const EdgeInsets.symmetric(
- vertical: 14,
- horizontal: 20,
- ),
- borderColor:
- Theme.of(
- context,
- ).extension<StackColors>()!.backgroundAppBar,
- child: child,
- ),
+ builder: (child) => RoundedWhiteContainer(
+ padding: const EdgeInsets.symmetric(
+ vertical: 14,
+ horizontal: 20,
+ ),
+ borderColor: Theme.of(
+ context,
+ ).extension<StackColors>()!.backgroundAppBar,
+ child: child,
+ ),
child: SimpleWalletCard(
walletId: wallet.walletId,
popPrevious:
widget.overrideSimpleWalletCardPopPreviousValueWith ==
- null
- ? isDesktop
- : widget
- .overrideSimpleWalletCardPopPreviousValueWith!,
- desktopNavigatorState:
- isDesktop ? widget.navigatorState : null,
+ null
+ ? isDesktop
+ : widget
+ .overrideSimpleWalletCardPopPreviousValueWith!,
+ desktopNavigatorState: isDesktop
+ ? widget.navigatorState
+ : null,
),
);
}
},
- separatorBuilder:
- (_, __) => SizedBox(height: isDesktop ? 10 : 8),
+ separatorBuilder: (_, __) =>
+ SizedBox(height: isDesktop ? 10 : 8),
itemCount: data.length,
);
},
Why this scored 20/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.