import 'dart:convert'; import 'package:bip39/bip39.dart' as bip39; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/address_from_output.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/electrum_wallet_snapshot.dart'; import 'package:cw_bitcoin/payjoin/manager.dart'; import 'package:cw_bitcoin/payjoin/storage.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; import 'package:cw_bitcoin/psbt/signer.dart'; import 'package:cw_bitcoin/psbt/transaction_builder.dart'; import 'package:cw_bitcoin/psbt/v0_deserialize.dart'; import 'package:cw_bitcoin/psbt/v0_finalizer.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/payjoin_session.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_keys_file.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:ledger_bitcoin/ledger_bitcoin.dart'; import 'package:ledger_bitcoin/psbt.dart'; import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; import 'package:mobx/mobx.dart'; part 'bitcoin_wallet.g.dart'; class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet; abstract class BitcoinWalletBase extends ElectrumWallet with Store { BitcoinWalletBase({ required String password, required WalletInfo walletInfo, required Box unspentCoinsInfo, required Box payjoinBox, required EncryptionFileUtils encryptionFileUtils, Uint8List? seedBytes, String? mnemonic, String? xpub, String? addressPageType, BasedUtxoNetwork? networkParam, List? initialAddresses, ElectrumBalance? initialBalance, Map? initialRegularAddressIndex, Map? initialChangeAddressIndex, String? passphrase, List? initialSilentAddresses, int initialSilentAddressIndex = 0, bool? alwaysScan, }) : super( mnemonic: mnemonic, passphrase: passphrase, xpub: xpub, password: password, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, network: networkParam == null ? BitcoinNetwork.mainnet : networkParam == BitcoinNetwork.mainnet ? BitcoinNetwork.mainnet : BitcoinNetwork.testnet, initialAddresses: initialAddresses, initialBalance: initialBalance, seedBytes: seedBytes, encryptionFileUtils: encryptionFileUtils, currency: networkParam == BitcoinNetwork.testnet ? CryptoCurrency.tbtc : CryptoCurrency.btc, alwaysScan: alwaysScan, ) { // in a standard BIP44 wallet, mainHd derivation path = m/84'/0'/0'/0 (account 0, index unspecified here) // the sideHd derivation path = m/84'/0'/0'/1 (account 1, index unspecified here) // String derivationPath = walletInfo.derivationInfo!.derivationPath!; // String sideDerivationPath = derivationPath.substring(0, derivationPath.length - 1) + "1"; // final hd = bitcoin.HDWallet.fromSeed(seedBytes, network: networkType); payjoinManager = PayjoinManager(PayjoinStorage(payjoinBox), this); walletAddresses = BitcoinWalletAddresses(walletInfo, initialAddresses: initialAddresses, initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, initialSilentAddresses: initialSilentAddresses, initialSilentAddressIndex: initialSilentAddressIndex, mainHd: hd, sideHd: accountHD.childKey(Bip32KeyIndex(1)), network: networkParam ?? network, masterHd: seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null, isHardwareWallet: walletInfo.isHardwareWallet, payjoinManager: payjoinManager); autorun((_) { this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; }); } @override bool get hasRescan => true; static Future create({ required String mnemonic, required String password, required WalletInfo walletInfo, required Box unspentCoinsInfo, required Box payjoinBox, required EncryptionFileUtils encryptionFileUtils, String? passphrase, String? addressPageType, BasedUtxoNetwork? network, List? initialAddresses, List? initialSilentAddresses, ElectrumBalance? initialBalance, Map? initialRegularAddressIndex, Map? initialChangeAddressIndex, int initialSilentAddressIndex = 0, }) async { late Uint8List seedBytes; switch (walletInfo.derivationInfo?.derivationType) { case DerivationType.bip39: seedBytes = await bip39.mnemonicToSeed( mnemonic, passphrase: passphrase ?? "", ); break; case DerivationType.electrum: default: seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); break; } return BitcoinWallet( mnemonic: mnemonic, passphrase: passphrase ?? "", password: password, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, initialAddresses: initialAddresses, initialSilentAddresses: initialSilentAddresses, initialSilentAddressIndex: initialSilentAddressIndex, initialBalance: initialBalance, encryptionFileUtils: encryptionFileUtils, seedBytes: seedBytes, initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, addressPageType: addressPageType, networkParam: network, payjoinBox: payjoinBox, ); } static Future open({ required String name, required WalletInfo walletInfo, required Box unspentCoinsInfo, required Box payjoinBox, required String password, required EncryptionFileUtils encryptionFileUtils, required bool alwaysScan, }) async { final network = walletInfo.network != null ? BasedUtxoNetwork.fromName(walletInfo.network!) : BitcoinNetwork.mainnet; final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type); ElectrumWalletSnapshot? snp = null; try { snp = await ElectrumWalletSnapshot.load( encryptionFileUtils, name, walletInfo.type, password, network, ); } catch (e) { if (!hasKeysFile) rethrow; } final WalletKeysData keysData; // Migrate wallet from the old scheme to then new .keys file scheme if (!hasKeysFile) { keysData = WalletKeysData( mnemonic: snp!.mnemonic, xPub: snp.xpub, passphrase: snp.passphrase, ); } else { keysData = await WalletKeysFile.readKeysFile( name, walletInfo.type, password, encryptionFileUtils, ); } walletInfo.derivationInfo ??= DerivationInfo(); // set the default if not present: walletInfo.derivationInfo!.derivationPath ??= snp?.derivationPath ?? electrum_path; walletInfo.derivationInfo!.derivationType ??= snp?.derivationType ?? DerivationType.electrum; Uint8List? seedBytes = null; final mnemonic = keysData.mnemonic; final passphrase = keysData.passphrase; if (mnemonic != null) { switch (walletInfo.derivationInfo!.derivationType) { case DerivationType.electrum: seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); break; case DerivationType.bip39: default: seedBytes = await bip39.mnemonicToSeed( mnemonic, passphrase: passphrase ?? '', ); break; } } return BitcoinWallet( mnemonic: mnemonic, xpub: keysData.xPub, password: password, passphrase: passphrase, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, initialAddresses: snp?.addresses, initialSilentAddresses: snp?.silentAddresses, initialSilentAddressIndex: snp?.silentAddressIndex ?? 0, initialBalance: snp?.balance, encryptionFileUtils: encryptionFileUtils, seedBytes: seedBytes, initialRegularAddressIndex: snp?.regularAddressIndex, initialChangeAddressIndex: snp?.changeAddressIndex, addressPageType: snp?.addressPageType, networkParam: network, alwaysScan: alwaysScan, payjoinBox: payjoinBox); } LedgerConnection? _ledgerConnection; BitcoinLedgerApp? _bitcoinLedgerApp; @override void setLedgerConnection(LedgerConnection connection) { _ledgerConnection = connection; _bitcoinLedgerApp = BitcoinLedgerApp(_ledgerConnection!, derivationPath: walletInfo.derivationInfo!.derivationPath!); } late final PayjoinManager payjoinManager; bool get isPayjoinAvailable => unspentCoinsInfo.values .where((element) => element.walletId == id && element.isSending && !element.isFrozen) .isNotEmpty; Future buildPsbt({ required List outputs, required BigInt fee, required BasedUtxoNetwork network, required List utxos, required Map publicKeys, required Uint8List masterFingerprint, String? memo, bool enableRBF = false, BitcoinOrdering inputOrdering = BitcoinOrdering.bip69, BitcoinOrdering outputOrdering = BitcoinOrdering.bip69, }) async { final psbtReadyInputs = []; for (final utxo in utxos) { final rawTx = await electrumClient.getTransactionHex(hash: utxo.utxo.txHash); final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!; psbtReadyInputs.add(PSBTReadyUtxoWithAddress( utxo: utxo.utxo, rawTx: rawTx, ownerDetails: utxo.ownerDetails, ownerDerivationPath: publicKeyAndDerivationPath.derivationPath, ownerMasterFingerprint: masterFingerprint, ownerPublicKey: publicKeyAndDerivationPath.publicKey, )); } return PSBTTransactionBuild(inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF) .psbt; } @override Future buildHardwareWalletTransaction({ required List outputs, required BigInt fee, required BasedUtxoNetwork network, required List utxos, required Map publicKeys, String? memo, bool enableRBF = false, BitcoinOrdering inputOrdering = BitcoinOrdering.bip69, BitcoinOrdering outputOrdering = BitcoinOrdering.bip69, }) async { final masterFingerprint = await _bitcoinLedgerApp!.getMasterFingerprint(); final psbt = await buildPsbt( outputs: outputs, fee: fee, network: network, utxos: utxos, publicKeys: publicKeys, masterFingerprint: masterFingerprint, memo: memo, enableRBF: enableRBF, inputOrdering: inputOrdering, outputOrdering: outputOrdering, ); final rawHex = await _bitcoinLedgerApp!.signPsbt(psbt: psbt); return BtcTransaction.fromRaw(BytesUtils.toHexString(rawHex)); } @override Future createTransaction(Object credentials) async { credentials = credentials as BitcoinTransactionCredentials; final tx = (await super.createTransaction(credentials)) as PendingBitcoinTransaction; final payjoinUri = credentials.payjoinUri; if (payjoinUri == null) return tx; final transaction = await buildPsbt( utxos: tx.utxos, outputs: tx.outputs .map((e) => BitcoinOutput( address: addressFromScript(e.scriptPubKey), value: e.amount, isSilentPayment: e.isSilentPayment, isChange: e.isChange, )) .toList(), fee: BigInt.from(tx.fee), network: network, memo: credentials.outputs.first.memo, outputOrdering: BitcoinOrdering.none, enableRBF: true, publicKeys: tx.publicKeys!, masterFingerprint: Uint8List(0)); final originalPsbt = await signPsbt(base64.encode(transaction.asPsbtV0()), getUtxoWithPrivateKeys()); tx.commitOverride = () async { final sender = await payjoinManager.initSender(payjoinUri, originalPsbt, int.parse(tx.feeRate)); payjoinManager.spawnNewSender( sender: sender, pjUrl: payjoinUri, amount: BigInt.from(tx.amount)); }; return tx; } List getUtxoWithPrivateKeys() => unspentCoins .where((e) => (e.isSending && !e.isFrozen)) .map((unspent) => UtxoWithPrivateKey.fromUnspent(unspent, this)) .toList(); Future commitPsbt(String finalizedPsbt) { final psbt = PsbtV2()..deserializeV0(base64.decode(finalizedPsbt)); final btcTx = BtcTransaction.fromRaw(BytesUtils.toHexString(psbt.extract())); return PendingBitcoinTransaction( btcTx, type, electrumClient: electrumClient, amount: 0, fee: 0, feeRate: "", network: network, hasChange: true, ).commit(); } Future signPsbt(String preProcessedPsbt, List utxos) async { final psbt = PsbtV2()..deserializeV0(base64Decode(preProcessedPsbt)); await psbt.signWithUTXO(utxos, (txDigest, utxo, key, sighash) { return utxo.utxo.isP2tr ? key.signTapRoot( txDigest, sighash: sighash, tweak: utxo.utxo.isSilentPayment != true, ) : key.signInput(txDigest, sigHash: sighash); }, (txId, vout) async { final txHex = await electrumClient.getTransactionHex(hash: txId); final output = BtcTransaction.fromRaw(txHex).outputs[vout]; return TaprootAmountScriptPair(output.amount, output.scriptPubKey); }); psbt.finalizeV0(); return base64Encode(psbt.asPsbtV0()); } @override Future signMessage(String message, {String? address = null}) async { if (walletInfo.isHardwareWallet) { final addressEntry = address != null ? walletAddresses.allAddresses.firstWhere((element) => element.address == address) : null; final index = addressEntry?.index ?? 0; final isChange = addressEntry?.isHidden == true ? 1 : 0; final accountPath = walletInfo.derivationInfo?.derivationPath; final derivationPath = accountPath != null ? "$accountPath/$isChange/$index" : null; final signature = await _bitcoinLedgerApp! .signMessage(message: ascii.encode(message), signDerivationPath: derivationPath); return base64Encode(signature); } return super.signMessage(message, address: address); } }