Files
hash-wallet/cw_bitcoin/lib/bitcoin_wallet.dart
T
d7ebf42814 Cw 1551 quick bitcoin wallet sync (#3446)
* add Wallet Accounts page and integrate UI

* account model refactor

* add walletInfoAccount table and API

* add WalletAccountList API

* add multi-account support for Electrum wallets

* add Bitcoin lightning card and card style handling

* refine account filtering and card selection

* update current account balance on wallet changes

* update configure.dar

* Use named parameters for card design

* Add Bitcoin account handling and refactor lookup

* refactor dashboard

* refactor cards UI and account balance handling

* Improve Bitcoin account selection and balances

* restrict unspent coin usage to current account

* filter transactions by current Bitcoin account

* refactor electrum wallet

* refactor address generation

* add accountIndex to address fetch logic

* Update configure.dart

* reload transactions on Bitcoin account change

* restore multiple BTC accounts and improve fetch

* show account balance with in btc

* track account index in Bitcoin transactions

* discover btc BIP39 accounts during restore

* fix account switching and card ordering bugs

* fix card customizer

* limit account name length in modal

* update BTC account list balance display

* auto-reformat

* formating fix

* skip HD map validation

* fix 0 balance issue

* minor fix

* quick bitcoin wallet sync

* restore shuffle change output

* restore address lookup fix

* Update electrum.dart

* remove account customizer

* Merge branch 'CW-1142-Add-accounts-feature-to-BTC' into CW-1551-Quick-Bitcoin-Wallet-Sync

* probe only segwit in account discovery

* add account discovery limit and tracking

* Improve Electrum sync account handling [skip ci]

* remove legacy monero account and address list UI

* rework BTC account balance refres

* avoid unnecessary BTC account reloads

* refactor account header in addresses page

* fix blank popup when editing an account

* fix: dedupe UTXO balance

* minor fix

* add Bitcoin current account API

* Apply batched suggestions from code review [skip ci]

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* address PR review comments [skip ci]

* remove account customizer and fix accounts init

* restrict extra BTC accounts to SegWit

* add BTC multi-account toggle

* Fix BTC sync gating and account balance update

* minor fix

* Update electrum_wallet.dart

* store selected account on WalletInfo

* Refine wallet account reset visibility

* refactor BTC address prep and move accounts page

* refactor account stack and customizer routing

* migrate legacy Lightning card styles

* unify dashboard account change state

* fix account routing and Electrum balances

* fix account setup and wallet cards UI

* disable Bitcoin Accounts for all hardware

* merge conflicts fix

* minor fixes

* update cards_view [skip ci]

* reverted cards view update [skip ci]

* fix asset name

* moved the transaction filtering into the electrum wallet

* localization

* disable accounts for watch-only wallets

* fix: stop card render from switching the wallet account

* fix: restore Lightning transactions in tx history

* minor fix

* fix: skip duplicate legacy address generation [skip ci]

* fix: correct account balance/design mismatch bugs

* addressing review comments

---------

Co-authored-by: Robert Malikowski <malikowskirobert@gmail.com>
Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
2026-10-03 00:44:57 +02:00

749 lines
26 KiB
Dart

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/.secrets.g.dart' as secrets;
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_receive_page_option.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_transaction_info.dart';
import 'package:cw_bitcoin/electrum_wallet.dart';
import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
import 'package:cw_bitcoin/electrum_wallet_snapshot.dart';
import 'package:cw_bitcoin/locktime.dart';
import 'package:cw_bitcoin/hardware/bitcoin_hardware_wallet_service.dart';
import 'package:cw_bitcoin/lightning/lightning_wallet.dart';
import 'package:cw_bitcoin/hardware/bitcoin_ledger_service.dart';
import 'package:cw_bitcoin/output_ordering.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/amount/money.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/output_info.dart';
import 'package:cw_core/payjoin_session.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_core/sync_status.dart';
import "package:cw_core/receive_page_option.dart";
import 'package:cw_core/unspent_coin_type.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/utils/zpub.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/psbt.dart';
import 'package:mobx/mobx.dart';
import 'package:ur/cbor_lite.dart';
import 'package:ur/ur.dart';
import 'package:ur/ur_decoder.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 DerivationInfo derivationInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo,
required Box<PayjoinSession> payjoinBox,
required EncryptionFileUtils encryptionFileUtils,
Uint8List? seedBytes,
String? mnemonic,
String? xpub,
String? addressPageType,
BasedUtxoNetwork? networkParam,
List<BitcoinAddressRecord>? initialAddresses,
ElectrumBalance? initialBalance,
ElectrumBalance? initialLightningBalance,
Map<String, int>? initialRegularAddressIndex,
Map<String, int>? initialChangeAddressIndex,
String? passphrase,
List<BitcoinSilentPaymentAddressRecord>? initialSilentAddresses,
int initialSilentAddressIndex = 0,
bool? alwaysScan,
bool? useLightning,
String? cachedLightningAddress,
}) : super(
mnemonic: mnemonic,
passphrase: passphrase,
xpub: xpub,
password: password,
walletInfo: walletInfo,
derivationInfo: derivationInfo,
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,
useLightning: useLightning ?? true,
) {
// 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);
if (mnemonic != null && this.useLightning && LightningWallet.isAvailable) {
try {
lightningWallet = LightningWallet(
mnemonic: mnemonic,
passphrase: passphrase,
seedBytes: seedBytes,
apiKey: secrets.breezApiKey,
lnurlDomain: "cake.cash",
cachedAddress: cachedLightningAddress,
);
} catch (e) {
printV(e);
}
}
payjoinManager = PayjoinManager(PayjoinStorage(payjoinBox), this);
walletAddresses = BitcoinWalletAddresses(
walletInfo,
initialAddresses: initialAddresses,
initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex,
initialSilentAddresses: initialSilentAddresses,
initialSilentAddressIndex: initialSilentAddressIndex,
mainHdByTypeAndAccount: mainHdByTypeAndAccount,
sideHdByTypeAndAccount: sideHdByTypeAndAccount,
accountIndexes: [currentAccountIndex],
currentAccountIndex: currentAccountIndex,
legacyMainHd: mainHd,
legacySideHd: sideHd,
network: networkParam ?? network,
masterHd: seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null,
isHardwareWallet: walletInfo.isHardwareWallet,
payjoinManager: payjoinManager,
lightningWallet: lightningWallet,
);
if (lightningWallet != null) {
walletAddresses.setLightningAddress(walletInfo.name);
}
autorun((_) {
this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
});
reaction((_) => this.useLightning, (bool useLightning) {
if (useLightning && LightningWallet.isAvailable) {
if (mnemonic != null) {
lightningWallet = LightningWallet(
mnemonic: mnemonic,
passphrase: passphrase,
seedBytes: seedBytes,
apiKey: secrets.breezApiKey,
lnurlDomain: "cake.cash",
cachedAddress: cachedLightningAddress,
);
walletAddresses.setLightningAddress(walletInfo.name);
}
} else {
lightningWallet = null;
}
});
if (initialLightningBalance != null) {
balance[CryptoCurrency.btcln] = initialLightningBalance;
}
}
bool get isLightningInitialized => lightningWallet?.isInitialized == true;
@override
bool get hasRescan => true;
static Future<BitcoinWallet> create({
required String mnemonic,
required String password,
required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo,
required Box<PayjoinSession> payjoinBox,
required EncryptionFileUtils encryptionFileUtils,
String? passphrase,
String? addressPageType,
BasedUtxoNetwork? network,
List<BitcoinAddressRecord>? initialAddresses,
List<BitcoinSilentPaymentAddressRecord>? initialSilentAddresses,
ElectrumBalance? initialBalance,
Map<String, int>? initialRegularAddressIndex,
Map<String, int>? initialChangeAddressIndex,
int initialSilentAddressIndex = 0,
}) async {
late Uint8List seedBytes;
final derivationInfo = await walletInfo.getDerivationInfo();
switch (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,
derivationInfo: derivationInfo,
unspentCoinsInfo: unspentCoinsInfo,
initialAddresses: initialAddresses,
initialSilentAddresses: initialSilentAddresses,
initialSilentAddressIndex: initialSilentAddressIndex,
initialBalance: initialBalance,
encryptionFileUtils: encryptionFileUtils,
seedBytes: seedBytes,
initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex,
addressPageType: addressPageType,
networkParam: network,
payjoinBox: payjoinBox,
useLightning: true,
);
}
static Future<BitcoinWallet> open({
required String name,
required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo,
required Box<PayjoinSession> payjoinBox,
required String password,
required EncryptionFileUtils encryptionFileUtils,
}) 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,
);
}
final derivationInfo = await walletInfo.getDerivationInfo();
// set the default if not present:
derivationInfo.derivationPath ??= snp?.derivationPath ?? electrum_path;
derivationInfo.derivationType ??= snp?.derivationType ?? DerivationType.electrum;
if (derivationInfo.derivationType == DerivationType.unknown) {
if (snp?.derivationPath == electrum_path || snp?.derivationType == DerivationType.electrum) {
derivationInfo.derivationPath = electrum_path;
derivationInfo.derivationType = DerivationType.electrum;
} else {
derivationInfo.derivationPath = segwit_path;
derivationInfo.derivationType = DerivationType.bip39;
}
}
await derivationInfo.save();
Uint8List? seedBytes = null;
final mnemonic = keysData.mnemonic;
final passphrase = keysData.passphrase;
if (mnemonic != null) {
switch (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 != null ? convertZpubToXpub(keysData.xPub!) : null,
password: password,
passphrase: passphrase,
walletInfo: walletInfo,
derivationInfo: derivationInfo,
unspentCoinsInfo: unspentCoinsInfo,
initialAddresses: snp?.addresses,
initialSilentAddresses: snp?.silentAddresses,
initialSilentAddressIndex: snp?.silentAddressIndex ?? 0,
initialBalance: snp?.balance,
initialLightningBalance: snp?.lightningBalance,
encryptionFileUtils: encryptionFileUtils,
seedBytes: seedBytes,
initialRegularAddressIndex: snp?.regularAddressIndex,
initialChangeAddressIndex: snp?.changeAddressIndex,
addressPageType: snp?.addressPageType,
networkParam: network,
alwaysScan: snp?.alwaysScan,
useLightning: snp?.useLightning,
cachedLightningAddress: snp?.cachedLightningAddress,
payjoinBox: payjoinBox,
);
}
@override
Future<void> close({bool shouldCleanup = false}) async {
payjoinManager.cleanupSessions();
await lightningWallet?.close();
super.close(shouldCleanup: shouldCleanup);
}
@override
Future<ElectrumBalance> fetchBalances() async {
final balance = await super.fetchBalances();
if (!isLightningInitialized || lightningWallet == null) {
return balance;
}
try {
final lBalance = await lightningWallet!.getBalance();
this.balance[CryptoCurrency.btcln] = ElectrumBalance(
confirmed: lBalance,
unconfirmed: Money.zero(CryptoCurrency.btcln),
frozen: Money.zero(CryptoCurrency.btcln));
} catch (e) {
printV("Error fetching lightning balance: $e");
}
return ElectrumBalance(
confirmed: balance.confirmed,
unconfirmed: balance.unconfirmed,
frozen: balance.frozen ?? Money.zero(currency),
);
}
@override
@action
Future<void> subscribeForUpdates() async {
if (isLightningInitialized && lightningWallet != null) {
lightningWallet!.setEventListener(
onTransactionEvent: (tx) async {
if (transactionHistory.transactions[tx.id]?.isPending != tx.isPending) {
transactionHistory.addOne(tx);
await transactionHistory.save();
await fetchBalances();
}
},
onCreateDepositTransactionEvent: (txs) async {
if (txs.isNotEmpty) {
transactionHistory.addMany(txs);
await transactionHistory.save();
}
},
onUpdateDepositTransactionEvent: (txs) async {
if (txs.isNotEmpty) {
txs.forEach((tx) => transactionHistory.transactions.remove(tx.id));
await transactionHistory.save();
}
},
onBalanceChangedEvent: fetchBalances,
);
}
return super.subscribeForUpdates();
}
@override
Future<Map<String, ElectrumTransactionInfo>> fetchTransactions() async {
if (lightningWallet != null) {
final existingTx = transactionHistory.transactions.values
.where((e) => (e.additionalInfo["isLightning"] as bool?) == true)
.lastOrNull;
lightningWallet!.getTransactionHistory(fromDate: existingTx?.date).then((lnHistory) async {
transactionHistory.addMany(lnHistory);
await transactionHistory.save();
}).onError((_, __) {});
}
return super.fetchTransactions();
}
LightningWallet? lightningWallet;
late final PayjoinManager payjoinManager;
@override
bool get hasPayjoinSupport => keys.privateKey.isNotEmpty;
@override
bool get hasLightningSupport => lightningWallet?.sdk != null;
bool get isPayjoinAvailable => unspentCoinsInfo.values
.where((element) => element.walletId == id && element.isSending && !element.isFrozen)
.isNotEmpty;
Future<PsbtV2> buildPsbt({
required List<BitcoinOutput> outputs,
required List<OutputInfo> cwOutputs,
required BigInt fee,
required BasedUtxoNetwork network,
required List<UtxoWithAddress> utxos,
required Map<String, PublicKeyWithDerivationPath> publicKeys,
required Uint8List masterFingerprint,
String? memo,
bool enableRBF = false,
BitcoinOrdering inputOrdering = BitcoinOrdering.bip69,
BitcoinOrdering outputOrdering = BitcoinOrdering.bip69,
}) async {
final psbtReadyInputs = <PSBTReadyUtxoWithAddress>[];
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,
),
);
}
final psbtReadyOutputs = outputs.map((o) {
final cwOutput = cwOutputs
.where(
(e) => [e.address, e.extractedAddress]
.map((e) => e?.toLowerCase())
.contains(o.address.toAddress().toLowerCase()),
)
.firstOrNull;
if (o.isChange && publicKeys.containsKey(o.address.pubKeyHash())) {
final changeKey = publicKeys[o.address.pubKeyHash()]!;
return PSBTReadyBitcoinOutput(
address: o.address,
value: o.value,
isSilentPayment: o.isSilentPayment,
isChange: o.isChange,
changeMasterFingerprint: masterFingerprint,
changeDerivationPath: changeKey.derivationPath,
changePublicKey: changeKey.publicKey,
outputInfo: cwOutput,
);
}
return PSBTReadyBitcoinOutput(
address: o.address,
value: o.value,
isSilentPayment: o.isSilentPayment,
isChange: o.isChange,
outputInfo: cwOutput,
);
}).toList();
final locktime = antiFeeSnipingLocktime(
chainTip: await getCurrentChainTip(),
synced: syncStatus is SyncedSyncStatus,
);
return PSBTTransactionBuild(
inputs: psbtReadyInputs,
outputs: psbtReadyOutputs,
enableRBF: enableRBF,
locktime: locktime,
).psbt;
}
@override
Future<BtcTransaction> buildHardwareWalletTransaction({
required List<BitcoinOutput> outputs,
required BigInt fee,
required BasedUtxoNetwork network,
required List<UtxoWithAddress> utxos,
required List<OutputInfo> cwOutputs,
required Map<String, PublicKeyWithDerivationPath> publicKeys,
String? memo,
bool enableRBF = false,
BitcoinOrdering inputOrdering = BitcoinOrdering.bip69,
BitcoinOrdering outputOrdering = BitcoinOrdering.bip69,
}) async {
final masterFingerprint =
await (hardwareWalletService as BitcoinHardwareWalletService).getMasterFingerprint();
final orderedOutputs = orderOutputs(outputs, outputOrdering);
final psbt = await buildPsbt(
outputs: orderedOutputs,
fee: fee,
network: network,
utxos: utxos,
cwOutputs: cwOutputs,
publicKeys: publicKeys,
masterFingerprint: masterFingerprint,
memo: memo,
enableRBF: enableRBF,
inputOrdering: inputOrdering,
// Already applied above; don't reorder again.
outputOrdering: BitcoinOrdering.none,
);
final psbtStr = base64Encode(psbt.serialize());
if (hardwareWalletService is BitcoinLedgerService && derivationInfo.derivationPath != null) {
(hardwareWalletService as BitcoinLedgerService)
.setAccountDerivationPath(derivationInfo.derivationPath!);
}
final rawHex = await hardwareWalletService!.signTransaction(transaction: psbtStr);
return BtcTransaction.fromRaw(BytesUtils.toHexString(rawHex));
}
@override
Future<PendingTransaction> createTransaction(Object credentials) async {
credentials = credentials as BitcoinTransactionCredentials;
final lnAddr = credentials.outputs.first.isParsedAddress
? credentials.outputs.first.extractedAddress!
: credentials.outputs.first.address;
final isLNCompatible = await lightningWallet?.isCompatible(lnAddr);
if ((credentials.coinTypeToSpendFrom == UnspentCoinType.lightning && lightningWallet != null) ||
isLNCompatible == true) {
Money amount;
if (credentials.outputs.first.sendAll) {
amount = await lightningWallet!.getBalance();
} else {
amount = credentials.outputs.first.cryptoAmount;
}
return lightningWallet!.createTransaction(
lnAddr,
amount.amount > BigInt.zero ? amount.amount : null,
credentials.priority,
credentials.outputs.first.sendAll,
);
}
final tx = (await super.createTransaction(credentials)) as PendingBitcoinTransaction;
final payjoinUri = credentials.payjoinUri;
if (payjoinUri == null && !tx.shouldCommitUR()) 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(),
cwOutputs: credentials.outputs,
fee: tx.fee.amount,
network: network,
memo: credentials.outputs.first.memo,
outputOrdering: BitcoinOrdering.none,
enableRBF: true,
publicKeys: tx.publicKeys!,
masterFingerprint: Uint8List.fromList([0, 0, 0, 0]));
if (tx.shouldCommitUR()) {
tx.unsignedPsbt = transaction.asPsbtV0();
return tx;
}
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: tx.amount.amount);
};
return tx;
}
List<UtxoWithPrivateKey> getUtxoWithPrivateKeys({bool confirmedOnly = false}) =>
unspentCoinsForCurrentAccount
.where(
(e) => e.isSending && !e.isFrozen && (!confirmedOnly || (e.confirmations ?? 0) > 0))
.map((unspent) => UtxoWithPrivateKey.fromUnspent(unspent, this))
.toList();
Future<void> commitPsbt(String finalizedPsbt) {
final psbt = PsbtV2()..deserializeV0(base64.decode(finalizedPsbt));
final btcTx = BtcTransaction.fromRaw(BytesUtils.toHexString(psbt.extract()));
return PendingBitcoinTransaction(
btcTx,
type,
accountIndex: currentAccountIndex,
electrumClient: electrumClient,
amount: Money.zero(currency),
fee: Money.zero(currency),
feeRate: "",
network: network,
hasChange: true,
isViewOnly: false,
).commit();
}
Future<String> signPsbt(String preProcessedPsbt, List<UtxoWithPrivateKey> 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());
}
Future<void> commitPsbtUR(List<String> urCodes) async {
if (urCodes.isEmpty) throw Exception("No QR code got scanned");
bool isUr = urCodes.any((str) {
return str.startsWith("ur:psbt/");
});
if (isUr) {
final ur = URDecoder();
for (final inp in urCodes) {
ur.receivePart(inp);
}
final result = (ur.result as UR);
final cbor = result.cbor;
final cborDecoder = CBORDecoder(cbor);
final out = cborDecoder.decodeBytes();
final bytes = out.$1;
final base64psbt = base64Encode(bytes);
final psbt = PsbtV2()..deserializeV0(base64Decode(base64psbt));
// psbt.finalize();
final finalized = base64Encode(psbt.serialize());
await commitPsbt(finalized);
} else {
final btcTx = BtcTransaction.fromRaw(urCodes.first);
return PendingBitcoinTransaction(
btcTx,
type,
accountIndex: currentAccountIndex,
electrumClient: electrumClient,
amount: Money.zero(currency),
fee: Money.zero(currency),
feeRate: "",
network: network,
hasChange: true,
isViewOnly: false,
).commit();
}
}
@override
Future<String> 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 derivationInfo = await walletInfo.getDerivationInfo();
final accountPath = derivationInfo.derivationPath;
final derivationPath = accountPath != null ? "$accountPath/$isChange/$index" : null;
final signature = await hardwareWalletService!
.signMessage(message: ascii.encode(message), derivationPath: derivationPath);
return base64Encode(signature);
}
return super.signMessage(message, address: address);
}
@override
bool receiveOptionAvailable(ReceivePageOption option) {
final isExtraAccount = currentAccountIndex > 0;
if (option == BitcoinReceivePageOption.lightning) {
// Lightning is tied to the primary account only
return !isExtraAccount && hasLightningSupport;
}
if (option == BitcoinReceivePageOption.silent_payments) {
// Silent payments are tied to the primary account only
return !isExtraAccount && hasSilentPaymentsScanning;
}
// Restrict extra Bitcoin accounts (accountIndex > 0) to the allowed address types
if (isExtraAccount && option is BitcoinReceivePageOption) {
final addressType = option.toType();
if (!EXTRA_ACCOUNT_ADDRESS_TYPES.contains(addressType)) {
return false;
}
}
return true;
}
}