import 'dart:async';
import 'dart:convert';

import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
import 'package:cw_bitcoin/electrum_worker/methods/methods.dart';
import 'package:cw_bitcoin/exceptions.dart';
import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
import 'package:cw_bitcoin/psbt_transaction_builder.dart';
import 'package:cw_bitcoin/bitcoin_unspent.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_bitcoin/electrum_derivations.dart';
import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart';
import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/electrum_wallet.dart';
import 'package:cw_bitcoin/electrum_wallet_snapshot.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_direction.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/wallet_info.dart';
import 'package:cw_core/wallet_keys_file.dart';
import 'package:hive/hive.dart';
import 'package:ledger_bitcoin/ledger_bitcoin.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 {
  @observable
  bool nodeSupportsSilentPayments = true;
  @observable
  bool silentPaymentsScanningActive = false;
  @observable
  bool allowedToSwitchNodesForScanning = false;

  BitcoinWalletBase({
    required String password,
    required WalletInfo walletInfo,
    required Box<UnspentCoinsInfo> unspentCoinsInfo,
    required EncryptionFileUtils encryptionFileUtils,
    List<int>? seedBytes,
    String? mnemonic,
    String? xpub,
    String? addressPageType,
    BasedUtxoNetwork? networkParam,
    List<BitcoinAddressRecord>? initialAddresses,
    ElectrumBalance? initialBalance,
    Map<String, int>? initialRegularAddressIndex,
    Map<String, int>? initialChangeAddressIndex,
    String? passphrase,
    List<BitcoinSilentPaymentAddressRecord>? initialSilentAddresses,
    int initialSilentAddressIndex = 0,
    bool? alwaysScan,
    required bool mempoolAPIEnabled,
    super.hdWallets,
    super.initialUnspentCoins,
  }) : 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,
          mempoolAPIEnabled: mempoolAPIEnabled,
        ) {
    walletAddresses = BitcoinWalletAddresses(
      walletInfo,
      initialAddresses: initialAddresses,
      initialSilentAddresses: initialSilentAddresses,
      network: networkParam ?? network,
      isHardwareWallet: walletInfo.isHardwareWallet,
      hdWallets: hdWallets,
    );

    autorun((_) {
      this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
    });
  }

  static Future<BitcoinWallet> create({
    required String mnemonic,
    required String password,
    required WalletInfo walletInfo,
    required Box<UnspentCoinsInfo> unspentCoinsInfo,
    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,
    required bool mempoolAPIEnabled,
  }) async {
    List<int>? seedBytes = null;
    final Map<CWBitcoinDerivationType, Bip32Slip10Secp256k1> hdWallets = {};

    if (walletInfo.isRecovery) {
      for (final derivation in walletInfo.derivations ?? <DerivationInfo>[]) {
        if (derivation.description?.contains("SP") ?? false) {
          continue;
        }

        if (derivation.derivationType == DerivationType.bip39) {
          seedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase);
          hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromSeed(seedBytes);

          break;
        } else {
          try {
            seedBytes = ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase);
            hdWallets[CWBitcoinDerivationType.electrum] = Bip32Slip10Secp256k1.fromSeed(seedBytes);
          } catch (e) {
            printV("electrum_v2 seed error: $e");

            try {
              seedBytes = ElectrumV1SeedGenerator(mnemonic).generate();
              hdWallets[CWBitcoinDerivationType.electrum] =
                  Bip32Slip10Secp256k1.fromSeed(seedBytes);
            } catch (e) {
              printV("electrum_v1 seed error: $e");
            }
          }

          break;
        }
      }

      if (hdWallets[CWBitcoinDerivationType.bip39] != null) {
        hdWallets[CWBitcoinDerivationType.old_bip39] = hdWallets[CWBitcoinDerivationType.bip39]!;
      }
      if (hdWallets[CWBitcoinDerivationType.electrum] != null) {
        hdWallets[CWBitcoinDerivationType.old_electrum] =
            hdWallets[CWBitcoinDerivationType.electrum]!;
      }
    } else {
      switch (walletInfo.derivationInfo?.derivationType) {
        case DerivationType.bip39:
          seedBytes = await Bip39SeedGenerator.generateFromString(mnemonic, passphrase);
          hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromSeed(seedBytes);
          break;
        case DerivationType.electrum:
        default:
          seedBytes = await ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase);
          hdWallets[CWBitcoinDerivationType.electrum] = Bip32Slip10Secp256k1.fromSeed(seedBytes);
          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,
      hdWallets: hdWallets,
      initialRegularAddressIndex: initialRegularAddressIndex,
      initialChangeAddressIndex: initialChangeAddressIndex,
      addressPageType: addressPageType,
      networkParam: network,
      mempoolAPIEnabled: mempoolAPIEnabled,
      initialUnspentCoins: [],
    );
  }

  static Future<BitcoinWallet> open({
    required String name,
    required WalletInfo walletInfo,
    required Box<UnspentCoinsInfo> unspentCoinsInfo,
    required String password,
    required EncryptionFileUtils encryptionFileUtils,
    required bool alwaysScan,
    required bool mempoolAPIEnabled,
  }) 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;

    List<int>? seedBytes = null;
    final Map<CWBitcoinDerivationType, Bip32Slip10Secp256k1> hdWallets = {};
    final mnemonic = keysData.mnemonic;
    final passphrase = keysData.passphrase;

    if (mnemonic != null) {
      final derivations = walletInfo.derivations ?? <DerivationInfo>[];

      for (final derivation in derivations) {
        if (derivation.description?.contains("SP") ?? false) {
          continue;
        }

        if (derivation.derivationType == DerivationType.bip39) {
          seedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase);
          hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromSeed(seedBytes);

          break;
        } else {
          try {
            seedBytes = ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase);
            hdWallets[CWBitcoinDerivationType.electrum] = Bip32Slip10Secp256k1.fromSeed(seedBytes);
          } catch (e) {
            printV("electrum_v2 seed error: $e");

            try {
              seedBytes = ElectrumV1SeedGenerator(mnemonic).generate();
              hdWallets[CWBitcoinDerivationType.electrum] =
                  Bip32Slip10Secp256k1.fromSeed(seedBytes);
            } catch (e) {
              printV("electrum_v1 seed error: $e");
            }
          }

          break;
        }
      }

      if (hdWallets[CWBitcoinDerivationType.bip39] != null) {
        hdWallets[CWBitcoinDerivationType.old_bip39] = hdWallets[CWBitcoinDerivationType.bip39]!;
      }
      if (hdWallets[CWBitcoinDerivationType.electrum] != null) {
        hdWallets[CWBitcoinDerivationType.old_electrum] =
            hdWallets[CWBitcoinDerivationType.electrum]!;
      }

      if (derivations.isEmpty) {
        switch (walletInfo.derivationInfo?.derivationType) {
          case DerivationType.bip39:
            seedBytes = await Bip39SeedGenerator.generateFromString(mnemonic, passphrase);
            hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromSeed(seedBytes);
            break;
          case DerivationType.electrum:
          default:
            seedBytes = await ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase);
            hdWallets[CWBitcoinDerivationType.electrum] = Bip32Slip10Secp256k1.fromSeed(seedBytes);
            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,
      mempoolAPIEnabled: mempoolAPIEnabled,
      hdWallets: hdWallets,
      initialUnspentCoins: snp?.unspentCoins ?? [],
    );
  }

  Future<bool> getNodeIsElectrs() async {
    if (node?.isElectrs != null) {
      return node!.isElectrs!;
    }

    final isNamedElectrs = node?.uri.host.contains("electrs") ?? false;
    if (isNamedElectrs) {
      node!.isElectrs = true;
    }

    final isNamedFulcrum = node!.uri.host.contains("fulcrum");
    if (isNamedFulcrum) {
      node!.isElectrs = false;
    }

    if (node!.isElectrs == null) {
      final version = await sendWorker(ElectrumWorkerGetVersionRequest());

      if (version is List<String> && version.isNotEmpty) {
        final server = version[0];

        if (server.toLowerCase().contains('electrs')) {
          node!.isElectrs = true;
        }
      } else if (version is String && version.toLowerCase().contains('electrs')) {
        node!.isElectrs = true;
      } else {
        node!.isElectrs = false;
      }
    }

    node!.save();
    return node!.isElectrs!;
  }

  Future<bool> getNodeSupportsSilentPayments() async {
    if (node?.supportsSilentPayments != null) {
      return node!.supportsSilentPayments!;
    }

    // As of today (august 2024), only ElectrumRS supports silent payments
    final isElectrs = await getNodeIsElectrs();
    if (!isElectrs) {
      node!.supportsSilentPayments = false;
    }

    if (node!.supportsSilentPayments == null) {
      try {
        final workerResponse = (await sendWorker(ElectrumWorkerCheckTweaksRequest())) as String;
        final tweaksResponse = ElectrumWorkerCheckTweaksResponse.fromJson(
          json.decode(workerResponse) as Map<String, dynamic>,
        );
        final supportsScanning = tweaksResponse.result == true;

        if (supportsScanning) {
          node!.supportsSilentPayments = true;
        } else {
          node!.supportsSilentPayments = false;
        }
      } catch (_) {
        node!.supportsSilentPayments = false;
      }
    }
    node!.save();
    return node!.supportsSilentPayments!;
  }

  LedgerConnection? _ledgerConnection;
  BitcoinLedgerApp? _bitcoinLedgerApp;

  @override
  void setLedgerConnection(LedgerConnection connection) {
    _ledgerConnection = connection;
    _bitcoinLedgerApp = BitcoinLedgerApp(_ledgerConnection!,
        derivationPath: walletInfo.derivationInfo!.derivationPath!);
  }

  @override
  Future<BtcTransaction> buildHardwareWalletTransaction({
    required List<BitcoinBaseOutput> outputs,
    required BigInt fee,
    required BasedUtxoNetwork network,
    required List<UtxoWithAddress> utxos,
    required Map<String, PublicKeyWithDerivationPath> publicKeys,
    String? memo,
    bool enableRBF = false,
    BitcoinOrdering inputOrdering = BitcoinOrdering.bip69,
    BitcoinOrdering outputOrdering = BitcoinOrdering.bip69,
  }) async {
    final masterFingerprint = await _bitcoinLedgerApp!.getMasterFingerprint();

    final psbtReadyInputs = <PSBTReadyUtxoWithAddress>[];
    for (final utxo in utxos) {
      final rawTx =
          (await getTransactionExpanded(hash: utxo.utxo.txHash)).originalTransaction.toHex();
      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 psbt =
        PSBTTransactionBuild(inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF);

    final rawHex = await _bitcoinLedgerApp!.signPsbt(psbt: psbt.psbt);
    return BtcTransaction.fromRaw(BytesUtils.toHexString(rawHex));
  }

  @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?.isChange == 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);
  }

  @action
  Future<void> setSilentPaymentsScanning(bool active) async {
    silentPaymentsScanningActive = active;
    final nodeSupportsSilentPayments = await getNodeSupportsSilentPayments();
    final isAllowedToScan = nodeSupportsSilentPayments || allowedToSwitchNodesForScanning;

    if (active && isAllowedToScan) {
      syncStatus = AttemptingScanSyncStatus();

      final tip = currentChainTip!;

      if (tip == walletInfo.restoreHeight) {
        syncStatus = SyncedTipSyncStatus(tip);
        return;
      }

      if (tip > walletInfo.restoreHeight) {
        _setListeners(walletInfo.restoreHeight);
      }
    } else if (syncStatus is! SyncedSyncStatus) {
      await sendWorker(ElectrumWorkerStopScanningRequest());
      await startSync();
    }
  }

  @override
  @action
  Future<void> updateAllUnspents() async {
    List<BitcoinUnspent> updatedUnspentCoins = [];

    // Update unspents stored from scanned silent payment transactions
    transactionHistory.transactions.values.forEach((tx) {
      if (tx.unspents != null) {
        updatedUnspentCoins.addAll(tx.unspents!);
      }
    });

    unspentCoins.addAll(updatedUnspentCoins);

    await super.updateAllUnspents();

    final walletAddresses = this.walletAddresses as BitcoinWalletAddresses;

    walletAddresses.silentPaymentAddresses.forEach((addressRecord) {
      addressRecord.txCount = 0;
      addressRecord.balance = 0;
    });
    walletAddresses.receivedSPAddresses.forEach((addressRecord) {
      addressRecord.txCount = 0;
      addressRecord.balance = 0;
    });

    final silentPaymentWallet = walletAddresses.silentPaymentWallet;

    unspentCoins.forEach((unspent) {
      if (unspent.bitcoinAddressRecord is BitcoinReceivedSPAddressRecord) {
        _updateSilentAddressRecord(unspent);

        final receiveAddressRecord = unspent.bitcoinAddressRecord as BitcoinReceivedSPAddressRecord;
        final silentPaymentAddress = SilentPaymentAddress(
          version: silentPaymentWallet!.version,
          B_scan: silentPaymentWallet.B_scan,
          B_spend: receiveAddressRecord.labelHex != null
              ? silentPaymentWallet.B_spend.tweakAdd(
                  BigintUtils.fromBytes(
                    BytesUtils.fromHexString(receiveAddressRecord.labelHex!),
                  ),
                )
              : silentPaymentWallet.B_spend,
        );

        walletAddresses.silentPaymentAddresses.forEach((addressRecord) {
          if (addressRecord.address == silentPaymentAddress.toAddress(network)) {
            addressRecord.txCount += 1;
            addressRecord.balance += unspent.value;
          }
        });
        walletAddresses.receivedSPAddresses.forEach((addressRecord) {
          if (addressRecord.address == receiveAddressRecord.address) {
            addressRecord.txCount += 1;
            addressRecord.balance += unspent.value;
          }
        });
      }
    });

    await walletAddresses.updateAddressesInBox();
  }

  @override
  void updateCoin(BitcoinUnspent coin) {
    final coinInfoList = unspentCoinsInfo.values.where(
      (element) =>
          element.walletId.contains(id) &&
          element.hash.contains(coin.hash) &&
          element.vout == coin.vout,
    );

    if (coinInfoList.isNotEmpty) {
      final coinInfo = coinInfoList.first;

      coin.isFrozen = coinInfo.isFrozen;
      coin.isSending = coinInfo.isSending;
      coin.note = coinInfo.note;
      if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord)
        coin.bitcoinAddressRecord.balance += coinInfo.value;
    } else {
      addCoinInfo(coin);
    }
  }

  @action
  @override
  Future<void> startSync() async {
    await _setInitialScanHeight();

    await super.startSync();

    if (alwaysScan == true) {
      _setListeners(walletInfo.restoreHeight);
    }
  }

  @action
  @override
  Future<void> rescan({required int height, bool? doSingleScan}) async {
    silentPaymentsScanningActive = true;
    _setListeners(height, doSingleScan: doSingleScan);
  }

  // @action
  // Future<void> registerSilentPaymentsKey(bool register) async {
  //   silentPaymentsScanningActive = active;

  //   if (active) {
  //     syncStatus = AttemptingScanSyncStatus();

  //     final tip = await getUpdatedChainTip();

  //     if (tip == walletInfo.restoreHeight) {
  //       syncStatus = SyncedTipSyncStatus(tip);
  //       return;
  //     }

  //     if (tip > walletInfo.restoreHeight) {
  //       _setListeners(walletInfo.restoreHeight, chainTipParam: _currentChainTip);
  //     }
  //   } else {
  //     alwaysScan = false;

  //     _isolate?.then((value) => value.kill(priority: Isolate.immediate));

  //     if (electrumClient.isConnected) {
  //       syncStatus = SyncedSyncStatus();
  //     } else {
  //       syncStatus = NotConnectedSyncStatus();
  //     }
  //   }
  // }

  @action
  Future<void> registerSilentPaymentsKey() async {
    // final registered = await electrumClient.tweaksRegister(
    //   secViewKey: walletAddresses.silentAddress!.b_scan.toHex(),
    //   pubSpendKey: walletAddresses.silentAddress!.B_spend.toHex(),
    //   labels: walletAddresses.silentAddresses
    //       .where((addr) => addr.type == SilentPaymentsAddresType.p2sp && addr.labelIndex >= 1)
    //       .map((addr) => addr.labelIndex)
    //       .toList(),
    // );

    // printV("registered: $registered");
  }

  @action
  void _updateSilentAddressRecord(BitcoinUnspent unspent) {
    final walletAddresses = this.walletAddresses as BitcoinWalletAddresses;
    walletAddresses.addReceivedSPAddresses(
      [unspent.bitcoinAddressRecord as BitcoinReceivedSPAddressRecord],
    );
  }

  @override
  @action
  Future<void> handleWorkerResponse(dynamic message) async {
    super.handleWorkerResponse(message);

    Map<String, dynamic> messageJson;
    if (message is String) {
      messageJson = jsonDecode(message) as Map<String, dynamic>;
    } else {
      messageJson = message as Map<String, dynamic>;
    }
    final workerMethod = messageJson['method'] as String;
    final workerError = messageJson['error'] as String?;

    switch (workerMethod) {
      case ElectrumRequestMethods.tweaksSubscribeMethod:
        if (workerError != null) {
          printV(messageJson);
          // _onConnectionStatusChange(ConnectionStatus.failed);
          break;
        }

        final response = ElectrumWorkerTweaksSubscribeResponse.fromJson(messageJson);
        onTweaksSyncResponse(response.result);
        break;
    }
  }

  @action
  Future<void> onTweaksSyncResponse(TweaksSyncResponse result) async {
    if (result.transactions?.isNotEmpty == true) {
      (walletAddresses as BitcoinWalletAddresses).silentPaymentAddresses.forEach((addressRecord) {
        addressRecord.txCount = 0;
        addressRecord.balance = 0;
      });
      (walletAddresses as BitcoinWalletAddresses).receivedSPAddresses.forEach((addressRecord) {
        addressRecord.txCount = 0;
        addressRecord.balance = 0;
      });

      for (final map in result.transactions!.entries) {
        final txid = map.key;
        final tx = map.value;

        if (tx.unspents != null) {
          final existingTxInfo = transactionHistory.transactions[txid];
          final txAlreadyExisted = existingTxInfo != null;

          // Updating tx after re-scanned
          if (txAlreadyExisted) {
            existingTxInfo.amount = tx.amount;
            existingTxInfo.confirmations = tx.confirmations;
            existingTxInfo.height = tx.height;

            final newUnspents = tx.unspents!
                .where((unspent) => !(existingTxInfo.unspents?.any((element) =>
                        element.hash.contains(unspent.hash) &&
                        element.vout == unspent.vout &&
                        element.value == unspent.value) ??
                    false))
                .toList();

            if (newUnspents.isNotEmpty) {
              newUnspents.forEach(_updateSilentAddressRecord);

              existingTxInfo.unspents ??= [];
              existingTxInfo.unspents!.addAll(newUnspents);

              final newAmount = newUnspents.length > 1
                  ? newUnspents.map((e) => e.value).reduce((value, unspent) => value + unspent)
                  : newUnspents[0].value;

              if (existingTxInfo.direction == TransactionDirection.incoming) {
                existingTxInfo.amount += newAmount;
              }

              // Updates existing TX
              transactionHistory.addOne(existingTxInfo);
              // Update balance record
              balance[currency]!.confirmed += newAmount;
            }
          } else {
            // else: First time seeing this TX after scanning
            tx.unspents!.forEach(_updateSilentAddressRecord);

            transactionHistory.addOne(tx);
            balance[currency]!.confirmed += tx.amount;
          }

          await updateAllUnspents();
        }
      }
    }

    final newSyncStatus = result.syncStatus;

    if (newSyncStatus != null) {
      if (newSyncStatus is UnsupportedSyncStatus) {
        nodeSupportsSilentPayments = false;
      }

      if (newSyncStatus is SyncingSyncStatus) {
        syncStatus = SyncingSyncStatus(newSyncStatus.blocksLeft, newSyncStatus.ptc);
      } else {
        syncStatus = newSyncStatus;

        if (newSyncStatus is SyncedSyncStatus) {
          silentPaymentsScanningActive = false;
        }
      }

      final height = result.height;
      if (height != null) {
        await walletInfo.updateRestoreHeight(height);
      }
    }
  }

  @action
  Future<void> _setListeners(int height, {bool? doSingleScan}) async {
    if (currentChainTip == null) {
      throw Exception("currentChainTip is null");
    }

    final chainTip = currentChainTip!;

    if (chainTip == height) {
      syncStatus = SyncedSyncStatus();
      return;
    }

    syncStatus = AttemptingScanSyncStatus();

    final walletAddresses = this.walletAddresses as BitcoinWalletAddresses;
    workerSendPort!.send(
      ElectrumWorkerTweaksSubscribeRequest(
        scanData: ScanData(
          silentPaymentsWallets: walletAddresses.silentPaymentWallets,
          network: network,
          height: height,
          chainTip: chainTip,
          transactionHistoryIds: transactionHistory.transactions.keys.toList(),
          labels: walletAddresses.labels,
          labelIndexes: walletAddresses.silentPaymentAddresses
              .where((addr) =>
                  addr.addressType == SilentPaymentsAddresType.p2sp && addr.labelIndex >= 1)
              .map((addr) => addr.labelIndex)
              .toList(),
          isSingleScan: doSingleScan ?? false,
          shouldSwitchNodes:
              !(await getNodeSupportsSilentPayments()) && allowedToSwitchNodesForScanning,
        ),
      ).toJson(),
    );
  }

  @override
  @action
  Future<Map<String, ElectrumTransactionInfo>> fetchTransactions() async {
    throw UnimplementedError();
    // try {
    //   final Map<String, ElectrumTransactionInfo> historiesWithDetails = {};

    //   await Future.wait(
    //     BITCOIN_ADDRESS_TYPES.map(
    //       (type) => fetchTransactionsForAddressType(historiesWithDetails, type),
    //     ),
    //   );

    //   transactionHistory.transactions.values.forEach((tx) async {
    //     final isPendingSilentPaymentUtxo =
    //         (tx.isPending || tx.confirmations == 0) && historiesWithDetails[tx.id] == null;

    //     if (isPendingSilentPaymentUtxo) {
    //       final info = await fetchTransactionInfo(hash: tx.id, height: tx.height);

    //       if (info != null) {
    //         tx.confirmations = info.confirmations;
    //         tx.isPending = tx.confirmations == 0;
    //         transactionHistory.addOne(tx);
    //         await transactionHistory.save();
    //       }
    //     }
    //   });

    //   return historiesWithDetails;
    // } catch (e) {
    //   printV("fetchTransactions $e");
    //   return {};
    // }
  }

  @override
  @action
  Future<void> updateTransactions([List<BitcoinAddressRecord>? addresses]) async {
    super.updateTransactions();

    transactionHistory.transactions.values.forEach((tx) {
      if (tx.unspents != null &&
          tx.unspents!.isNotEmpty &&
          tx.height != null &&
          tx.height! > 0 &&
          (currentChainTip ?? 0) > 0) {
        tx.confirmations = currentChainTip! - tx.height! + 1;
      }
    });
  }

  // @action
  // Future<ElectrumBalance> fetchBalances() async {
  //   final balance = await super.fetchBalances();

  //   int totalFrozen = balance.frozen;
  //   int totalConfirmed = balance.confirmed;

  //   // Add values from unspent coins that are not fetched by the address list
  //   // i.e. scanned silent payments
  //   transactionHistory.transactions.values.forEach((tx) {
  //     if (tx.unspents != null) {
  //       tx.unspents!.forEach((unspent) {
  //         if (unspent.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) {
  //           if (unspent.isFrozen) totalFrozen += unspent.value;
  //           totalConfirmed += unspent.value;
  //         }
  //       });
  //     }
  //   });

  //   return ElectrumBalance(
  //     confirmed: totalConfirmed,
  //     unconfirmed: balance.unconfirmed,
  //     frozen: totalFrozen,
  //   );
  // }

  @override
  @action
  Future<void> onHeadersResponse(ElectrumHeaderResponse response) async {
    super.onHeadersResponse(response);

    _setInitialScanHeight();

    // New headers received, start scanning
    if (alwaysScan == true && syncStatus is SyncedSyncStatus) {
      _setListeners(walletInfo.restoreHeight);
    }
  }

  Future<void> _setInitialScanHeight() async {
    final validChainTip = currentChainTip != null && currentChainTip != 0;
    if (validChainTip && walletInfo.restoreHeight == 0) {
      await walletInfo.updateRestoreHeight(currentChainTip!);
    }
  }

  @override
  @action
  void syncStatusReaction(SyncStatus syncStatus) {
    switch (syncStatus.runtimeType) {
      case SyncingSyncStatus:
        return;
      case SyncedTipSyncStatus:
        silentPaymentsScanningActive = false;

        // Message is shown on the UI for 3 seconds, then reverted to synced
        Timer(Duration(seconds: 3), () {
          if (this.syncStatus is SyncedTipSyncStatus) this.syncStatus = SyncedSyncStatus();
        });
        break;
      default:
        super.syncStatusReaction(syncStatus);
    }
  }

  @override
  Future<int> calcFee({
    required List<UtxoWithAddress> utxos,
    required List<BitcoinBaseOutput> outputs,
    String? memo,
    required int feeRate,
    List<ECPrivateInfo>? inputPrivKeyInfos,
    List<Outpoint>? vinOutpoints,
  }) async =>
      feeRate *
      BitcoinTransactionBuilder.estimateTransactionSize(
        utxos: utxos,
        outputs: outputs,
        network: network,
        memo: memo,
        inputPrivKeyInfos: inputPrivKeyInfos,
        vinOutpoints: vinOutpoints,
      );

  @override
  TxCreateUtxoDetails createUTXOS({
    required bool sendAll,
    bool paysToSilentPayment = false,
    int credentialsAmount = 0,
    int? inputsCount,
    UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
  }) {
    List<UtxoWithAddress> utxos = [];
    List<Outpoint> vinOutpoints = [];
    List<ECPrivateInfo> inputPrivKeyInfos = [];
    final publicKeys = <String, PublicKeyWithDerivationPath>{};
    int allInputsAmount = 0;
    bool spendsSilentPayment = false;
    bool spendsUnconfirmedTX = false;

    int leftAmount = credentialsAmount;
    var availableInputs = unspentCoins.where((utx) {
      if (!utx.isSending || utx.isFrozen) {
        return false;
      }
      return true;
    }).toList();
    final unconfirmedCoins = availableInputs.where((utx) => utx.confirmations == 0).toList();

    for (int i = 0; i < availableInputs.length; i++) {
      final utx = availableInputs[i];
      if (!spendsUnconfirmedTX) spendsUnconfirmedTX = utx.confirmations == 0;

      if (paysToSilentPayment) {
        // Check inputs for shared secret derivation
        if (utx.bitcoinAddressRecord.addressType == SegwitAddresType.p2wsh) {
          throw BitcoinTransactionSilentPaymentsNotSupported();
        }
      }

      allInputsAmount += utx.value;
      leftAmount = leftAmount - utx.value;

      final address = RegexUtils.addressTypeFromStr(utx.address, network);
      ECPrivate? privkey;
      bool? isSilentPayment = false;

      if (utx.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) {
        privkey = (utx.bitcoinAddressRecord as BitcoinReceivedSPAddressRecord).getSpendKey(
          (walletAddresses as BitcoinWalletAddresses).silentPaymentWallets,
          network,
        );
        spendsSilentPayment = true;
        isSilentPayment = true;
      } else if (!isHardwareWallet) {
        final addressRecord = (utx.bitcoinAddressRecord as BitcoinAddressRecord);
        final path = addressRecord.derivationInfo.derivationPath
            .addElem(Bip32KeyIndex(
              BitcoinAddressUtils.getAccountFromChange(addressRecord.isChange),
            ))
            .addElem(Bip32KeyIndex(addressRecord.index));

        privkey = ECPrivate.fromBip32(bip32: bip32.derive(path));
      }

      vinOutpoints.add(Outpoint(txid: utx.hash, index: utx.vout));
      String pubKeyHex;

      if (privkey != null) {
        inputPrivKeyInfos.add(ECPrivateInfo(
          privkey,
          address.type == SegwitAddresType.p2tr,
          tweak: !isSilentPayment,
        ));

        pubKeyHex = privkey.getPublic().toHex();
      } else {
        pubKeyHex = walletAddresses.hdWallet
            .childKey(Bip32KeyIndex(utx.bitcoinAddressRecord.index))
            .publicKey
            .toHex();
      }

      if (utx.bitcoinAddressRecord is BitcoinAddressRecord) {
        final derivationPath = (utx.bitcoinAddressRecord as BitcoinAddressRecord)
            .derivationInfo
            .derivationPath
            .toString();
        publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath(pubKeyHex, derivationPath);
      }

      utxos.add(
        UtxoWithAddress(
          utxo: BitcoinUtxo(
            txHash: utx.hash,
            value: BigInt.from(utx.value),
            vout: utx.vout,
            scriptType: BitcoinAddressUtils.getScriptType(address),
            isSilentPayment: isSilentPayment,
          ),
          ownerDetails: UtxoAddressDetails(
            publicKey: pubKeyHex,
            address: address,
          ),
        ),
      );

      // sendAll continues for all inputs
      if (!sendAll) {
        bool amountIsAcquired = leftAmount <= 0;
        if ((inputsCount == null && amountIsAcquired) || inputsCount == i + 1) {
          break;
        }
      }
    }

    if (utxos.isEmpty) {
      throw BitcoinTransactionNoInputsException();
    }

    return TxCreateUtxoDetails(
      availableInputs: availableInputs,
      unconfirmedCoins: unconfirmedCoins,
      utxos: utxos,
      vinOutpoints: vinOutpoints,
      inputPrivKeyInfos: inputPrivKeyInfos,
      publicKeys: publicKeys,
      allInputsAmount: allInputsAmount,
      spendsSilentPayment: spendsSilentPayment,
      spendsUnconfirmedTX: spendsUnconfirmedTX,
    );
  }

  @override
  Future<EstimatedTxResult> estimateSendAllTx(
    List<BitcoinOutput> outputs,
    int feeRate, {
    String? memo,
    bool hasSilentPayment = false,
    UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
  }) async {
    final utxoDetails = createUTXOS(sendAll: true, paysToSilentPayment: hasSilentPayment);

    int fee = await calcFee(
      utxos: utxoDetails.utxos,
      outputs: outputs,
      memo: memo,
      feeRate: feeRate,
      inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos,
      vinOutpoints: utxoDetails.vinOutpoints,
    );

    if (fee == 0) {
      throw BitcoinTransactionNoFeeException();
    }

    // Here, when sending all, the output amount equals to the input value - fee to fully spend every input on the transaction and have no amount left for change
    int amount = utxoDetails.allInputsAmount - fee;

    if (amount <= 0) {
      throw BitcoinTransactionWrongBalanceException(amount: utxoDetails.allInputsAmount + fee);
    }

    // Attempting to send less than the dust limit
    if (isBelowDust(amount)) {
      throw BitcoinTransactionNoDustException();
    }

    if (outputs.length == 1) {
      outputs[0] = BitcoinOutput(address: outputs.last.address, value: BigInt.from(amount));
    }

    return EstimatedTxResult(
      utxos: utxoDetails.utxos,
      inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos,
      publicKeys: utxoDetails.publicKeys,
      fee: fee,
      amount: amount,
      isSendAll: true,
      hasChange: false,
      memo: memo,
      spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX,
      spendsSilentPayment: utxoDetails.spendsSilentPayment,
    );
  }

  @override
  Future<EstimatedTxResult> estimateTxForAmount(
    int credentialsAmount,
    List<BitcoinOutput> outputs,
    int feeRate, {
    List<BitcoinOutput> updatedOutputs = const [],
    int? inputsCount,
    String? memo,
    bool? useUnconfirmed,
    bool hasSilentPayment = false,
    UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
  }) async {
    // Attempting to send less than the dust limit
    if (isBelowDust(credentialsAmount)) {
      throw BitcoinTransactionNoDustException();
    }

    final utxoDetails = createUTXOS(
      sendAll: false,
      credentialsAmount: credentialsAmount,
      inputsCount: inputsCount,
      paysToSilentPayment: hasSilentPayment,
    );

    final spendingAllCoins = utxoDetails.availableInputs.length == utxoDetails.utxos.length;
    final spendingAllConfirmedCoins = !utxoDetails.spendsUnconfirmedTX &&
        utxoDetails.utxos.length ==
            utxoDetails.availableInputs.length - utxoDetails.unconfirmedCoins.length;

    // How much is being spent - how much is being sent
    int amountLeftForChangeAndFee = utxoDetails.allInputsAmount - credentialsAmount;

    if (amountLeftForChangeAndFee <= 0) {
      if (!spendingAllCoins) {
        return estimateTxForAmount(
          credentialsAmount,
          outputs,
          feeRate,
          updatedOutputs: updatedOutputs,
          inputsCount: utxoDetails.utxos.length + 1,
          memo: memo,
          hasSilentPayment: hasSilentPayment,
        );
      }

      throw BitcoinTransactionWrongBalanceException();
    }

    final changeAddress = await walletAddresses.getChangeAddress(
      inputs: utxoDetails.availableInputs,
      outputs: updatedOutputs,
    );
    final address = RegexUtils.addressTypeFromStr(changeAddress.address, network);
    updatedOutputs.add(BitcoinOutput(
      address: address,
      value: BigInt.from(amountLeftForChangeAndFee),
      isChange: true,
    ));
    outputs.add(BitcoinOutput(
      address: address,
      value: BigInt.from(amountLeftForChangeAndFee),
      isChange: true,
    ));

    // Get Derivation path for change Address since it is needed in Litecoin and BitcoinCash hardware Wallets
    final changeDerivationPath = changeAddress.derivationInfo.derivationPath.toString();
    utxoDetails.publicKeys[address.pubKeyHash()] =
        PublicKeyWithDerivationPath('', changeDerivationPath);

    // calcFee updates the silent payment outputs to calculate the tx size accounting
    // for taproot addresses, but if more inputs are needed to make up for fees,
    // the silent payment outputs need to be recalculated for the new inputs
    var temp = outputs.map((output) => output).toList();
    int fee = await calcFee(
      utxos: utxoDetails.utxos,
      // Always take only not updated bitcoin outputs here so for every estimation
      // the SP outputs are re-generated to the proper taproot addresses
      outputs: temp,
      memo: memo,
      feeRate: feeRate,
      inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos,
      vinOutpoints: utxoDetails.vinOutpoints,
    );

    updatedOutputs.clear();
    updatedOutputs.addAll(temp);

    if (fee == 0) {
      throw BitcoinTransactionNoFeeException();
    }

    int amount = credentialsAmount;
    final lastOutput = updatedOutputs.last;
    final amountLeftForChange = amountLeftForChangeAndFee - fee;

    if (isBelowDust(amountLeftForChange)) {
      // If has change that is lower than dust, will end up with tx rejected by network rules
      // so remove the change amount
      updatedOutputs.removeLast();
      outputs.removeLast();

      if (amountLeftForChange < 0) {
        if (!spendingAllCoins) {
          return estimateTxForAmount(
            credentialsAmount,
            outputs,
            feeRate,
            updatedOutputs: updatedOutputs,
            inputsCount: utxoDetails.utxos.length + 1,
            memo: memo,
            useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins,
            hasSilentPayment: hasSilentPayment,
          );
        } else {
          throw BitcoinTransactionWrongBalanceException();
        }
      }

      return EstimatedTxResult(
        utxos: utxoDetails.utxos,
        inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos,
        publicKeys: utxoDetails.publicKeys,
        fee: fee,
        amount: amount,
        hasChange: false,
        isSendAll: spendingAllCoins,
        memo: memo,
        spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX,
        spendsSilentPayment: utxoDetails.spendsSilentPayment,
      );
    } else {
      // Here, lastOutput already is change, return the amount left without the fee to the user's address.
      updatedOutputs[updatedOutputs.length - 1] = BitcoinOutput(
        address: lastOutput.address,
        value: BigInt.from(amountLeftForChange),
        isSilentPayment: lastOutput.isSilentPayment,
        isChange: true,
      );
      outputs[outputs.length - 1] = BitcoinOutput(
        address: lastOutput.address,
        value: BigInt.from(amountLeftForChange),
        isSilentPayment: lastOutput.isSilentPayment,
        isChange: true,
      );

      return EstimatedTxResult(
        utxos: utxoDetails.utxos,
        inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos,
        publicKeys: utxoDetails.publicKeys,
        fee: fee,
        amount: amount,
        hasChange: true,
        isSendAll: spendingAllCoins,
        memo: memo,
        spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX,
        spendsSilentPayment: utxoDetails.spendsSilentPayment,
      );
    }
  }

  @override
  Future<PendingTransaction> createTransaction(Object credentials) async {
    try {
      final outputs = <BitcoinOutput>[];
      final transactionCredentials = credentials as BitcoinTransactionCredentials;
      final hasMultiDestination = transactionCredentials.outputs.length > 1;
      final sendAll = !hasMultiDestination && transactionCredentials.outputs.first.sendAll;
      final memo = transactionCredentials.outputs.first.memo;
      final coinTypeToSpendFrom = transactionCredentials.coinTypeToSpendFrom;

      int credentialsAmount = 0;
      bool hasSilentPayment = false;

      for (final out in transactionCredentials.outputs) {
        final outputAmount = out.formattedCryptoAmount!;

        if (!sendAll && isBelowDust(outputAmount)) {
          throw BitcoinTransactionNoDustException();
        }

        if (hasMultiDestination) {
          if (out.sendAll) {
            throw BitcoinTransactionWrongBalanceException();
          }
        }

        credentialsAmount += outputAmount;

        final address = RegexUtils.addressTypeFromStr(
            out.isParsedAddress ? out.extractedAddress! : out.address, network);
        final isSilentPayment = address is SilentPaymentAddress;

        if (isSilentPayment) {
          hasSilentPayment = true;
        }

        if (sendAll) {
          // The value will be changed after estimating the Tx size and deducting the fee from the total to be sent
          outputs.add(BitcoinOutput(
            address: address,
            value: BigInt.from(0),
            isSilentPayment: isSilentPayment,
          ));
        } else {
          outputs.add(BitcoinOutput(
            address: address,
            value: BigInt.from(outputAmount),
            isSilentPayment: isSilentPayment,
          ));
        }
      }

      final feeRateInt = transactionCredentials.feeRate != null
          ? transactionCredentials.feeRate!
          : feeRate(transactionCredentials.priority!);

      EstimatedTxResult estimatedTx;
      final updatedOutputs = outputs
          .map((e) => BitcoinOutput(
                address: e.address,
                value: e.value,
                isSilentPayment: e.isSilentPayment,
                isChange: e.isChange,
              ))
          .toList();

      if (sendAll) {
        estimatedTx = await estimateSendAllTx(
          updatedOutputs,
          feeRateInt,
          memo: memo,
          hasSilentPayment: hasSilentPayment,
          coinTypeToSpendFrom: coinTypeToSpendFrom,
        );
      } else {
        estimatedTx = await estimateTxForAmount(
          credentialsAmount,
          outputs,
          feeRateInt,
          updatedOutputs: updatedOutputs,
          memo: memo,
          hasSilentPayment: hasSilentPayment,
          coinTypeToSpendFrom: coinTypeToSpendFrom,
        );
      }

      if (walletInfo.isHardwareWallet) {
        final transaction = await buildHardwareWalletTransaction(
          utxos: estimatedTx.utxos,
          outputs: updatedOutputs,
          publicKeys: estimatedTx.publicKeys,
          fee: BigInt.from(estimatedTx.fee),
          network: network,
          memo: estimatedTx.memo,
          outputOrdering: BitcoinOrdering.none,
          enableRBF: true,
        );

        return PendingBitcoinTransaction(
          transaction,
          type,
          sendWorker: sendWorker,
          amount: estimatedTx.amount,
          fee: estimatedTx.fee,
          feeRate: feeRateInt.toString(),
          hasChange: estimatedTx.hasChange,
          isSendAll: estimatedTx.isSendAll,
          hasTaprootInputs: false, // ToDo: (Konsti) Support Taproot
        )..addListener((transaction) async {
            transactionHistory.addOne(transaction);
            await updateBalance();
          });
      }

      final txb = BitcoinTransactionBuilder(
        utxos: estimatedTx.utxos,
        outputs: updatedOutputs,
        fee: BigInt.from(estimatedTx.fee),
        network: network,
        memo: estimatedTx.memo,
        outputOrdering: BitcoinOrdering.none,
        enableRBF: !estimatedTx.spendsUnconfirmedTX,
      );

      bool hasTaprootInputs = false;

      final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) {
        String error = "Cannot find private key.";

        ECPrivateInfo? key;

        if (estimatedTx.inputPrivKeyInfos.isEmpty) {
          error += "\nNo private keys generated.";
        } else {
          error += "\nAddress: ${utxo.ownerDetails.address.toAddress(network)}";

          try {
            key = estimatedTx.inputPrivKeyInfos.firstWhere((element) {
              final elemPubkey = element.privkey.getPublic().toHex();
              if (elemPubkey == publicKey) {
                return true;
              } else {
                error += "\nExpected: $publicKey";
                error += "\nPubkey: $elemPubkey";
                return false;
              }
            });
          } catch (_) {
            throw Exception(error);
          }
        }

        if (key == null) {
          throw Exception(error);
        }

        if (utxo.utxo.isP2tr()) {
          hasTaprootInputs = true;
          return key.privkey.signTapRoot(
            txDigest,
            sighash: sighash,
            tweak: utxo.utxo.isSilentPayment != true,
          );
        } else {
          return key.privkey.signInput(txDigest, sigHash: sighash);
        }
      });

      return PendingBitcoinTransaction(
        transaction,
        type,
        sendWorker: sendWorker,
        amount: estimatedTx.amount,
        fee: estimatedTx.fee,
        feeRate: feeRateInt.toString(),
        hasChange: estimatedTx.hasChange,
        isSendAll: estimatedTx.isSendAll,
        hasTaprootInputs: hasTaprootInputs,
        utxos: estimatedTx.utxos,
        hasSilentPayment: hasSilentPayment,
      )..addListener((transaction) async {
          transactionHistory.addOne(transaction);
          if (estimatedTx.spendsSilentPayment) {
            transactionHistory.transactions.values.forEach((tx) {
              tx.unspents?.removeWhere(
                  (unspent) => estimatedTx.utxos.any((e) => e.utxo.txHash == unspent.hash));
              transactionHistory.addOne(tx);
            });
          }

          unspentCoins
              .removeWhere((utxo) => estimatedTx.utxos.any((e) => e.utxo.txHash == utxo.hash));

          await updateBalance();
        });
    } catch (e) {
      throw e;
    }
  }
}