Skip to content

Wallet Group Resetting Exploration #2240

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 21 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5c48598
fix(generic-fixes): Fixes to a couple of raised issues.
Blazebrain Apr 30, 2025
a4cf8a2
fix: Wallet grouping
Blazebrain May 5, 2025
7d9f940
feat(generic-fixes): Fix wallet grouping issue
Blazebrain May 7, 2025
e457481
fix(generic-fixes): Wallet Grouping Bug
Blazebrain May 8, 2025
a8f3dd7
feat: more logging for wallet groups
MrCyjaneK May 8, 2025
5f20923
fix(generic-fixes): Fixes to a couple of raised issues.
Blazebrain Apr 30, 2025
c94e003
fix: Wallet grouping
Blazebrain May 5, 2025
dc027d6
feat(generic-fixes): Fix wallet grouping issue
Blazebrain May 7, 2025
9c8d228
fix(generic-fixes): Wallet Grouping Bug
Blazebrain May 8, 2025
308a196
dev: more logging features to catch WalletInfo.hive getting corrupted
MrCyjaneK May 20, 2025
e1216f8
fix: added fallback for lsof
MrCyjaneK May 21, 2025
9f529ba
fix(generic-fixes): Fixes to a couple of raised issues.
Blazebrain Apr 30, 2025
733e03a
fix: Wallet grouping
Blazebrain May 5, 2025
7c9ccdc
feat(generic-fixes): Fix wallet grouping issue
Blazebrain May 7, 2025
c52344c
fix(generic-fixes): Wallet Grouping Bug
Blazebrain May 8, 2025
6a9bdd2
feat: more logging for wallet groups
MrCyjaneK May 8, 2025
8c0ed42
dev: more logging features to catch WalletInfo.hive getting corrupted
MrCyjaneK May 20, 2025
f590f94
fix: added fallback for lsof
MrCyjaneK May 21, 2025
2e78c4b
fix: don't fail with broken `su`
MrCyjaneK Jun 16, 2025
d47865d
[skip ci] print->printV
MrCyjaneK Jun 16, 2025
acfe13a
fix: make fetchLsof respect FeatureFlag.hasDevOptions
MrCyjaneK Jun 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions cw_core/lib/utils/print_verbose.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,27 @@
import 'dart:io';
import 'dart:math';
import 'package:flutter/foundation.dart';

void printV(dynamic content) {
String? printVLogFilePath;

void printV(dynamic content, {bool separateMultiline = false}) {
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");
}
}
if (separateMultiline) {
final lines = content.toString().split("\n");
for (final s in lines) {
print("${programInfo.fileName}#${programInfo.lineNumber}:${programInfo.columnNumber} ${programInfo.callerFunctionName}: $s");
}
} else {
print(logMsg);
}
}

// https://stackoverflow.com/a/59386101
Expand Down
44 changes: 41 additions & 3 deletions cw_core/lib/wallet_info.dart
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -81,7 +86,7 @@ class WalletInfo extends HiveObject {
this.derivationInfo,
this.hardwareWalletType,
this.parentAddress,
this.hashedWalletIdentifier,
this._hashedWalletIdentifier,
this.isNonSeedWallet,
) : _yatLastUsedAddressController = StreamController<String>.broadcast();

Expand Down Expand Up @@ -195,15 +200,48 @@ class WalletInfo extends HiveObject {

@HiveField(22)
String? parentAddress;

@HiveField(23)
List<String>? hiddenAddresses;

@HiveField(24)
List<String>? 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<void> _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) printV('Failed to log hash change: $e');
}
}

@HiveField(26, defaultValue: false)
bool isNonSeedWallet;
Expand Down
1 change: 1 addition & 0 deletions lib/core/backup_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ class $BackupService {

Future<void> verifyWallets() async {
final walletInfoSource = await reloadHiveWalletInfoBox();
printV("WalletInfoSource length (backup service): ${walletInfoSource.length}");
correctWallets =
walletInfoSource.values.where((info) => availableWalletTypes.contains(info.type)).toList();

Expand Down
1 change: 1 addition & 0 deletions lib/core/wallet_loading_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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>(WalletInfo.boxName);
printV("WalletInfoSource length (wallet loading service): ${walletInfoSource.length}");
WalletBase? wallet;
for (var walletInfo in walletInfoSource.values) {
try {
Expand Down
2 changes: 1 addition & 1 deletion lib/entities/hash_wallet_identifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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}';
Expand Down
13 changes: 3 additions & 10 deletions lib/entities/load_current_wallet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,15 @@ import 'package:cake_wallet/core/wallet_loading_service.dart';

Future<void> loadCurrentWallet({String? password}) async {
final appStore = getIt.get<AppStore>();
final name = getIt
.get<SharedPreferences>()
.getString(PreferencesKey.currentWalletName);
final typeRaw =
getIt.get<SharedPreferences>().getInt(PreferencesKey.currentWalletType) ??
0;
final name = getIt.get<SharedPreferences>().getString(PreferencesKey.currentWalletName);
final typeRaw = getIt.get<SharedPreferences>().getInt(PreferencesKey.currentWalletType) ?? 0;

if (name == null) {
throw Exception('Incorrect current wallet name: $name');
}

final type = deserializeFromInt(typeRaw);
final walletLoadingService = getIt.get<WalletLoadingService>();
final wallet = await walletLoadingService.load(
type,
name,
password: password);
final wallet = await walletLoadingService.load(type, name, password: password);
await appStore.changeCurrentWallet(wallet);
}
67 changes: 23 additions & 44 deletions lib/entities/wallet_manager.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<void> 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();
}

Expand Down
24 changes: 24 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:ui';
import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
Expand Down Expand Up @@ -28,6 +29,8 @@ import 'package:cake_wallet/store/authentication_store.dart';
import 'package:cake_wallet/themes/utils/theme_provider.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/dev/lsof_view_model.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';
Expand Down Expand Up @@ -74,6 +77,12 @@ Future<void> 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();

Expand Down Expand Up @@ -198,7 +207,16 @@ Future<void> initializeAppConfigs({bool loadWallet = true}) async {
encryptionKey: transactionDescriptionsBoxKey);
final trades = await CakeHive.openBox<Trade>(Trade.boxName, encryptionKey: tradesBoxKey);
final orders = await CakeHive.openBox<Order>(Order.boxName, encryptionKey: ordersBoxKey);

if (FeatureFlag.hasDevOptions) {
printV("lsof (before): ${await LsofViewModelBase.fetchLsof()}", separateMultiline: true);
}
final walletInfoSource = await CakeHive.openBox<WalletInfo>(WalletInfo.boxName);
printV("lsof ( after): ${await LsofViewModelBase.fetchLsof()}", separateMultiline: true);
printV("WalletInfoSource length (initializeAppConfigs): ${walletInfoSource.length}");
if (FeatureFlag.hasDevOptions) {
printV("lsof ( after): ${await LsofViewModelBase.fetchLsof()}", separateMultiline: true);
}
final templates = await CakeHive.openBox<Template>(Template.boxName);
final exchangeTemplates = await CakeHive.openBox<ExchangeTemplate>(ExchangeTemplate.boxName);
final anonpayInvoiceInfo = await CakeHive.openBox<AnonpayInvoiceInfo>(AnonpayInvoiceInfo.boxName);
Expand Down Expand Up @@ -428,6 +446,12 @@ Future<void> backgroundSync() async {
WidgetsFlutterBinding.ensureInitialized();
printV("- DartPluginRegistrant.ensureInitialized()");
DartPluginRegistrant.ensureInitialized();
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;
printV("- FlutterDaemon.markBackgroundSync()");
final val = await FlutterDaemon().markBackgroundSync();
if (val) {
Expand Down
12 changes: 12 additions & 0 deletions lib/router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,11 @@ import 'package:cake_wallet/src/screens/dashboard/pages/address_page.dart';
import 'package:cake_wallet/src/screens/dashboard/pages/nft_details_page.dart';
import 'package:cake_wallet/src/screens/dashboard/pages/transactions_page.dart';
import 'package:cake_wallet/src/screens/dashboard/sign_page.dart';
import 'package:cake_wallet/src/screens/dev/hash_change_logs_page.dart';
import 'package:cake_wallet/src/screens/dev/lsof.dart';
import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart';
import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart';
import 'package:cake_wallet/src/screens/dev/print_verbose_logs_page.dart';
import 'package:cake_wallet/src/screens/dev/secure_preferences_page.dart';
import 'package:cake_wallet/src/screens/dev/shared_preferences_page.dart';
import 'package:cake_wallet/src/screens/dev/background_sync_logs_page.dart';
Expand Down Expand Up @@ -919,6 +922,15 @@ Route<dynamic> createRoute(RouteSettings settings) {
return MaterialPageRoute<void>(
builder: (_) => getIt.get<DevSecurePreferencesPage>(),
);

case Routes.devHashChangeLogs:
return MaterialPageRoute<void>(builder: (_) => HashChangeLogsPage());

case Routes.devPrintVerbose:
return MaterialPageRoute<void>(builder: (_) => PrintVerboseLogsPage());

case Routes.devLsof:
return MaterialPageRoute<void>(builder: (_) => DevLsof());

default:
return MaterialPageRoute<void>(
Expand Down
5 changes: 4 additions & 1 deletion lib/routes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,10 @@ class Routes {
static const devSharedPreferences = '/dev/shared_preferences';
static const devSecurePreferences = '/dev/secure_preferences';
static const devBackgroundSyncLogs = '/dev/background_sync_logs';

static const devHashChangeLogs = '/dev/hash_change_logs';
static const devPrintVerbose = '/dev/print_verbose';
static const devLsof = '/dev/lsof';

static const signPage = '/sign_page';
static const connectDevices = '/device/connect';
static const urqrAnimatedPage = '/urqr/animated_page';
Expand Down
Loading
Loading