Skip to content

Commit 36c3312

Browse files
authored
Merge pull request SatoshiPortal#2124 from SatoshiPortal/fix-checkdefaultwalletusecase-liquid-check
Atomic wallet creation. Create drift db only if bdk and lwk init is successful. Better error on failure.
2 parents fd99b29 + 485ca83 commit 36c3312

32 files changed

Lines changed: 193 additions & 154 deletions

lib/core/wallet/data/repositories/wallet_repository.dart

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,8 @@ class WalletRepository {
9898
}
9999
}
100100

101-
await _walletMetadataDatasource.store(metadata);
102101
final balance = await _getBalance(metadata, sync: sync);
102+
await _walletMetadataDatasource.store(metadata);
103103

104104
return Wallet(
105105
origin: metadata.id,
@@ -369,14 +369,21 @@ class WalletRepository {
369369
if (metadata == null) throw WalletError.notFound(walletId);
370370

371371
if (metadata.isBitcoin) {
372-
await _bdkWallet.delete(wallet: WalletModel.fromMetadata(metadata));
372+
try {
373+
await _bdkWallet.delete(wallet: WalletModel.fromMetadata(metadata));
374+
} on WalletNotFound {
375+
log.warning('deleteWallet: BDK file already absent for $walletId');
376+
}
373377
}
374378

375379
if (metadata.isLiquid) {
376-
await _lwkWallet.delete(wallet: WalletModel.fromMetadata(metadata));
380+
try {
381+
await _lwkWallet.delete(wallet: WalletModel.fromMetadata(metadata));
382+
} on WalletNotFound {
383+
log.warning('deleteWallet: LWK file already absent for $walletId');
384+
}
377385
}
378386

379-
// Delete wallet metadata from database
380387
await _walletMetadataDatasource.delete(walletId);
381388
}
382389

@@ -389,7 +396,11 @@ class WalletRepository {
389396
);
390397

391398
for (final metadata in liquidDefaultWallets) {
392-
await _lwkWallet.delete(wallet: WalletModel.fromMetadata(metadata));
399+
try {
400+
await _lwkWallet.delete(wallet: WalletModel.fromMetadata(metadata));
401+
} on WalletNotFound {
402+
log.warning('deleteLwkDb: LWK file already absent for ${metadata.id}');
403+
}
393404
}
394405
}
395406

lib/core/wallet/domain/usecases/create_default_wallets_usecase.dart

Lines changed: 45 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import 'package:bb_mobile/core/errors/bull_exception.dart';
22
import 'package:bb_mobile/core/seed/data/repository/seed_repository.dart';
33
import 'package:bb_mobile/core/seed/data/services/mnemonic_generator.dart';
4-
import 'package:bb_mobile/core/seed/domain/entity/seed.dart';
54
import 'package:bb_mobile/core/settings/data/settings_repository.dart';
65
import 'package:bb_mobile/core/utils/logger.dart';
76
import 'package:bb_mobile/core/wallet/data/repositories/wallet_repository.dart';
@@ -47,52 +46,54 @@ class CreateDefaultWalletsUsecase {
4746
final hasLiquid = existing.any((w) => w.network.isLiquid);
4847
if (hasBitcoin && hasLiquid) return existing;
4948

50-
final Seed seed;
51-
DateTime? birthday;
52-
if (existing.isNotEmpty) {
53-
final missing = !hasBitcoin ? 'bitcoin' : 'liquid';
54-
log.severe(
55-
message: 'CreateDefaultWalletsUsecase: partial default set detected',
56-
error: CreateDefaultWalletsException('missing $missing default wallet'),
57-
trace: StackTrace.current,
58-
);
59-
seed = await _seedRepository.get(existing.first.masterFingerprint);
60-
} else {
61-
final isGenerated = mnemonicWords == null;
62-
final mnemonic = mnemonicWords ?? _mnemonicGenerator.generate();
63-
if (isGenerated) birthday = DateTime.now().toUtc();
64-
seed = await _seedRepository.createFromMnemonic(
65-
mnemonicWords: mnemonic,
66-
passphrase: passphrase,
67-
);
68-
}
49+
final isGenerated = mnemonicWords == null;
50+
final mnemonic = mnemonicWords ?? _mnemonicGenerator.generate();
51+
final DateTime? birthday = isGenerated ? DateTime.now().toUtc() : null;
52+
final seed = await _seedRepository.createFromMnemonic(
53+
mnemonicWords: mnemonic,
54+
passphrase: passphrase,
55+
);
6956

70-
final tasks = <Future<Wallet>>[];
71-
if (!hasBitcoin) {
72-
tasks.add(
73-
_wallet.createWallet(
74-
seed: seed,
75-
network: bitcoinNetwork,
76-
scriptType: scriptType,
77-
isDefault: true,
78-
birthday: birthday,
79-
),
80-
);
81-
}
82-
if (!hasLiquid) {
83-
tasks.add(
84-
_wallet.createWallet(
85-
seed: seed,
86-
network: liquidNetwork,
87-
scriptType: scriptType,
88-
isDefault: true,
89-
birthday: birthday,
90-
),
91-
);
57+
final created = <Wallet>[];
58+
try {
59+
if (!hasBitcoin) {
60+
created.add(
61+
await _wallet.createWallet(
62+
seed: seed,
63+
network: bitcoinNetwork,
64+
scriptType: scriptType,
65+
isDefault: true,
66+
birthday: birthday,
67+
),
68+
);
69+
}
70+
if (!hasLiquid) {
71+
created.add(
72+
await _wallet.createWallet(
73+
seed: seed,
74+
network: liquidNetwork,
75+
scriptType: scriptType,
76+
isDefault: true,
77+
birthday: birthday,
78+
),
79+
);
80+
}
81+
} catch (_) {
82+
for (final wallet in created) {
83+
try {
84+
await _wallet.deleteWallet(walletId: wallet.id);
85+
} catch (e, stackTrace) {
86+
log.severe(
87+
message: 'CreateDefaultWalletsUsecase: rollback failed',
88+
error: StateError('rollback threw ${e.runtimeType}'),
89+
trace: stackTrace,
90+
);
91+
}
92+
}
93+
rethrow;
9294
}
9395

94-
final newWallets = await Future.wait(tasks);
95-
return [...existing, ...newWallets];
96+
return [...existing, ...created];
9697
} catch (e) {
9798
throw CreateDefaultWalletsException(e.toString());
9899
}

lib/features/app_startup/domain/usecases/check_for_existing_default_wallets_usecase.dart

Lines changed: 62 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'package:bb_mobile/core/seed/data/repository/seed_repository.dart';
22
import 'package:bb_mobile/core/settings/data/settings_repository.dart';
33
import 'package:bb_mobile/core/utils/logger.dart';
44
import 'package:bb_mobile/core/wallet/data/repositories/wallet_repository.dart';
5+
import 'package:bb_mobile/core/wallet/domain/entities/wallet.dart';
56

67
class CheckForExistingDefaultWalletsUsecase {
78
final SettingsRepository _settingsRepository;
@@ -20,7 +21,7 @@ class CheckForExistingDefaultWalletsUsecase {
2021
final settings = await _settingsRepository.fetch();
2122
final environment = settings.environment;
2223

23-
late final List defaultWallets;
24+
List<Wallet> defaultWallets;
2425
try {
2526
defaultWallets = await _walletRepository.getWallets(
2627
onlyDefaults: true,
@@ -40,29 +41,68 @@ class CheckForExistingDefaultWalletsUsecase {
4041
}
4142
}
4243

43-
if (defaultWallets.isNotEmpty) {
44-
log.fine('FINE: found default wallet');
45-
// Check all seeds in parallel to avoid sequential keychain reads
46-
// blocking the UI thread
47-
await Future.wait(
48-
defaultWallets.map((wallet) async {
49-
try {
50-
await _seedRepository.get(wallet.masterFingerprint);
51-
log.fine('FINE: Seed Found');
52-
} catch (e) {
53-
log.severe(
54-
message: 'Seed not found for default wallet ',
55-
error: e,
56-
trace: StackTrace.current,
57-
);
58-
rethrow;
59-
}
60-
}),
61-
);
62-
return true;
63-
} else {
44+
if (defaultWallets.isEmpty) {
6445
log.fine('No default wallets found');
6546
return false;
6647
}
48+
49+
final hasBitcoin = defaultWallets.any((w) => w.network.isBitcoin);
50+
final hasLiquid = defaultWallets.any((w) => w.network.isLiquid);
51+
if (!hasBitcoin || !hasLiquid) {
52+
final missing = !hasBitcoin ? 'bitcoin' : 'liquid';
53+
log.severe(
54+
message:
55+
'CheckForExistingDefaultWalletsUsecase: partial default set at cold start',
56+
error: StateError('missing $missing default wallet'),
57+
trace: StackTrace.current,
58+
);
59+
try {
60+
final seed = await _seedRepository.get(
61+
defaultWallets.first.masterFingerprint,
62+
);
63+
final network = !hasBitcoin
64+
? (environment.isMainnet
65+
? Network.bitcoinMainnet
66+
: Network.bitcoinTestnet)
67+
: (environment.isMainnet
68+
? Network.liquidMainnet
69+
: Network.liquidTestnet);
70+
await _walletRepository.createWallet(
71+
seed: seed,
72+
network: network,
73+
scriptType: ScriptType.bip84,
74+
isDefault: true,
75+
);
76+
defaultWallets = await _walletRepository.getWallets(
77+
onlyDefaults: true,
78+
environment: environment,
79+
);
80+
} catch (e, stackTrace) {
81+
log.severe(
82+
message:
83+
'CheckForExistingDefaultWalletsUsecase: legacy heal failed',
84+
error: StateError('legacy heal threw ${e.runtimeType}'),
85+
trace: stackTrace,
86+
);
87+
}
88+
}
89+
90+
log.fine('FINE: found default wallet');
91+
await Future.wait(
92+
defaultWallets.map((wallet) async {
93+
try {
94+
await _seedRepository.get(wallet.masterFingerprint);
95+
log.fine('FINE: Seed Found');
96+
} catch (e) {
97+
log.severe(
98+
message: 'Seed not found for default wallet ',
99+
error: e,
100+
trace: StackTrace.current,
101+
);
102+
rethrow;
103+
}
104+
}),
105+
);
106+
return true;
67107
}
68108
}

lib/features/onboarding/presentation/bloc/onboarding_bloc.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ class OnboardingBloc extends Bloc<OnboardingEvent, OnboardingState> {
5555
state.copyWith(
5656
onboardingStepStatus: OnboardingStepStatus.loading,
5757
step: OnboardingStep.create,
58+
statusError: '',
5859
),
5960
);
6061
await _createDefaultWalletsUsecase.execute();
@@ -75,6 +76,7 @@ class OnboardingBloc extends Bloc<OnboardingEvent, OnboardingState> {
7576
state.copyWith(
7677
onboardingStepStatus: OnboardingStepStatus.loading,
7778
step: OnboardingStep.recover,
79+
statusError: '',
7880
),
7981
);
8082
await _createDefaultWalletsUsecase.execute(

lib/features/onboarding/ui/onboarding_router.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:bb_mobile/features/onboarding/ui/screens/onboarding_splash.dart'
44
import 'package:bb_mobile/features/onboarding/ui/screens/recover_options.dart';
55
import 'package:bb_mobile/features/wallet/presentation/bloc/wallet_bloc.dart';
66
import 'package:bb_mobile/features/wallet/ui/wallet_router.dart';
7+
import 'package:bb_mobile/core/utils/build_context_x.dart';
78
import 'package:bb_mobile/locator.dart';
89
import 'package:flutter/material.dart';
910
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -55,6 +56,18 @@ class OnboardingRouter {
5556
}
5657
},
5758
),
59+
BlocListener<OnboardingBloc, OnboardingState>(
60+
listenWhen: (previous, current) =>
61+
previous.statusError != current.statusError &&
62+
current.statusError.isNotEmpty,
63+
listener: (context, state) {
64+
ScaffoldMessenger.of(context).showSnackBar(
65+
SnackBar(
66+
content: Text(context.loc.walletSetupErrorTryAgain),
67+
),
68+
);
69+
},
70+
),
5871
],
5972
child: child,
6073
),

lib/features/recoverbull/errors.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ class VaultDecryptionError extends RecoverBullError {
8585
class VaultRecoveryError extends RecoverBullError {
8686
@override
8787
String toTranslated(BuildContext context) {
88-
return context.loc.recoverbullErrorRecoveryFailed;
88+
return context.loc.walletSetupErrorTryAgain;
8989
}
9090
}
9191

localization/app_ar.arb

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4008,10 +4008,6 @@
40084008
"@payCorporate": {
40094009
"description": "Label for corporate checkbox"
40104010
},
4011-
"recoverbullErrorRecoveryFailed": "فشل في استعادة الخزنة",
4012-
"@recoverbullErrorRecoveryFailed": {
4013-
"description": "Error message when vault recovery fails"
4014-
},
40154011
"buyMax": "ماكس",
40164012
"@buyMax": {
40174013
"description": "Button to fill maximum amount"
@@ -12586,5 +12582,6 @@
1258612582
"legacyStorageContinueWithoutBackupButton": "متابعة دون نسخة احتياطية",
1258712583
"legacyStorageAcceptRisksCheckbox": "أوافق على مخاطر عدم عمل نسخة احتياطية",
1258812584
"legacyStorageHasBackupImportantBody": "يرجى عدم تخطي هذه العملية إلا إذا كنت بحاجة ماسة إلى استخدام المحفظة بشكل عاجل أو في انتظار اكتمال عملية مبادلة.",
12589-
"legacyStorageRiskAckButtonHasBackup": "أفهم المخاطر"
12585+
"legacyStorageRiskAckButtonHasBackup": "أفهم المخاطر",
12586+
"walletSetupErrorTryAgain": "Something went wrong setting up your wallet. Please try again."
1259012587
}

localization/app_as.arb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,5 +195,6 @@
195195
"legacyStorageContinueWithoutBackupButton": "বেকআপ নকৰাকৈ আগবাঢ়ক",
196196
"legacyStorageAcceptRisksCheckbox": "মই বেকআপ নকৰাৰ বিপদ গ্ৰহণ কৰোঁ",
197197
"legacyStorageHasBackupImportantBody": "এই প্ৰক্ৰিয়া এৰি নাযাব যদিহে আপুনি ৱালেট জৰুৰীভাৱে ব্যৱহাৰ কৰাটো অত্যাৱশ্যক নহয় বা এটা স্বেপ সম্পূৰ্ণ হোৱালৈ অপেক্ষা কৰি আছে।",
198-
"legacyStorageRiskAckButtonHasBackup": "মই বিপদটো বুজি পাইছোঁ"
198+
"legacyStorageRiskAckButtonHasBackup": "মই বিপদটো বুজি পাইছোঁ",
199+
"walletSetupErrorTryAgain": "Something went wrong setting up your wallet. Please try again."
199200
}

localization/app_bg.arb

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3443,10 +3443,6 @@
34433443
"@payCorporate": {
34443444
"description": "Label for corporate checkbox"
34453445
},
3446-
"recoverbullErrorRecoveryFailed": "Неуспях да възстановя сейфа",
3447-
"@recoverbullErrorRecoveryFailed": {
3448-
"description": "Error message when vault recovery fails"
3449-
},
34503446
"buyMax": "Макс.",
34513447
"@buyMax": {
34523448
"description": "Button to fill maximum amount"
@@ -9593,5 +9589,6 @@
95939589
"legacyStorageContinueWithoutBackupButton": "Продължи без резервно копие",
95949590
"legacyStorageAcceptRisksCheckbox": "Приемам рисковете от това да не направя резервно копие",
95959591
"legacyStorageHasBackupImportantBody": "Моля, не пропускайте този процес, освен ако наистина не се налага спешно да използвате портфейла или не чакате суап да приключи.",
9596-
"legacyStorageRiskAckButtonHasBackup": "Разбирам риска"
9592+
"legacyStorageRiskAckButtonHasBackup": "Разбирам риска",
9593+
"walletSetupErrorTryAgain": "Something went wrong setting up your wallet. Please try again."
95979594
}

localization/app_bn.arb

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4175,10 +4175,6 @@
41754175
"@payCorporate": {
41764176
"description": "Label for corporate checkbox"
41774177
},
4178-
"recoverbullErrorRecoveryFailed": "ভল্ট পুনরুদ্ধার করতে ব্যর্থ হয়েছে",
4179-
"@recoverbullErrorRecoveryFailed": {
4180-
"description": "Error message when vault recovery fails"
4181-
},
41824178
"buyMax": "সর্বোচ্চ",
41834179
"@buyMax": {
41844180
"description": "Button to fill maximum amount"
@@ -12300,5 +12296,6 @@
1230012296
"legacyStorageContinueWithoutBackupButton": "ব্যাকআপ ছাড়াই চালিয়ে যান",
1230112297
"legacyStorageAcceptRisksCheckbox": "ব্যাকআপ না করার ঝুঁকি আমি গ্রহণ করছি",
1230212298
"legacyStorageHasBackupImportantBody": "এই প্রক্রিয়াটি এড়িয়ে যাবেন না যদি না আপনার জরুরিভাবে ওয়ালেট ব্যবহার করার একান্ত প্রয়োজন হয় অথবা একটি স্বপ সম্পন্ন হওয়ার জন্য অপেক্ষা করছেন।",
12303-
"legacyStorageRiskAckButtonHasBackup": "আমি ঝুঁকি বুঝতে পারছি"
12299+
"legacyStorageRiskAckButtonHasBackup": "আমি ঝুঁকি বুঝতে পারছি",
12300+
"walletSetupErrorTryAgain": "Something went wrong setting up your wallet. Please try again."
1230412301
}

0 commit comments

Comments
 (0)