cw-1683-prepare-zano-removal (#3668)
What changed, and why it matters
This commit prepares Cake Wallet to remove support for Zano and Decred wallets. It adds a new database table to store encrypted seed phrases for wallets that are being deprecated, shows warning popups to users so they back up their seeds, and stops new Zano/Decred wallets from being created. It also fixes a minor cleanup bug where a cached Zano wallet might not be closed before deletion. There is no direct evidence in the commit of an exploitable security vulnerability; the changes are primarily user-facing deprecation and data-preservation work.
Treat as a routine feature/removal commit, not a security patch. Reviewers should verify that the new `DeprecatedWalletSeeds` table is properly encrypted before storage, that seeds are deleted when wallets are removed, and that the `_isClosed` guard and cached-wallet close logic prevent use-after-close issues. No immediate security response is indicated by the diff itself.
Security signals we found
New database table stores seed/passphrase for deprecated wallets
UI added to warn users to back up seeds before wallet type removal
Wallet type removal prevents future creation of Zano/Decred wallets
Cached Zano wallet now explicitly closed before file-system removal
Auto-save timer guard added to avoid operations after wallet close
Evidence from the diff
The patch bumps the SQLite database schema to version 15 and introduces a DeprecatedWalletSeeds table (walletInfoId, seed, passphrase) with a foreign key to WalletInfo. It adds UI strings and a WalletRemovalPopup that warns users when Zano or Decred wallets are present, and it marks those wallet types as deprecated in the dashboard view model. tool/configure.dart comments out Decred and Zano from the generated available wallet types, preventing creation of new wallets of those types. In cw_zano, it adds an _isClosed guard to stop starting an auto-save timer on an already-closed wallet, and adds closeCachedWallet to close a cached Zano wallet before removing its files. The commit title and message describe this as ‘prepare zano removal’ and ‘also deprecate decred’.
Changed components
cw_core SQLite database schema and migrationcw_zano wallet lifecycle and service removallib/entities/default_settings migration utilitieslib/new-ui dashboard and wallet removal popuplib/view_model/dashboard view modeltool/configure.dart wallet type generationInspect captured patch +192 / −9
### cw_core/lib/db/sqlite.dart
@@ -66,7 +66,7 @@ Future<void> _initDb({String? pathOverride}) async {
await db?.close();
db = await openDatabase(
dbFile.path,
- version: 14,
+ version: 15,
onUpgrade: (db, oldVersion, newVersion) async {
printV("migrating: $oldVersion, $newVersion");
if (oldVersion <= 1) {
@@ -193,6 +193,9 @@ CREATE TABLE IF NOT EXISTS BalanceCardStyleSettings (
await _migrateBitcoinCardStylesForAccounts(db);
}
+ if(oldVersion <= 14) {
+ await _createDeprecatedWalletSeedTable(db);
+ }
},
onCreate: (Database db, int version) async {
await db.execute('''
@@ -299,6 +302,8 @@ CREATE TABLE BalanceCardStyleSettings (
await _createTronTokenTable(db);
await _createImportedNFTTable(db);
await _createWalletInfoAccountTable(db);
+ await _createDeprecatedWalletSeedTable(db);
+
},
);
}
@@ -589,3 +594,15 @@ isDefault BOOLEAN DEFAULT FALSE
);
""");
}
+
+
+Future<void> _createDeprecatedWalletSeedTable(Database db) async {
+ await db.execute("""
+CREATE TABLE DeprecatedWalletSeeds (
+walletInfoId INTEGER PRIMARY KEY,
+seed TEXT NOT NULL,
+passphrase TEXT NOT NULL,
+FOREIGN KEY (walletInfoId) REFERENCES WalletInfo(walletInfoId)
+);
+""");
+}
### cw_core/lib/wallet_info.dart
@@ -793,7 +793,8 @@ class WalletInfo {
}
static Future<int> delete(WalletInfo walletInfo) async {
- return await db!.delete(tableName, where: 'id = ?', whereArgs: [walletInfo.id]);
+ final deleted = await db!.delete(tableName, where: 'id = ?', whereArgs: [walletInfo.id]);
+ return deleted;
}
static Future<List<WalletInfo>> selectList(String where, List<dynamic> whereArgs,
### cw_zano/lib/zano_wallet.dart
@@ -122,6 +122,7 @@ abstract class ZanoWalletBase
bool _isTransactionUpdating;
bool _hasSyncAfterStartup;
Timer? _autoSaveTimer;
+ bool _isClosed = false;
/// number of transactions in each request
static final int _txChunkSize = (pow(2, 32) - 1).toInt();
@@ -422,6 +423,7 @@ abstract class ZanoWalletBase
@override
Future<void> close({bool shouldCleanup = true}) async {
+ _isClosed = true;
closeWallet(null);
_updateSyncInfoTimer?.cancel();
_autoSaveTimer?.cancel();
@@ -534,6 +536,9 @@ abstract class ZanoWalletBase
await walletAddresses.init();
await walletAddresses.updateAddress(address);
await updateTransactions();
+ if (_isClosed) {
+ return;
+ }
_autoSaveTimer = Timer.periodic(Duration(seconds: _autoSaveIntervalSeconds), (_) async {
await save();
});
### cw_zano/lib/zano_wallet_api.dart
@@ -458,6 +458,13 @@ mixin ZanoWalletApi {
static Map<String, CreateWalletResult> openWalletCache = {};
+ static Future<void> closeCachedWallet(String path) async {
+ final cached = openWalletCache.remove(path);
+ if (cached != null) {
+ await _closeWallet(cached.walletId);
+ }
+ }
+
Future<TransferResult> transfer(
List<Destination> destinations, BigInt fee, String comment) async {
final params = TransferParams(
### cw_zano/lib/zano_wallet_service.dart
@@ -112,6 +112,7 @@ class ZanoWalletService extends WalletService<
@override
Future<void> remove(String wallet) async {
+ await ZanoWalletApi.closeCachedWallet(await pathForWallet(name: wallet, type: getType()));
final path = await pathForWalletDir(name: wallet, type: getType());
final file = Directory(path);
final isExist = file.existsSync();
### lib/entities/default_settings_migration.dart
@@ -2,7 +2,10 @@ import 'dart:convert';
import 'dart:io' show Directory, File, Platform;
import 'package:cake_wallet/bitcoin/bitcoin.dart';
+import "package:cake_wallet/core/key_service.dart";
import 'package:cake_wallet/core/secure_storage.dart';
+import "package:cake_wallet/decred/decred.dart";
+import "package:cake_wallet/di.dart";
import 'package:cake_wallet/entities/balance_display_mode.dart';
import 'package:cake_wallet/entities/contact.dart';
import 'package:cake_wallet/entities/exchange_api_mode.dart';
@@ -14,6 +17,9 @@ import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/entities/secret_store_key.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/new-ui/model/charts/charts_asset.dart';
+import "package:cake_wallet/store/settings_store.dart";
+import "package:cake_wallet/zano/zano.dart";
+import "package:cw_core/cake_hive.dart";
import 'package:cake_wallet/wownero/wownero.dart';
import 'package:collection/collection.dart';
import 'package:cw_core/crypto_currency.dart';
@@ -23,8 +29,10 @@ import 'package:cw_core/node_list.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/root_dir.dart';
import 'package:cw_core/spl_token.dart';
+import "package:cw_core/unspent_coins_info.dart";
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_info.dart';
+import "package:cw_core/wallet_service.dart";
import 'package:cw_core/wallet_type.dart';
import 'package:encrypt/encrypt.dart' as encrypt;
import 'package:hive/hive.dart';
@@ -1377,4 +1385,4 @@ Future<void> _addTbbTokenToExistingSolanaWallets() async {
} catch (e) {
printV("Error in TBB migration: $e");
}
-}
+}
\ No newline at end of file
### lib/entities/preferences_key.dart
@@ -1,3 +1,5 @@
+import "package:cw_core/wallet_type.dart";
+
class PreferencesKey {
static const currentWalletType = 'current_wallet_type';
static const currentWalletName = 'current_wallet_name';
@@ -147,4 +149,6 @@ class PreferencesKey {
static const balanceHideCounter = "balance_hide_counter";
static const zcashMigrationModalViewed = "zcash_migration_modal_viewed";
static const showCiBuildOverlay = "show_ci_build_overlay";
+ static String deprecationPopupViewed(WalletType type) =>
+ "deprecation_popup_viewed_${walletTypeToString(type)}";
}
### lib/new-ui/new_dashboard.dart
@@ -7,6 +7,7 @@ import "package:cake_wallet/new-ui/page_open_listener.dart";
import 'package:cake_wallet/new-ui/pages/charts_page.dart';
import 'package:cake_wallet/new-ui/pages/home_page.dart';
import 'package:cake_wallet/new-ui/widgets/changelog_modal.dart';
+import "package:cake_wallet/new-ui/widgets/wallet_removal_popup.dart";
import 'package:cake_wallet/src/screens/contact/contact_list_page.dart';
import 'package:cake_wallet/src/screens/dashboard/pages/cake_features_page.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/new_main_navbar_widget.dart';
@@ -55,6 +56,7 @@ class _NewDashboardState extends State<NewDashboard> {
setState(() {
_selectedPage = 0;
});
+ _showWalletRemovalPopup(context);
});
Future.delayed(Duration(milliseconds: 300)).then((_) {
@@ -63,6 +65,7 @@ class _NewDashboardState extends State<NewDashboard> {
});
});
_showVulnerableSeedsPopup(context);
+ _showWalletRemovalPopup(context);
}
@override
@@ -182,4 +185,14 @@ class _NewDashboardState extends State<NewDashboard> {
);
}
}
+
+ Future<void> _showWalletRemovalPopup(BuildContext context) async {
+ if(await widget.dashboardViewModel.shouldShowRemovalPopup()) {
+ await Future.delayed(const Duration(seconds: 1));
+ final toBeRemoved = await widget.dashboardViewModel.walletsToBeRemoved;
+ if(context.mounted) {
+ await showPopUp<void>(context: context, builder: (context)=>WalletRemovalPopup(affectedWallets: toBeRemoved,));
+ }
+ }
+ }
}
### lib/new-ui/widgets/wallet_removal_popup.dart
@@ -0,0 +1,102 @@
+import "package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart";
+import "package:cake_wallet/generated/i18n.dart";
+import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
+import "package:cake_wallet/src/widgets/cake_image_widget.dart";
+import "package:cake_wallet/src/widgets/new_list_row/new_list_section.dart";
+import "package:cake_wallet/themes/core/theme_extension.dart";
+import "package:cw_core/currency_for_wallet_type.dart";
+import "package:cw_core/wallet_info.dart";
+import "package:cw_core/wallet_type.dart";
+import "package:flutter/material.dart";
+
+class WalletRemovalPopup extends StatelessWidget {
+ const WalletRemovalPopup({required this.affectedWallets, super.key});
+
+ final List<WalletInfo> affectedWallets;
+
+ @override
+ Widget build(BuildContext context) {
+ final types = affectedWallets.map((item) => item.type).toSet().toList();
+ final iconPaths = types.map((item) => walletTypeToCryptoCurrency(item).iconPath ?? "").toList();
+ final typeNames = types.map(walletTypeToDisplayName).toList();
+
+ return Center(
+ child: Material(
+ color: Colors.transparent,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 18),
+ child: Container(
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.surface,
+ borderRadius: BorderRadius.circular(18),
+ ),
+ child: SingleChildScrollView(
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ spacing: 18,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ spacing: 8,
+ children: iconPaths
+ .map(
+ (item) => CakeImageWidget(
+ imageUrl: item,
+ width: 64,
+ height: 64,
+ ),
+ )
+ .toList(),
+ ),
+ Text(
+ textAlign: TextAlign.center,
+ S.of(context).support_will_soon_be_removed(typeNames.join(", ")),
+ style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
+ ),
+ Text(
+ textAlign: TextAlign.center,
+ types.map(deprecationReasonForType).whereType<String>().join("\n\n"),
+ ),
+ Text(
+ textAlign: TextAlign.center,
+ S.of(context).make_sure_you_migrated(typeNames.join("/")),
+ style: TextStyle(
+ fontWeight: FontWeight.w500,
+ color: context.currentTheme.customColors.warningOutlineColor,
+ ),
+ ),
+ NewListSections(
+ sections: {
+ "": affectedWallets
+ .map(
+ (item) => ListItemRegularRow(
+ iconPath: walletTypeToCryptoCurrency(item.type).iconPath ?? "",
+ keyValue: item.name,
+ label: item.name,
+ showArrow: false,
+ ),
+ )
+ .toList(),
+ },
+ ),
+ NewPrimaryButton(
+ onPressed: Navigator.of(context).pop,
+ text: S.of(context).close,
+ color: Theme.of(context).colorScheme.primary,
+ textColor: Theme.of(context).colorScheme.onPrimary,
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ String? deprecationReasonForType(WalletType type) =>
+ switch (type) { WalletType.zano => S.current.zano_removal_reason, _ => null };
+}
### lib/view_model/dashboard/dashboard_view_model.dart
@@ -1697,4 +1697,25 @@ abstract class DashboardViewModelBase with Store {
Future<void> refreshDashboard() async {
reconnect();
}
+
+
+ static const walletTypesToBeRemoved = [
+ WalletType.zano,
+ WalletType.decred,
+ ];
+
+ Future<bool> shouldShowRemovalPopup() async {
+ final show = (await WalletInfo.getAll()).any((item) =>
+ walletTypesToBeRemoved.contains(item.type) &&
+ !(sharedPreferences.getBool(PreferencesKey.deprecationPopupViewed(item.type)) ?? false),);
+ for (final type in walletTypesToBeRemoved) {
+ await sharedPreferences.setBool(PreferencesKey.deprecationPopupViewed(type), true);
+ }
+ // if user is actively using a wallet type that's about to be removed, keep nagging them
+ return show || walletTypesToBeRemoved.contains(wallet.type);
+ }
+
+ Future<List<WalletInfo>> get walletsToBeRemoved async => (await WalletInfo.getAll())
+ .where((item) => walletTypesToBeRemoved.contains(item.type))
+ .toList();
}
### res/values/strings_en.arb
@@ -668,6 +668,7 @@
"low_fee_alert": "You currently are using a low network fee priority. This could cause long waits, different rates, or canceled trades. We recommend setting a higher fee for a better experience.",
"low_kyc": "Low KYC",
"made_easy": "made easy",
+ "make_sure_you_migrated": "Please be sure you've backed up your seed phrase from Recovery & Keys in the meantime, and consider migrating to another ${walletTypes} wallet.",
"manage_nodes": "Manage nodes",
"manage_pow_nodes": "Manage PoW nodes",
"manage_providers": "Manage providers",
@@ -1180,6 +1181,7 @@
"show_recovery_phrase": "Show me my Recovery Phrase",
"show_recovery_phrase_and_keys": "Show my Recovery Phrase & Keys",
"show_seed": "Show seed",
+ "show_seed_confirmation": "View seed of this wallet?",
"show_zcash_card": "Show zcash missing funds card",
"sign_all": "Sign All",
"sign_message": "Sign Message",
@@ -1239,6 +1241,7 @@
"support_title_guides": "Cake Wallet docs",
"support_title_live_chat": "Live support",
"support_title_other_links": "Other support links",
+ "support_will_soon_be_removed": "Support for ${walletTypes} will soon be removed",
"supported": "Supported",
"swap": "Swap",
"swap_from_network": "Swap from ${network}",
@@ -1578,6 +1581,7 @@
"youCanGoBackToYourDapp": "You can go back to your dApp now",
"your": "Your",
"yy": "YY",
+ "zano_removal_reason": "Zano has undergone multiple hard-forks to resolve a critical network issue, including a rollback of the chain. Due to serious issues with the forks and the maintenance burden it brings to us, we will be removing Zano from Cake Wallet in a future release.",
"zcash_card_description": "If you don't see your full balance Cake Wallet can attempt to sweep funds from internal change addresses used by some wallets.",
"zcash_card_dismiss": "Dismiss",
"zcash_card_enable_later": "You can always enable this card later in settings",
### tool/configure.dart
@@ -2077,13 +2077,13 @@ Future<void> generateWalletTypes({
outputContent += '\tWalletType.nano,\n';
}
- if (hasDecred) {
- outputContent += '\tWalletType.decred,\n';
- }
+ // if (hasDecred) {
+ // outputContent += '\tWalletType.decred,\n';
+ // }
- if (hasZano) {
- outputContent += '\tWalletType.zano,\n';
- }
+ // if (hasZano) {
+ // outputContent += '\tWalletType.zano,\n';
+ // }
if (hasBanano) {
outputContent += '\tWalletType.banano,\n';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.