Skip to content
Open
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
599 changes: 0 additions & 599 deletions PAYJOIN_PACKAGE_PLAN.md

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,36 +1,27 @@
import 'package:bb_mobile/core/seed/data/models/seed_model.dart'
show MnemonicSeedModel, SeedModel;
import 'package:bb_mobile/core/seed/data/repository/seed_repository.dart';
import 'package:bb_mobile/core/settings/domain/settings_entity.dart';
import 'package:bb_mobile/core/utils/logger.dart';
import 'package:bb_mobile/core/wallet/data/repositories/wallet_repository.dart';

class VerifyPhysicalBackupUsecase {
final WalletRepository _walletRepository;
final SeedRepository _seedRepository;
VerifyPhysicalBackupUsecase({
required this._walletRepository,
required this._seedRepository,
});

Future<bool> execute(List<String> mnemonic) async {
try {
final defaultWallets = await _walletRepository.getWallets(
onlyDefaults: true,
onlyBitcoin: true,
environment: Environment.mainnet,
);
if (defaultWallets.isEmpty) {
throw Exception('No default wallet found');
}
final defaultWallet = defaultWallets.first;
final defaultFingerprint = defaultWallet.masterFingerprint;
final defaultSeed = await _seedRepository.get(defaultFingerprint);
VerifyPhysicalBackupUsecase({required this._seedRepository});

final defaultSeedModel = SeedModel.fromEntity(defaultSeed);
final mnemonicWords = switch (defaultSeedModel) {
/// Compares [mnemonic] against the seed stored for [fingerprint].
///
/// The stored secret is read at the point of use and never leaves this
/// method; only the comparison result is returned.
Future<bool> execute({
required String fingerprint,
required List<String> mnemonic,
}) async {
try {
final seed = await _seedRepository.get(fingerprint);
final seedModel = SeedModel.fromEntity(seed);
final mnemonicWords = switch (seedModel) {
MnemonicSeedModel(:final mnemonicWords) => mnemonicWords,
_ => throw Exception('Default seed is not a mnemonic seed'),
_ => throw Exception('Selected seed is not a mnemonic seed'),
};

return mnemonic.length == mnemonicWords.length &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:bb_mobile/core/wallet/domain/entities/wallet.dart';
import 'package:bb_mobile/features/onboarding/complete_physical_backup_verification_usecase.dart';
import 'package:bb_mobile/features/test_wallet_backup/domain/usecases/get_mnemonic_from_fingerprint_usecase.dart';
import 'package:bb_mobile/features/test_wallet_backup/domain/usecases/load_wallets_for_network_usecase.dart';
import 'package:bb_mobile/features/test_wallet_backup/domain/usecases/verify_physical_backup_usecase.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

Expand All @@ -13,146 +14,111 @@ part 'test_wallet_backup_state.dart';

class TestWalletBackupBloc
extends Bloc<TestWalletBackupEvent, TestWalletBackupState> {
final CompletePhysicalBackupVerificationUsecase
_completePhysicalBackupVerificationUsecase;
final LoadWalletsForNetworkUsecase _loadWalletsForNetworkUsecase;
final GetMnemonicFromFingerprintUsecase _getMnemonicFromFingerprintUsecase;
final VerifyPhysicalBackupUsecase _verifyPhysicalBackupUsecase;

TestWalletBackupBloc({
required this._completePhysicalBackupVerificationUsecase,
required this._loadWalletsForNetworkUsecase,
required this._getMnemonicFromFingerprintUsecase,
required this._verifyPhysicalBackupUsecase,
}) : super(const TestWalletBackupState()) {
on<OnWordsSelected>(_onWordsSelected);
on<VerifyPhysicalBackup>(_verifyPhysicalBackup);
on<StartPhysicalBackupVerification>((event, emit) {});
on<LoadWallets>(_onLoadWallets);
on<LoadMnemonicForWallet>(_onLoadMnemonicForWallet);
on<ClearError>((event, emit) => emit(state.copyWith(statusError: '')));
}

final CompletePhysicalBackupVerificationUsecase
_completePhysicalBackupVerificationUsecase;
final LoadWalletsForNetworkUsecase _loadWalletsForNetworkUsecase;
final GetMnemonicFromFingerprintUsecase _getMnemonicFromFingerprintUsecase;

/// Handles word selection during backup verification
/// Validates word order and updates test state
Future<void> _onWordsSelected(
OnWordsSelected event,
Emitter<TestWalletBackupState> emit,
) async {
final mnemonic = state.mnemonic;
final reorderedMnemonic = List<String>.from(
state.reorderedMnemonic + [event.word],
);

final isCorrect = mnemonic
.join(' ')
.startsWith(reorderedMnemonic.join(' '));

if (isCorrect) {
emit(
on<WalletSelected>(_onWalletSelected);
on<VerifyPhysicalBackup>(_verifyPhysicalBackup);
on<ClearError>(
(event, emit) => emit(
state.copyWith(
reorderedMnemonic: [...state.reorderedMnemonic, event.word],
statusError: '',
selectedMnemonicWords: [...state.selectedMnemonicWords, event.index],
verificationStatus: BackupVerificationStatus.idle,
),
);
} else {
final shuffled = List<String>.from(mnemonic)..shuffle();
emit(
state.copyWith(
shuffledMnemonic: shuffled,
reorderedMnemonic: [],
selectedMnemonicWords: [],
statusError: 'Incorrect word order. Please try again.',
),
);
}
),
);
}

Future<void> _verifyPhysicalBackup(
VerifyPhysicalBackup event,
Emitter<TestWalletBackupState> emit,
) async {
try {
if (state.mnemonic.isEmpty) {
emit(state.copyWith(statusError: 'No mnemonic loaded'));
return;
}

if (state.reorderedMnemonic.length != state.mnemonic.length) {
emit(state.copyWith(statusError: 'Please select all words'));
return;
}

// Compare with original mnemonic
final isCorrect =
state.mnemonic.join(' ') == state.reorderedMnemonic.join(' ');

if (isCorrect) {
await _completePhysicalBackupVerificationUsecase.execute();
} else {
// Reset test state when wrong
final shuffled = state.mnemonic.toList()..shuffle();
emit(
state.copyWith(
statusError: 'Incorrect word order. Please try again.',
shuffledMnemonic: shuffled,
reorderedMnemonic: [],
selectedMnemonicWords: [],
),
);
}
} catch (e) {
emit(state.copyWith(statusError: 'Verification failed: $e'));
/// Reads the selected wallet's secret at the point of use.
///
/// The mnemonic and passphrase are returned directly to the caller and are
/// never held in bloc state: secrets must stay ephemeral and must never
/// appear in the freezed `toString()` of the state.
Future<(List<String>, String?)> loadSelectedWalletMnemonic() {
final wallet = state.selectedWallet;
if (wallet == null) {
throw Exception('No wallet selected');
}
return _getMnemonicFromFingerprintUsecase.execute(wallet.masterFingerprint);
}

Future<void> _onLoadWallets(
LoadWallets event,
Emitter<TestWalletBackupState> emit,
) async {
try {
emit(state.copyWith(selectedWallet: null));

final wallets = await _loadWalletsForNetworkUsecase.execute();
if (wallets.isEmpty) throw Exception('No wallets found');
final Wallet selected = wallets.firstWhere(
(w) => w.isDefault,
orElse: () => wallets.first,
);
emit(state.copyWith(wallets: wallets, selectedWallet: selected));

add(LoadMnemonicForWallet(wallet: selected));
emit(
state.copyWith(
wallets: wallets,
selectedWallet: selected,
verificationStatus: BackupVerificationStatus.idle,
),
);
} catch (e) {
emit(state.copyWith(statusError: 'Failed to load wallets: $e'));
}
}

Future<void> _onLoadMnemonicForWallet(
LoadMnemonicForWallet event,
Future<void> _onWalletSelected(
WalletSelected event,
Emitter<TestWalletBackupState> emit,
) async {
emit(
state.copyWith(
selectedWallet: event.wallet,
statusError: '',
verificationStatus: BackupVerificationStatus.idle,
),
);
}

Future<void> _verifyPhysicalBackup(
VerifyPhysicalBackup event,
Emitter<TestWalletBackupState> emit,
) async {
try {
emit(state.copyWith(selectedWallet: null));
final wallet = state.selectedWallet;
if (wallet == null) {
emit(state.copyWith(statusError: 'No wallet selected'));
return;
}

final wallet = event.wallet;
final (
mnemonicWords,
passphrase,
) = await _getMnemonicFromFingerprintUsecase.execute(
wallet.masterFingerprint,
final isCorrect = await _verifyPhysicalBackupUsecase.execute(
fingerprint: wallet.masterFingerprint,
mnemonic: event.reorderedWords,
);

emit(
state.copyWith(
selectedWallet: wallet,
mnemonic: mnemonicWords,
passphrase: passphrase ?? '',
shuffledMnemonic: mnemonicWords.toList()..shuffle(),
reorderedMnemonic: [],
selectedMnemonicWords: [],
),
);
if (isCorrect) {
await _completePhysicalBackupVerificationUsecase.execute();
emit(
state.copyWith(
verificationStatus: BackupVerificationStatus.success,
statusError: '',
),
);
} else {
emit(
state.copyWith(verificationStatus: BackupVerificationStatus.failure),
);
}
} catch (e) {
emit(state.copyWith(statusError: 'Failed to load mnemonic: $e'));
emit(state.copyWith(statusError: 'Verification failed: $e'));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,20 @@ sealed class TestWalletBackupEvent {
const TestWalletBackupEvent();
}

class OnWordsSelected extends TestWalletBackupEvent {
const OnWordsSelected({required this.word, required this.index});
final String word;
final int index;
}

class StartPhysicalBackupVerification extends TestWalletBackupEvent {
const StartPhysicalBackupVerification();
}

class VerifyPhysicalBackup extends TestWalletBackupEvent {
const VerifyPhysicalBackup();
}

class LoadWallets extends TestWalletBackupEvent {
const LoadWallets();
}

class LoadMnemonicForWallet extends TestWalletBackupEvent {
const LoadMnemonicForWallet({required this.wallet});
class WalletSelected extends TestWalletBackupEvent {
const WalletSelected({required this.wallet});
final Wallet wallet;
}

class VerifyPhysicalBackup extends TestWalletBackupEvent {
const VerifyPhysicalBackup({required this.reorderedWords});
final List<String> reorderedWords;
}

class ClearError extends TestWalletBackupEvent {
const ClearError();
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
part of 'test_wallet_backup_bloc.dart';

enum BackupVerificationStatus { idle, success, failure }

@freezed
abstract class TestWalletBackupState with _$TestWalletBackupState {
const factory TestWalletBackupState({
@Default([]) List<String> mnemonic,
@Default('') String passphrase,
@Default([]) List<String> shuffledMnemonic,
@Default([]) List<String> reorderedMnemonic,
@Default([]) List<int> selectedMnemonicWords,
@Default('') String statusError,
@Default([]) List<Wallet> wallets,
@Default(null) Wallet? selectedWallet,
@Default(BackupVerificationStatus.idle)
BackupVerificationStatus verificationStatus,
}) = _TestWalletBackupState;
const TestWalletBackupState._();
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:bb_mobile/core/wallet/data/repositories/wallet_repository.dart';
import 'package:bb_mobile/features/test_wallet_backup/domain/usecases/check_backup_usecase.dart';
import 'package:bb_mobile/features/test_wallet_backup/domain/usecases/get_mnemonic_from_fingerprint_usecase.dart';
import 'package:bb_mobile/features/test_wallet_backup/domain/usecases/load_wallets_for_network_usecase.dart';
import 'package:bb_mobile/features/test_wallet_backup/domain/usecases/verify_physical_backup_usecase.dart';
import 'package:get_it/get_it.dart';

class TestWalletBackupLocator {
Expand All @@ -19,6 +20,11 @@ class TestWalletBackupLocator {
seedRepository: locator<SeedRepository>(),
),
);
locator.registerLazySingleton<VerifyPhysicalBackupUsecase>(
() => VerifyPhysicalBackupUsecase(
seedRepository: locator<SeedRepository>(),
),
);
locator.registerFactory<CheckBackupUsecase>(
() => CheckBackupUsecase(
walletRepository: locator<WalletRepository>(),
Expand Down
4 changes: 2 additions & 2 deletions lib/features/test_wallet_backup/ui/app_bar_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class AppBarWidget extends StatelessWidget {
);

if (selectedWalletId != null) {
bloc.add(LoadMnemonicForWallet(wallet: selectedWallet!));
bloc.add(WalletSelected(wallet: selectedWallet!));
}
},
),
Expand Down Expand Up @@ -119,7 +119,7 @@ Future<String?> _showWalletPicker({
onPressed: () {
final wallet = wallets[controller.selectedItem];
context.read<TestWalletBackupBloc>().add(
LoadMnemonicForWallet(wallet: wallet),
WalletSelected(wallet: wallet),
);
Navigator.of(context).pop();
},
Expand Down
Loading
Loading