Files
hash-wallet/cw_bitcoin/lib/bitcoin_wallet_service.dart
T
e4806ad141 CW948: feat: bitcoin view-only (#2261)
* feat: bitcoin view-only

* fix: add UR to cw_bitcoin, fix payjoin issue

* feat: add unsignedPsbt to PendingBitcoinTransaction

* wip

* feat: xpub restore from bitcoin URI QR

* feat(dev): debug UR codes [skip ci]

* fix: master fingerprint default in create bitcoin transaction

* fix: handle empty private key in bitcoin transaction building

* fix: show success dialog after btc ur transaction

* fix(ui): remove SP from UI in view only wallet

* fix(cw_bitcoin): tx sending on non-airgap

* fix(cw_bitcoin): disable all non-segwit addresses

* feat(ur): support BBQR for coldcard qr

* flutter 3.27.0 -> 3.27.4

* feat: raw xpub qr support for coldcard q

* fix: update ledger_bitcoin
fix: update switching button

* fix: restore button not showing up after scanning

* feat: commit raw tx (from coldcard Q)

* feat: legacy crypto-psbt support for seed signer

* chore: address comments from review

* fix(cw_bitcoin): disable sp and payjoin for cupcake

* feat: enable DNSSEC BIP353 in PSBT (#2397)

* feat: enable DNSSEC BIP353 in PSBT

* chore: put bitcoin view only behind a featureflag

* fix: update Monero wallet to work with new internal URQR return format

* feat: add "Restore from Cupcake App" option with localized strings

* feat: enable DNSSEC BIP353 in PSBT using dnssec-prover (#2401)

* fix: use dnssec-prover
chore: bump ndk to r28c

* fix: validate DNSSEC proof before updating the UI

* fix: rename button to copy debug info to not hide actions on URQR widget

---------

Co-authored-by: Konstantin Ullrich <konstantinullrich12@gmail.com>
2025-07-29 17:01:33 +03:00

213 lines
7.5 KiB
Dart

import 'dart:io';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart';
import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart';
import 'package:cw_bitcoin/bitcoin_wallet_creation_credentials.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/payjoin_session.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_service.dart';
import 'package:cw_bitcoin/bitcoin_wallet.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:hive/hive.dart';
import 'package:collection/collection.dart';
import 'package:bip39/bip39.dart' as bip39;
class BitcoinWalletService extends WalletService<
BitcoinNewWalletCredentials,
BitcoinRestoreWalletFromSeedCredentials,
BitcoinWalletFromKeysCredentials,
BitcoinRestoreWalletFromHardware> {
BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource,
this.payjoinSessionSource, this.isDirect);
final Box<WalletInfo> walletInfoSource;
final Box<UnspentCoinsInfo> unspentCoinsInfoSource;
final Box<PayjoinSession> payjoinSessionSource;
final bool isDirect;
@override
WalletType getType() => WalletType.bitcoin;
@override
Future<BitcoinWallet> create(BitcoinNewWalletCredentials credentials, {bool? isTestnet}) async {
final network = isTestnet == true ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet;
credentials.walletInfo?.network = network.value;
final String mnemonic;
switch ( credentials.walletInfo?.derivationInfo?.derivationType) {
case DerivationType.bip39:
final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
mnemonic = credentials.mnemonic ?? await MnemonicBip39.generate(strength: strength);
break;
case DerivationType.electrum:
default:
mnemonic = await generateElectrumMnemonic();
break;
}
final wallet = await BitcoinWalletBase.create(
mnemonic: mnemonic,
password: credentials.password!,
passphrase: credentials.passphrase,
walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource,
payjoinBox: payjoinSessionSource,
network: network,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.save();
await wallet.init();
return wallet;
}
@override
Future<bool> isWalletExit(String name) async =>
File(await pathForWallet(name: name, type: getType())).existsSync();
@override
Future<BitcoinWallet> openWallet(String name, String password) async {
final walletInfo = walletInfoSource.values
.firstWhereOrNull((info) => info.id == WalletBase.idFor(name, getType()))!;
try {
final wallet = await BitcoinWalletBase.open(
password: password,
name: name,
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,
payjoinBox: payjoinSessionSource,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.init();
saveBackup(name);
return wallet;
} catch (_) {
await restoreWalletFilesFromBackup(name);
final wallet = await BitcoinWalletBase.open(
password: password,
name: name,
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,
payjoinBox: payjoinSessionSource,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.init();
return wallet;
}
}
@override
Future<void> remove(String wallet) async {
File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true);
final walletInfo = walletInfoSource.values
.firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!;
await walletInfoSource.delete(walletInfo.key);
final unspentCoinsToDelete = unspentCoinsInfoSource.values.where(
(unspentCoin) => unspentCoin.walletId == walletInfo.id).toList();
final keysToDelete = unspentCoinsToDelete.map((unspentCoin) => unspentCoin.key).toList();
if (keysToDelete.isNotEmpty) {
await unspentCoinsInfoSource.deleteAll(keysToDelete);
}
}
@override
Future<void> rename(String currentName, String password, String newName) async {
final currentWalletInfo = walletInfoSource.values
.firstWhereOrNull((info) => info.id == WalletBase.idFor(currentName, getType()))!;
final currentWallet = await BitcoinWalletBase.open(
password: password,
name: currentName,
walletInfo: currentWalletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,
payjoinBox: payjoinSessionSource,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await currentWallet.renameWalletFiles(newName);
await saveBackup(newName);
final newWalletInfo = currentWalletInfo;
newWalletInfo.id = WalletBase.idFor(newName, getType());
newWalletInfo.name = newName;
await walletInfoSource.put(currentWalletInfo.key, newWalletInfo);
}
@override
Future<BitcoinWallet> restoreFromHardwareWallet(BitcoinRestoreWalletFromHardware credentials,
{bool? isTestnet}) async {
final network = isTestnet == true ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet;
credentials.walletInfo?.network = network.value;
credentials.walletInfo?.derivationInfo?.derivationPath =
credentials.hwAccountData.derivationPath;
final wallet = await BitcoinWallet(
password: credentials.password!,
xpub: credentials.hwAccountData.xpub,
walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource,
networkParam: network,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
payjoinBox: payjoinSessionSource,
);
await wallet.save();
await wallet.init();
return wallet;
}
@override
Future<BitcoinWallet> restoreFromKeys(BitcoinWalletFromKeysCredentials credentials,
{bool? isTestnet}) async {
final network = isTestnet == true ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet;
credentials.walletInfo?.network = network.value;
final wallet = await BitcoinWallet(
password: credentials.password!,
xpub: credentials.xpub,
walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource,
networkParam: network,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
payjoinBox: payjoinSessionSource,
);
await wallet.save();
await wallet.init();
return wallet;
}
@override
Future<BitcoinWallet> restoreFromSeed(BitcoinRestoreWalletFromSeedCredentials credentials,
{bool? isTestnet}) async {
if (!validateMnemonic(credentials.mnemonic) && !bip39.validateMnemonic(credentials.mnemonic)) {
throw BitcoinMnemonicIsIncorrectException();
}
final network = isTestnet == true ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet;
credentials.walletInfo?.network = network.value;
final wallet = await BitcoinWalletBase.create(
password: credentials.password!,
passphrase: credentials.passphrase,
mnemonic: credentials.mnemonic,
walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource,
payjoinBox: payjoinSessionSource,
network: network,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.save();
await wallet.init();
return wallet;
}
}