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
20 changes: 17 additions & 3 deletions lib/core/utils/descriptor_derivation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,23 @@ class DescriptorDerivation {
required ScriptType scriptType,
required bool isTestnet,
}) async {
final lwk.Descriptor confidentialDescriptor = await lwk
.Descriptor.newConfidential(
network: isTestnet ? lwk.Network.testnet : lwk.Network.mainnet,
final network = isTestnet ? lwk.Network.testnet : lwk.Network.mainnet;

// Map ScriptType to lwk.ScriptVariant
final lwk.ScriptVariant scriptVariant = switch (scriptType) {
ScriptType.bip84 => lwk.ScriptVariant.wpkh,
ScriptType.bip49 => lwk.ScriptVariant.shWpkh,
ScriptType.bip44 => throw UnsupportedError(
'BIP44 is not supported for Liquid wallets. Use BIP84 or BIP49.',
),
};

// Use lwk-dart's BIP49 support with proper script variant
final lwk.Descriptor confidentialDescriptor =
await lwk.Descriptor.newConfidentialWithScript(
network: network,
mnemonic: mnemonic,
scriptVariant: scriptVariant,
);

return confidentialDescriptor.ctDescriptor;
Expand Down Expand Up @@ -104,4 +117,5 @@ class DescriptorDerivation {

return descriptor.asString();
}

}
14 changes: 14 additions & 0 deletions lib/core/wallet/data/repositories/wallet_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,20 @@ class WalletRepository {
)
.toList();

// Sort wallets: Liquid first, then Bitcoin; defaults before non-defaults within each network
filteredWallets.sort((a, b) {
// First, sort by network (Liquid first)
if (a.isLiquid != b.isLiquid) {
return a.isLiquid ? -1 : 1;
}
// Within same network, defaults first
if (a.isDefault != b.isDefault) {
return a.isDefault ? -1 : 1;
}
// If both same network and same default status, maintain original order
return 0;
});

final balances = await Future.wait(
filteredWallets.map((wallet) => _getBalance(wallet, sync: sync)),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,26 @@ import 'package:bb_mobile/core/settings/data/settings_repository.dart';
import 'package:bb_mobile/core/utils/logger.dart';
import 'package:bb_mobile/core/wallet/data/repositories/wallet_repository.dart';
import 'package:bb_mobile/core/wallet/domain/entities/wallet.dart';
import 'package:bb_mobile/core/wallet/domain/usecases/detect_liquid_script_type_usecase.dart';

class CreateDefaultWalletsUsecase {
final SeedRepository _seedRepository;
final SettingsRepository _settingsRepository;
final MnemonicGenerator _mnemonicGenerator;
final WalletRepository _wallet;
final DetectLiquidScriptTypeUsecase _detectLiquidScriptTypeUsecase;

CreateDefaultWalletsUsecase({
required SeedRepository seedRepository,
required SettingsRepository settingsRepository,
required MnemonicGenerator mnemonicGenerator,
required WalletRepository walletRepository,
required DetectLiquidScriptTypeUsecase detectLiquidScriptTypeUsecase,
}) : _seedRepository = seedRepository,
_settingsRepository = settingsRepository,
_mnemonicGenerator = mnemonicGenerator,
_wallet = walletRepository;
_wallet = walletRepository,
_detectLiquidScriptTypeUsecase = detectLiquidScriptTypeUsecase;

Future<List<Wallet>> execute({
List<String>? mnemonicWords,
Expand All @@ -44,7 +48,7 @@ class CreateDefaultWalletsUsecase {
);

// The current default script type for the wallets is BIP84
const scriptType = ScriptType.bip84;
const bitcoinScriptType = ScriptType.bip84;

// Get the current environment to determine the network
final settings = await _settingsRepository.fetch();
Expand All @@ -56,27 +60,38 @@ class CreateDefaultWalletsUsecase {
final liquidNetwork =
environment.isMainnet ? Network.liquidMainnet : Network.liquidTestnet;

// The default wallets should be 1 Bitcoin and 1 Liquid wallet.
final defaultWallets = await Future.wait([
// For Liquid wallets during recovery, check if user has legacy BIP49 (Aqua) funds
ScriptType liquidScriptType = ScriptType.bip84;
if (mnemonicWords != null) {
// Wallet recovery - check BIP49 first for Aqua compatibility
liquidScriptType = await _detectLiquidScriptTypeUsecase.execute(
seed: seed,
network: liquidNetwork,
birthday: birthday,
);
}

// Create default wallets with the determined script types
final allWallets = await Future.wait([
_wallet.createWallet(
seed: seed,
network: bitcoinNetwork,
scriptType: scriptType,
scriptType: bitcoinScriptType,
isDefault: true,
birthday: birthday,
),
_wallet.createWallet(
seed: seed,
network: liquidNetwork,
scriptType: scriptType,
scriptType: liquidScriptType,
isDefault: true,
birthday: birthday,
),
]);

log.fine('Default wallets created');
log.fine('Wallets created: ${allWallets.length} total');

return defaultWallets;
return allWallets;
} catch (e) {
throw CreateDefaultWalletsException(e.toString());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import 'package:bb_mobile/core/seed/domain/entity/seed.dart';
import 'package:bb_mobile/core/utils/logger.dart';
import 'package:bb_mobile/core/wallet/data/repositories/wallet_repository.dart';
import 'package:bb_mobile/core/wallet/domain/entities/wallet.dart';

/// Detects the appropriate script type for a Liquid wallet during recovery.
///
/// This usecase checks if the user has legacy BIP49 (Aqua wallet) funds by:
/// 1. Creating a temporary BIP49 Liquid wallet
/// 2. Syncing it to check on-chain balance
/// 3. Deleting the temporary wallet
/// 4. Returning BIP49 if funds found, otherwise BIP84
///
/// NOTE: We must create a temporary wallet because lwk-dart requires disk-based
/// wallets (no in-memory option like BDK) and doesn't support address derivation
/// without wallet creation. This is a one-time cost during wallet recovery for
/// Aqua compatibility.
class DetectLiquidScriptTypeUsecase {
final WalletRepository _walletRepository;

DetectLiquidScriptTypeUsecase({
required WalletRepository walletRepository,
}) : _walletRepository = walletRepository;

Future<ScriptType> execute({
required Seed seed,
required Network network,
DateTime? birthday,
}) async {
// Check BIP49 first for Aqua compatibility
final testBip49Wallet = await _walletRepository.createWallet(
seed: seed,
network: network,
scriptType: ScriptType.bip49,
isDefault: false,
birthday: birthday,
sync: true, // Must sync to detect on-chain funds
);

final hasFunds = testBip49Wallet.balanceSat > BigInt.zero;
await _walletRepository.deleteWallet(walletId: testBip49Wallet.id);

if (hasFunds) {
log.fine(
'Detected BIP49 Liquid wallet with balance: ${testBip49Wallet.balanceSat}',
);
log.warning(
'Importing legacy BIP49 Liquid wallet (Aqua compatibility). '
'Consider migrating to BIP84 for lower transaction fees.',
);
return ScriptType.bip49;
}

log.fine('No funds in BIP49 Liquid wallet, using BIP84');
return ScriptType.bip84;
}
}
39 changes: 34 additions & 5 deletions lib/core/wallet/domain/usecases/import_wallet_usecase.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,25 @@ import 'package:bb_mobile/core/settings/data/settings_repository.dart';
import 'package:bb_mobile/core/utils/logger.dart';
import 'package:bb_mobile/core/wallet/data/repositories/wallet_repository.dart';
import 'package:bb_mobile/core/wallet/domain/entities/wallet.dart';
import 'package:bb_mobile/core/wallet/domain/usecases/detect_liquid_script_type_usecase.dart';

class ImportWalletUsecase {
final SeedRepository _seedRepository;
final SettingsRepository _settingsRepository;
final WalletRepository _wallet;
final DetectLiquidScriptTypeUsecase _detectLiquidScriptTypeUsecase;

ImportWalletUsecase({
required SeedRepository seedRepository,
required SettingsRepository settingsRepository,
required WalletRepository walletRepository,
required DetectLiquidScriptTypeUsecase detectLiquidScriptTypeUsecase,
}) : _seedRepository = seedRepository,
_settingsRepository = settingsRepository,
_wallet = walletRepository;
_wallet = walletRepository,
_detectLiquidScriptTypeUsecase = detectLiquidScriptTypeUsecase;

Future<Wallet> execute({
Future<List<Wallet>> execute({
required List<String> mnemonicWords,
ScriptType scriptType = ScriptType.bip84,
String passphrase = '',
Expand All @@ -32,24 +36,49 @@ class ImportWalletUsecase {
environment.isMainnet
? Network.bitcoinMainnet
: Network.bitcoinTestnet;
final liquidNetwork =
environment.isMainnet ? Network.liquidMainnet : Network.liquidTestnet;

final seed = await _seedRepository.createFromMnemonic(
mnemonicWords: mnemonicWords,
passphrase: passphrase,
);

final wallet = _wallet.createWallet(
final importedWallets = <Wallet>[];

// Create Bitcoin wallet with user-selected script type
final bitcoinWallet = await _wallet.createWallet(
seed: seed,
network: bitcoinNetwork,
scriptType: scriptType,
isDefault: false,
sync: false,
label: label,
);
importedWallets.add(bitcoinWallet);
log.fine('Bitcoin wallet imported: ${bitcoinWallet.derivationPath}');

// For Liquid, check if user has legacy BIP49 (Aqua) funds
final liquidScriptType = await _detectLiquidScriptTypeUsecase.execute(
seed: seed,
network: liquidNetwork,
);

// Create Liquid wallet with determined script type
final liquidWallet = await _wallet.createWallet(
seed: seed,
network: liquidNetwork,
scriptType: liquidScriptType,
isDefault: false,
sync: false,
label: label,
);
importedWallets.add(liquidWallet);
log.fine('Liquid wallet imported: ${liquidWallet.derivationPath}');

log.fine('Wallet imported');
log.fine('Wallets imported: ${importedWallets.length} total');

return wallet;
return importedWallets;
} catch (e) {
throw ImportWalletException(e.toString());
}
Expand Down
8 changes: 8 additions & 0 deletions lib/core/wallet/wallet_locator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import 'package:bb_mobile/core/wallet/domain/usecases/check_wallet_status_usecas
import 'package:bb_mobile/core/wallet/domain/usecases/check_wallet_syncing_usecase.dart';
import 'package:bb_mobile/core/wallet/domain/usecases/create_default_wallets_usecase.dart';
import 'package:bb_mobile/core/wallet/domain/usecases/delete_wallet_usecase.dart';
import 'package:bb_mobile/core/wallet/domain/usecases/detect_liquid_script_type_usecase.dart';
import 'package:bb_mobile/core/wallet/domain/usecases/get_receive_address_usecase.dart';
import 'package:bb_mobile/core/wallet/domain/usecases/get_wallet_transactions_usecase.dart';
import 'package:bb_mobile/core/wallet/domain/usecases/get_wallet_usecase.dart';
Expand Down Expand Up @@ -129,12 +130,18 @@ class WalletLocator {
}

static void registerUsecases() {
locator.registerFactory<DetectLiquidScriptTypeUsecase>(
() => DetectLiquidScriptTypeUsecase(
walletRepository: locator<WalletRepository>(),
),
);
locator.registerFactory<CreateDefaultWalletsUsecase>(
() => CreateDefaultWalletsUsecase(
seedRepository: locator<SeedRepository>(),
settingsRepository: locator<SettingsRepository>(),
mnemonicGenerator: locator<MnemonicGenerator>(),
walletRepository: locator<WalletRepository>(),
detectLiquidScriptTypeUsecase: locator<DetectLiquidScriptTypeUsecase>(),
),
);
locator.registerFactory<GetWalletUsecase>(
Expand Down Expand Up @@ -210,6 +217,7 @@ class WalletLocator {
walletRepository: locator<WalletRepository>(),
seedRepository: locator<SeedRepository>(),
settingsRepository: locator<SettingsRepository>(),
detectLiquidScriptTypeUsecase: locator<DetectLiquidScriptTypeUsecase>(),
),
);
locator.registerFactory<TheDirtyUsecase>(
Expand Down
5 changes: 4 additions & 1 deletion lib/core/wallet/wallet_metadata_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,10 @@ class WalletMetadataService {
xpubFingerprint: xpub.fingerprintHex,
signer: Signer.local,
signerDevice: null,
xpub: xpub.convert(scriptType.getXpubType(network)),
// For Liquid, always use standard xpub format (LWK doesn't support ypub/zpub)
xpub: network.isLiquid
? xpub.toBase58()
: xpub.convert(scriptType.getXpubType(network)),
externalPublicDescriptor: descriptor,
internalPublicDescriptor: changeDescriptor,
isDefault: isDefault,
Expand Down
4 changes: 2 additions & 2 deletions lib/features/import_mnemonic/presentation/cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,13 @@ class ImportMnemonicCubit extends Cubit<ImportMnemonicState> {
emit(state.copyWith(isLoading: true, error: null));

final mnemonic = state.mnemonic!;
final wallet = await _importWalletUsecase.execute(
final wallets = await _importWalletUsecase.execute(
mnemonicWords: mnemonic.words,
label: mnemonic.label,
passphrase: mnemonic.passphrase,
scriptType: state.scriptType,
);
emit(state.copyWith(wallet: wallet, isLoading: false));
emit(state.copyWith(wallets: wallets, isLoading: false));
} catch (e) {
emit(
state.copyWith(
Expand Down
2 changes: 1 addition & 1 deletion lib/features/import_mnemonic/presentation/state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ sealed class ImportMnemonicState with _$ImportMnemonicState {
@Default(null) Mnemonic? mnemonic,
@Default(ScriptType.bip84) ScriptType scriptType,
@Default(false) bool isLoading,
@Default(null) Wallet? wallet,
@Default(null) List<Wallet>? wallets,
@Default(null) ({BigInt satoshis, int transactions})? bip44Status,
@Default(null) ({BigInt satoshis, int transactions})? bip49Status,
@Default(null) ({BigInt satoshis, int transactions})? bip84Status,
Expand Down
2 changes: 1 addition & 1 deletion lib/features/import_mnemonic/router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class ImportMnemonicRouter {
return BlocListener<ImportMnemonicCubit, ImportMnemonicState>(
listenWhen:
(previous, current) =>
previous.wallet == null && current.wallet != null,
previous.wallets == null && current.wallets != null,
listener: (context, state) {
// Trigger wallet refresh before navigating to home
context.read<WalletBloc>().add(const WalletStarted());
Expand Down