diff --git a/cw_core/lib/utils/print_verbose.dart b/cw_core/lib/utils/print_verbose.dart index 42014d609d..68a2a4fd24 100644 --- a/cw_core/lib/utils/print_verbose.dart +++ b/cw_core/lib/utils/print_verbose.dart @@ -1,9 +1,20 @@ +import 'dart:io'; import 'dart:math'; import 'package:flutter/foundation.dart'; +String? printVLogFilePath; + void printV(dynamic content) { CustomTrace programInfo = CustomTrace(StackTrace.current); - print("${programInfo.fileName}#${programInfo.lineNumber}:${programInfo.columnNumber} ${programInfo.callerFunctionName}: $content"); + final logMsg = "${programInfo.fileName}#${programInfo.lineNumber}:${programInfo.columnNumber} ${programInfo.callerFunctionName}: $content"; + if (printVLogFilePath != null) { + try { + File(printVLogFilePath!).writeAsStringSync("$logMsg\n", mode: FileMode.append); + } catch (e) { + print("Unable to write to log file (printV): $e"); + } + } + print(logMsg); } // https://stackoverflow.com/a/59386101 diff --git a/cw_core/lib/wallet_info.dart b/cw_core/lib/wallet_info.dart index d62e61941b..0979b79172 100644 --- a/cw_core/lib/wallet_info.dart +++ b/cw_core/lib/wallet_info.dart @@ -1,9 +1,14 @@ import 'dart:async'; +import 'dart:io'; import 'package:cw_core/address_info.dart'; import 'package:cw_core/hive_type_ids.dart'; +import 'package:cw_core/root_dir.dart'; +import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; +import 'package:path/path.dart' as p; part 'wallet_info.g.dart'; @@ -81,7 +86,7 @@ class WalletInfo extends HiveObject { this.derivationInfo, this.hardwareWalletType, this.parentAddress, - this.hashedWalletIdentifier, + this._hashedWalletIdentifier, this.isNonSeedWallet, ) : _yatLastUsedAddressController = StreamController.broadcast(); @@ -195,7 +200,7 @@ class WalletInfo extends HiveObject { @HiveField(22) String? parentAddress; - + @HiveField(23) List? hiddenAddresses; @@ -203,7 +208,40 @@ class WalletInfo extends HiveObject { List? manualAddresses; @HiveField(25) - String? hashedWalletIdentifier; + String? _hashedWalletIdentifier; + + String? get hashedWalletIdentifier => _hashedWalletIdentifier; + + set hashedWalletIdentifier(String? value) { + final oldValue = _hashedWalletIdentifier; + _logHashedIdentifierChange(oldValue, value, StackTrace.current); + _hashedWalletIdentifier = value; + } + + Future _logHashedIdentifierChange( + String? oldValue, + String? newValue, + StackTrace trace, + ) async { + try { + final customTrace = CustomTrace(trace); + final timestamp = DateTime.now().toIso8601String(); + final location = + '${customTrace.fileName}#${customTrace.lineNumber}:${customTrace.columnNumber}'; + final caller = customTrace.callerFunctionName; + final logLine = + '[$timestamp] $location ($caller) WalletType: (${type.name}) Name: ($name) ParentAddress: ($parentAddress) ' + 'isNonSeedWallet: ($isNonSeedWallet) isRecovery: ($isRecovery) Address: ($address) \n' + 'hashedWalletIdentifier: "$oldValue" → "$newValue"\n\n ${trace.toString()}\n\n\n'; + + final dir = await getAppDir(); + final file = File(p.join(dir.path, 'hashed_identifier_changes.log')); + + await file.writeAsString(logLine, mode: FileMode.append, flush: true); + } catch (e) { + if (kDebugMode) print('Failed to log hash change: $e'); + } + } @HiveField(26, defaultValue: false) bool isNonSeedWallet; diff --git a/lib/core/wallet_loading_service.dart b/lib/core/wallet_loading_service.dart index 382b1d6c28..83e9c5a6af 100644 --- a/lib/core/wallet_loading_service.dart +++ b/lib/core/wallet_loading_service.dart @@ -85,6 +85,7 @@ class WalletLoadingService { // try opening another wallet that is not corrupted to give user access to the app final walletInfoSource = await CakeHive.openBox(WalletInfo.boxName); + printV("WalletInfoSource length (wallet loading service): ${walletInfoSource.length}"); WalletBase? wallet; for (var walletInfo in walletInfoSource.values) { try { diff --git a/lib/entities/hash_wallet_identifier.dart b/lib/entities/hash_wallet_identifier.dart index 8e593ec796..d56518778e 100644 --- a/lib/entities/hash_wallet_identifier.dart +++ b/lib/entities/hash_wallet_identifier.dart @@ -5,7 +5,7 @@ import 'package:cw_core/wallet_base.dart'; import 'package:hashlib/hashlib.dart'; String createHashedWalletIdentifier(WalletBase wallet) { - if (wallet.seed == null) return ''; + if (wallet.seed == null || wallet.seed!.isEmpty) return ''; final salt = secrets.walletGroupSalt; final combined = '$salt.${wallet.seed}'; diff --git a/lib/entities/load_current_wallet.dart b/lib/entities/load_current_wallet.dart index a421168d07..251fa88e2e 100644 --- a/lib/entities/load_current_wallet.dart +++ b/lib/entities/load_current_wallet.dart @@ -7,12 +7,8 @@ import 'package:cake_wallet/core/wallet_loading_service.dart'; Future loadCurrentWallet({String? password}) async { final appStore = getIt.get(); - final name = getIt - .get() - .getString(PreferencesKey.currentWalletName); - final typeRaw = - getIt.get().getInt(PreferencesKey.currentWalletType) ?? - 0; + final name = getIt.get().getString(PreferencesKey.currentWalletName); + final typeRaw = getIt.get().getInt(PreferencesKey.currentWalletType) ?? 0; if (name == null) { throw Exception('Incorrect current wallet name: $name'); @@ -20,9 +16,6 @@ Future loadCurrentWallet({String? password}) async { final type = deserializeFromInt(typeRaw); final walletLoadingService = getIt.get(); - final wallet = await walletLoadingService.load( - type, - name, - password: password); + final wallet = await walletLoadingService.load(type, name, password: password); await appStore.changeCurrentWallet(wallet); } diff --git a/lib/entities/wallet_manager.dart b/lib/entities/wallet_manager.dart index 09b7642897..582f0dc755 100644 --- a/lib/entities/wallet_manager.dart +++ b/lib/entities/wallet_manager.dart @@ -1,5 +1,3 @@ -import 'dart:math'; - import 'package:cake_wallet/entities/hash_wallet_identifier.dart'; import 'package:cake_wallet/entities/wallet_group.dart'; import 'package:cw_core/wallet_base.dart'; @@ -34,12 +32,8 @@ class WalletManager { return walletInfo.hashedWalletIdentifier!; } - // Fallback to old logic final address = walletInfo.parentAddress ?? walletInfo.address; - if (address.isEmpty) { - return Random().nextInt(100000).toString(); - } - return address; + return address.isNotEmpty ? address : walletInfo.id; } WalletGroup _getOrCreateGroup(String groupKey) { @@ -100,56 +94,41 @@ class WalletManager { _saveCustomGroupName(groupKey, name); } - // --------------------------------------------------------------------------- - // This performs a Group-Based Lazy Migration: - // If the user opens a wallet in an old group, - // we migrate ALL wallets that share its old group key to a new hash. - // --------------------------------------------------------------------------- - - /// When a user opens a wallet, check if it has a real hash. + /// When user opens wallet, check if it has a real hash. + /// /// If not, migrate the ENTIRE old group so they keep the same group name /// and end up with the same new hash (preserving grouping). Future ensureGroupHasHashedIdentifier(WalletBase openedWallet) async { - WalletInfo walletInfo = openedWallet.walletInfo; + final info = openedWallet.walletInfo; - // If the openedWallet already has an hash, then there is nothing to do - if (walletInfo.hashedWalletIdentifier != null && - walletInfo.hashedWalletIdentifier!.isNotEmpty) { - updateWalletGroups(); // Still skeptical of calling this here. Looking for a better spot. + if (info.hashedWalletIdentifier?.isNotEmpty ?? false) { + updateWalletGroups(); return; } - // Identify the old group key for this wallet - final oldGroupKey = _resolveGroupKey(walletInfo); // parentAddress fallback + final oldGroupKey = info.parentAddress?.isNotEmpty == true ? info.parentAddress! : null; + final walletsToMigrate = oldGroupKey != null + ? _walletInfoSource.values.where((w) => (w.parentAddress ?? w.address) == oldGroupKey).toList() + : [info]; - // Find all wallets that share this old group key (i.e the old group) - final oldGroupWallets = _walletInfoSource.values.where((w) { - final key = w.hashedWalletIdentifier != null && w.hashedWalletIdentifier!.isNotEmpty - ? w.hashedWalletIdentifier - : (w.parentAddress ?? w.address); - return key == oldGroupKey; - }).toList(); + if (oldGroupKey != null && walletsToMigrate.isEmpty) return; - if (oldGroupWallets.isEmpty) { - // This shouldn't happen, but just in case it does, we return. - return; - } + final newHash = createHashedWalletIdentifier(openedWallet); - // Next, we determine the new group hash for these wallets - // Since they share the same seed, we can assign that group hash - // to all the wallets to preserve grouping. - final newGroupHash = createHashedWalletIdentifier(openedWallet); - - // Migrate the old group name from oldGroupKey(i.e parentAddress) to newGroupHash - await _migrateGroupName(oldGroupKey, newGroupHash); + if (oldGroupKey != null) { + await _migrateGroupName(oldGroupKey, newHash); + } - // Then we assign this new hash to each wallet in that old group and save them - for (final wallet in oldGroupWallets) { - wallet.hashedWalletIdentifier = newGroupHash; - await wallet.save(); + // This throttle is here so we don't overwhelm the app when we have a lot of wallets we want to migrate. + const maxConcurrent = 3; + for (var i = 0; i < walletsToMigrate.length; i += maxConcurrent) { + final batch = walletsToMigrate.skip(i).take(maxConcurrent); + await Future.wait(batch.map((w) { + w.hashedWalletIdentifier = newHash; + return w.save(); + })); } - // Finally, we rebuild the groups so that these wallets are now in the new group updateWalletGroups(); } diff --git a/lib/main.dart b/lib/main.dart index 7e5f24b607..b4a8283fc4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -28,6 +28,7 @@ import 'package:cake_wallet/store/authentication_store.dart'; import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/exception_handler.dart'; +import 'package:cake_wallet/utils/feature_flag.dart'; import 'package:cake_wallet/view_model/link_view_model.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cw_core/address_info.dart'; @@ -72,6 +73,12 @@ Future runAppWithZone({Key? topLevelKey}) async { return true; }; + final date = DateTime.now().toIso8601String().replaceAll(':', '-'); + final dir = '${(await getAppDir()).path}/print_v'; + if (!Directory(dir).existsSync()) { + Directory(dir).createSync(recursive: true); + } + printVLogFilePath = FeatureFlag.hasDevOptions ? '$dir/$date.log' : null; await FlutterDaemon().unmarkBackgroundSync(); await initializeAppAtRoot(); @@ -193,6 +200,7 @@ Future initializeAppConfigs({bool loadWallet = true}) async { final trades = await CakeHive.openBox(Trade.boxName, encryptionKey: tradesBoxKey); final orders = await CakeHive.openBox(Order.boxName, encryptionKey: ordersBoxKey); final walletInfoSource = await CakeHive.openBox(WalletInfo.boxName); + printV("WalletInfoSource length (initializeAppConfigs): ${walletInfoSource.length}"); final templates = await CakeHive.openBox