Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 19 additions & 0 deletions PR_BODY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Summary

- Preserve `WalletConfig.isLegacyWallet` in `copy()` so legacy wallets route to `AuthRestoreRequested`.
- Sanitize legacy wallet names: replace non-alphanumeric (Unicode letters/digits) except "_" with "_".
- Resolve collisions by appending the lowest integer suffix (e.g., name, name_1, name_2, ...).
- Apply during legacy migration in `AuthBloc._onRestore`; delete legacy entry after success.

Why

Fixes a critical bug where some users can't log into wallets created with old versions and the wallet disappears.

Testing

- Create a legacy wallet with special characters and attempt login; verify migration uses sanitized unique name and signs in.
- Verify if a non-legacy wallet already has the sanitized name, the migrated wallet uses the lowest available _N suffix.

Notes

No API changes. Static analysis passes for changed files.
56 changes: 46 additions & 10 deletions lib/bloc/auth_bloc/auth_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -239,12 +239,46 @@ class AuthBloc extends Bloc<AuthBlocEvent, AuthBlocState> with TrezorAuthMixin {
Emitter<AuthBlocState> emit,
) async {
try {
if (await _didSignInExistingWallet(event.wallet, event.password)) {
// Legacy wallets: sanitize base name, try sign-in, then resolve
// uniqueness only if needed. Non-legacy restores (seed imports) keep the
// user-provided name unchanged.
Wallet workingWallet = event.wallet;
if (event.wallet.isLegacyWallet) {
final String baseName = _walletsRepository.sanitizeLegacyMigrationName(
event.wallet.name,
);
final Wallet sanitizedBaseWallet = event.wallet.copyWith(
name: baseName,
);

// Attempt sign-in with sanitized base name first to avoid creating a
// suffixed duplicate when a migrated wallet already exists.
if (await _didSignInExistingWallet(
sanitizedBaseWallet,
event.password,
)) {
add(
AuthSignInRequested(
wallet: sanitizedBaseWallet,
password: event.password,
),
);
_log.warning('Wallet $baseName already exists, attempting sign-in');
return;
}

// Otherwise, resolve the lowest available unique name for registration.
final String uniqueName = await _walletsRepository
.resolveUniqueWalletName(baseName);
workingWallet = event.wallet.copyWith(name: uniqueName);
}

if (await _didSignInExistingWallet(workingWallet, event.password)) {
add(
AuthSignInRequested(wallet: event.wallet, password: event.password),
AuthSignInRequested(wallet: workingWallet, password: event.password),
);
_log.warning(
'Wallet ${event.wallet.name} already exists, attempting sign-in',
'Wallet ${workingWallet.name} already exists, attempting sign-in',
);
return;
}
Expand All @@ -254,10 +288,10 @@ class AuthBloc extends Bloc<AuthBlocEvent, AuthBlocState> with TrezorAuthMixin {
final weakPasswordsAllowed = await _areWeakPasswordsAllowed();
await _kdfSdk.auth.register(
password: event.password,
walletName: event.wallet.name,
walletName: workingWallet.name,
mnemonic: Mnemonic.plaintext(event.seed),
options: AuthOptions(
derivationMethod: event.wallet.config.type == WalletType.hdwallet
derivationMethod: workingWallet.config.type == WalletType.hdwallet
? DerivationMethod.hdWallet
: DerivationMethod.iguana,
allowWeakPassword: weakPasswordsAllowed,
Expand All @@ -268,16 +302,18 @@ class AuthBloc extends Bloc<AuthBlocEvent, AuthBlocState> with TrezorAuthMixin {
'Successfully restored wallet from a seed. '
'Setting up wallet metadata and logging in...',
);
await _kdfSdk.setWalletType(event.wallet.config.type);
await _kdfSdk.confirmSeedBackup(hasBackup: event.wallet.config.hasBackup);
await _kdfSdk.setWalletType(workingWallet.config.type);
await _kdfSdk.confirmSeedBackup(
hasBackup: workingWallet.config.hasBackup,
);
// Filter out geo-blocked assets from default coins before adding to wallet
final allowedDefaultCoins = _filterBlockedAssets(enabledByDefaultCoins);
await _kdfSdk.addActivatedCoins(allowedDefaultCoins);
if (event.wallet.config.activatedCoins.isNotEmpty) {
if (workingWallet.config.activatedCoins.isNotEmpty) {
// Seed import files and legacy wallets may contain removed or unsupported
// coins, so we filter them out before adding them to the wallet metadata.
final availableWalletCoins = _filterOutUnsupportedCoins(
event.wallet.config.activatedCoins,
workingWallet.config.activatedCoins,
);
// Also filter out geo-blocked assets from restored wallet coins
final allowedWalletCoins = _filterBlockedAssets(availableWalletCoins);
Expand All @@ -289,7 +325,7 @@ class AuthBloc extends Bloc<AuthBlocEvent, AuthBlocState> with TrezorAuthMixin {
if (event.wallet.isLegacyWallet) {
_log.info(
'Migration successful. '
'Deleting legacy wallet ${event.wallet.name}',
'Deleting legacy wallet ${workingWallet.name}',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Incorrect Wallet Name Logged During Deletion

When deleting a legacy wallet, the log message shows workingWallet.name (the sanitized name), but the deleteWallet call removes event.wallet (the original legacy wallet). This logs the incorrect wallet name for the item being deleted.

Fix in Cursor Fix in Web

);
await _walletsRepository.deleteWallet(
event.wallet,
Expand Down
34 changes: 34 additions & 0 deletions lib/blocs/wallets_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -220,4 +220,38 @@ class WalletsRepository {
}
}
}

/// Sanitizes a legacy wallet name for migration by replacing any
/// non-alphanumeric character (Unicode letters/digits) except underscore
/// with an underscore. This ensures compatibility with stricter name rules
/// in the target storage/backend.
String sanitizeLegacyMigrationName(String name) {
final sanitized = name.replaceAll(
RegExp(r'[^\p{L}\p{N}_]', unicode: true),
'_',
);
// Avoid returning an empty string
return sanitized.isEmpty ? '_' : sanitized;
}

/// Resolves a unique wallet name by appending the lowest integer suffix
/// starting at 1 that makes the name unique across both SDK and legacy
/// wallets. If [baseName] is already unique, it is returned unchanged.
Future<String> resolveUniqueWalletName(String baseName) async {
final List<Wallet> allWallets = await getWallets();
final Set<String> existing = allWallets.map((w) => w.name).toSet();
if (!existing.contains(baseName)) return baseName;

int i = 1;
while (existing.contains('${baseName}_$i')) {
i++;
}
return '${baseName}_$i';
}

/// Convenience helper for migration: sanitize and then ensure uniqueness.
Future<String> sanitizeAndResolveLegacyWalletName(String legacyName) async {
final sanitized = sanitizeLegacyMigrationName(legacyName);
return resolveUniqueWalletName(sanitized);
}
}
13 changes: 6 additions & 7 deletions lib/model/wallet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,8 @@ class Wallet {
Wallet copy() {
return Wallet(id: id, name: name, config: config.copy());
}

Wallet copyWith({
String? id,
String? name,
WalletConfig? config,
}) {

Wallet copyWith({String? id, String? name, WalletConfig? config}) {
return Wallet(
id: id ?? this.id,
name: name ?? this.name,
Expand Down Expand Up @@ -129,9 +125,12 @@ class WalletConfig {
type: type,
seedPhrase: seedPhrase,
pubKey: pubKey,
// Preserve legacy flag when copying config; losing this flag breaks
// legacy login flow and can hide the wallet from lists.
isLegacyWallet: isLegacyWallet,
);
}

WalletConfig copyWith({
String? seedPhrase,
String? pubKey,
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.9.3+0
version: 0.9.3+1

environment:
sdk: ">=3.8.1 <4.0.0"
Expand Down
Loading