mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-24 06:48:38 +02:00
CW-843: Enhance Wallet Groups Implementation (#2045)
* feat: Enhance Wallet Groups Implementation by using hashedIdentifiers instead of parentAddresses * fix: Call updateWalletGroups even if group has an hash identifier * feat: Add secrets to workflow * feat: Enhance Wallet Groups Implementation by using hashedIdentifiers instead of parentAddresses * Handle wallet grouping edgecase where wallet is restored via non seed medium * fix: Valid wallet/wallet groups not showing up when choosing wallet/groups for creating new wallets
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:cake_wallet/.secrets.g.dart' as secrets;
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:hashlib/hashlib.dart';
|
||||
|
||||
String createHashedWalletIdentifier(WalletBase wallet) {
|
||||
if (wallet.seed == null) return '';
|
||||
|
||||
final salt = secrets.walletGroupSalt;
|
||||
final combined = '$salt.${wallet.seed}';
|
||||
|
||||
// Convert to UTF-8 bytes.
|
||||
final bytes = utf8.encode(combined);
|
||||
|
||||
// Perform SHA-256 hash.
|
||||
final digest = sha256.convert(bytes);
|
||||
|
||||
// Return the hex string representation of the hash.
|
||||
return digest.toString();
|
||||
}
|
||||
@@ -10,7 +10,7 @@ class WalletEditPageArguments {
|
||||
this.isWalletGroup = false,
|
||||
this.walletListViewModel,
|
||||
this.groupName = '',
|
||||
this.parentAddress = '',
|
||||
this.walletGroupKey = '',
|
||||
this.walletEditViewModel,
|
||||
this.walletNewVM,
|
||||
this.authService,
|
||||
@@ -19,7 +19,7 @@ class WalletEditPageArguments {
|
||||
final WalletListItem editingWallet;
|
||||
final bool isWalletGroup;
|
||||
final String groupName;
|
||||
final String parentAddress;
|
||||
final String walletGroupKey;
|
||||
final WalletListViewModel? walletListViewModel;
|
||||
|
||||
final WalletEditViewModel? walletEditViewModel;
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
|
||||
class WalletGroup {
|
||||
WalletGroup(this.parentAddress) : wallets = [];
|
||||
WalletGroup(this.groupKey) : wallets = [];
|
||||
|
||||
/// Main identifier for each group, compulsory.
|
||||
final String parentAddress;
|
||||
/// Primary identifier for the group. Previously was `parentAddress`.
|
||||
/// Now we store either the wallet's hash OR fallback to parentAddress/address.
|
||||
final String groupKey;
|
||||
|
||||
/// Child wallets that share the same parent address within this group
|
||||
List<WalletInfo> wallets;
|
||||
/// Child wallets that share the same group key
|
||||
final List<WalletInfo> wallets;
|
||||
|
||||
/// Custom name for the group, editable for multi-child wallet groups
|
||||
String? groupName;
|
||||
|
||||
@@ -1,70 +1,61 @@
|
||||
import 'package:cake_wallet/entities/hash_wallet_identifier.dart';
|
||||
import 'package:cake_wallet/entities/wallet_group.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class WalletManager {
|
||||
WalletManager(
|
||||
this._walletInfoSource,
|
||||
this._sharedPreferences,
|
||||
);
|
||||
WalletManager(this._walletInfoSource, this._sharedPreferences);
|
||||
|
||||
final Box<WalletInfo> _walletInfoSource;
|
||||
final SharedPreferences _sharedPreferences;
|
||||
|
||||
final List<WalletGroup> walletGroups = [];
|
||||
|
||||
/// Categorize wallets into groups based on their parentAddress.
|
||||
///
|
||||
/// Update the lead wallet for each group and clean up empty groups
|
||||
/// i.e remove group if there's no lead wallet (i.e, no wallets left)
|
||||
void updateWalletGroups() {
|
||||
walletGroups.clear();
|
||||
|
||||
for (var walletInfo in _walletInfoSource.values) {
|
||||
final group = _getOrCreateGroup(_resolveParentAddress(walletInfo));
|
||||
for (final walletInfo in _walletInfoSource.values) {
|
||||
final groupKey = _resolveGroupKey(walletInfo);
|
||||
final group = _getOrCreateGroup(groupKey);
|
||||
group.wallets.add(walletInfo);
|
||||
}
|
||||
|
||||
walletGroups.removeWhere((group) => group.wallets.isEmpty);
|
||||
|
||||
walletGroups.removeWhere((g) => g.wallets.isEmpty);
|
||||
_loadCustomGroupNames();
|
||||
}
|
||||
|
||||
/// Function to determine the correct parentAddress for a wallet.
|
||||
///
|
||||
/// If it's a parent wallet (parentAddress is null),
|
||||
/// use its own address as parentAddress.
|
||||
String _resolveParentAddress(WalletInfo walletInfo) {
|
||||
String _resolveGroupKey(WalletInfo walletInfo) {
|
||||
if (walletInfo.hashedWalletIdentifier != null &&
|
||||
walletInfo.hashedWalletIdentifier!.isNotEmpty) {
|
||||
return walletInfo.hashedWalletIdentifier!;
|
||||
}
|
||||
|
||||
// Fallback to old logic
|
||||
return walletInfo.parentAddress ?? walletInfo.address;
|
||||
}
|
||||
|
||||
/// Check if a group with the parentAddress already exists,
|
||||
/// If no group exists, create a new one.
|
||||
///
|
||||
WalletGroup _getOrCreateGroup(String parentAddress) {
|
||||
WalletGroup _getOrCreateGroup(String groupKey) {
|
||||
return walletGroups.firstWhere(
|
||||
(group) => group.parentAddress == parentAddress,
|
||||
(g) => g.groupKey == groupKey,
|
||||
orElse: () {
|
||||
final newGroup = WalletGroup(parentAddress);
|
||||
final newGroup = WalletGroup(groupKey);
|
||||
walletGroups.add(newGroup);
|
||||
return newGroup;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Add a new wallet and update lead wallet after adding.
|
||||
void addWallet(WalletInfo walletInfo) {
|
||||
final group = _getOrCreateGroup(_resolveParentAddress(walletInfo));
|
||||
final groupKey = _resolveGroupKey(walletInfo);
|
||||
final group = _getOrCreateGroup(groupKey);
|
||||
group.wallets.add(walletInfo);
|
||||
}
|
||||
|
||||
/// Removes a wallet from a group i.e when it's deleted.
|
||||
///
|
||||
/// Update lead wallet after removing,
|
||||
/// Remove the group if it's empty (i.e., no lead wallet).
|
||||
void removeWallet(WalletInfo walletInfo) {
|
||||
final group = _getOrCreateGroup(_resolveParentAddress(walletInfo));
|
||||
final groupKey = _resolveGroupKey(walletInfo);
|
||||
final group = _getOrCreateGroup(groupKey);
|
||||
group.wallets.remove(walletInfo);
|
||||
|
||||
if (group.wallets.isEmpty) {
|
||||
@@ -72,39 +63,99 @@ class WalletManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns all the child wallets within a group.
|
||||
///
|
||||
/// If the group is not found, returns an empty group with no wallets.
|
||||
List<WalletInfo> getWalletsInGroup(String parentAddress) {
|
||||
List<WalletInfo> getWalletsInGroup(String groupKey) {
|
||||
return walletGroups
|
||||
.firstWhere(
|
||||
(group) => group.parentAddress == parentAddress,
|
||||
orElse: () => WalletGroup(parentAddress),
|
||||
(g) => g.groupKey == groupKey,
|
||||
orElse: () => WalletGroup(groupKey),
|
||||
)
|
||||
.wallets;
|
||||
}
|
||||
|
||||
/// Iterate through all groups and load their custom names from storage
|
||||
void _loadCustomGroupNames() {
|
||||
for (var group in walletGroups) {
|
||||
final groupName = _sharedPreferences.getString('wallet_group_name_${group.parentAddress}');
|
||||
final key = 'wallet_group_name_${group.groupKey}';
|
||||
final groupName = _sharedPreferences.getString(key);
|
||||
if (groupName != null && group.wallets.length > 1) {
|
||||
group.groupName = groupName; // Restore custom name
|
||||
group.groupName = groupName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Save custom name for a group
|
||||
void _saveCustomGroupName(String parentAddress, String name) {
|
||||
_sharedPreferences.setString('wallet_group_name_$parentAddress', name);
|
||||
void _saveCustomGroupName(String groupKey, String name) {
|
||||
_sharedPreferences.setString('wallet_group_name_$groupKey', name);
|
||||
}
|
||||
|
||||
// Set custom group name and persist it
|
||||
void setGroupName(String parentAddress, String name) {
|
||||
if (parentAddress.isEmpty || name.isEmpty) return;
|
||||
void setGroupName(String groupKey, String name) {
|
||||
if (groupKey.isEmpty || name.isEmpty) return;
|
||||
|
||||
final group = walletGroups.firstWhere((group) => group.parentAddress == parentAddress);
|
||||
final group = walletGroups.firstWhere((g) => g.groupKey == groupKey);
|
||||
group.setCustomName(name);
|
||||
_saveCustomGroupName(parentAddress, name); // Persist the custom name
|
||||
_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.
|
||||
/// 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;
|
||||
|
||||
// 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.
|
||||
return;
|
||||
}
|
||||
|
||||
// Identify the old group key for this wallet
|
||||
final oldGroupKey = _resolveGroupKey(walletInfo); // parentAddress fallback
|
||||
|
||||
// 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 (oldGroupWallets.isEmpty) {
|
||||
// This shouldn't happen, but just in case it does, we return.
|
||||
return;
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
// Finally, we rebuild the groups so that these wallets are now in the new group
|
||||
updateWalletGroups();
|
||||
}
|
||||
|
||||
/// Copy an old group name to the new group key, then remove the old key.
|
||||
Future<void> _migrateGroupName(String oldGroupKey, String newGroupKey) async {
|
||||
final oldNameKey = 'wallet_group_name_$oldGroupKey';
|
||||
final newNameKey = 'wallet_group_name_$newGroupKey';
|
||||
|
||||
final oldGroupName = _sharedPreferences.getString(oldNameKey);
|
||||
if (oldGroupName != null) {
|
||||
await _sharedPreferences.setString(newNameKey, oldGroupName);
|
||||
await _sharedPreferences.remove(oldNameKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user