stack_wallet/lib/services/coins/bitcoin/bitcoin_wallet.dart

1932 lines
70 KiB
Dart
Raw Normal View History

// /*
// * This file is part of Stack Wallet.
// *
// * Copyright (c) 2023 Cypher Stack
// * All Rights Reserved.
// * The code is distributed under GPLv3 license, see LICENSE file for details.
// * Generated by Cypher Stack on 2023-05-26
// *
// */
//
// import 'dart:async';
// import 'dart:convert';
// import 'dart:io';
// import 'dart:math';
//
// import 'package:bech32/bech32.dart';
// import 'package:bip32/bip32.dart' as bip32;
// import 'package:bip39/bip39.dart' as bip39;
// import 'package:bitcoindart/bitcoindart.dart';
// import 'package:bs58check/bs58check.dart' as bs58check;
// import 'package:decimal/decimal.dart';
// import 'package:flutter/foundation.dart';
// import 'package:isar/isar.dart';
// import 'package:stackwallet/db/isar/main_db.dart';
// import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart';
// import 'package:stackwallet/electrumx_rpc/electrumx.dart';
// import 'package:stackwallet/exceptions/electrumx/no_such_transaction.dart';
// import 'package:stackwallet/models/balance.dart';
// import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models;
// import 'package:stackwallet/models/paymint/fee_object_model.dart';
// import 'package:stackwallet/models/signing_data.dart';
// import 'package:stackwallet/services/coins/coin_service.dart';
// import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart';
// import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart';
// import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
// import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
// import 'package:stackwallet/services/event_bus/global_event_bus.dart';
// import 'package:stackwallet/services/mixins/coin_control_interface.dart';
// import 'package:stackwallet/services/mixins/electrum_x_parsing.dart';
// import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart';
// import 'package:stackwallet/services/mixins/wallet_cache.dart';
// import 'package:stackwallet/services/mixins/wallet_db.dart';
// import 'package:stackwallet/services/mixins/xpubable.dart';
// import 'package:stackwallet/services/node_service.dart';
// import 'package:stackwallet/services/transaction_notification_tracker.dart';
// import 'package:stackwallet/utilities/address_utils.dart';
// import 'package:stackwallet/utilities/amount/amount.dart';
// import 'package:stackwallet/utilities/bip32_utils.dart';
// import 'package:stackwallet/utilities/constants.dart';
// import 'package:stackwallet/utilities/default_nodes.dart';
// import 'package:stackwallet/utilities/enums/coin_enum.dart';
// import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart';
// import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart';
// import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
// import 'package:stackwallet/utilities/format.dart';
// import 'package:stackwallet/utilities/logger.dart';
// import 'package:stackwallet/utilities/paynym_is_api.dart';
// import 'package:stackwallet/utilities/prefs.dart';
// import 'package:stackwallet/widgets/crypto_notifications.dart';
// import 'package:tuple/tuple.dart';
// import 'package:uuid/uuid.dart';
//
// class BitcoinWallet extends CoinServiceAPI
// with
// WalletCache,
// WalletDB,
// ElectrumXParsing,
// PaynymWalletInterface,
// CoinControlInterface
// implements XPubAble {
// BitcoinWallet({
// required String walletId,
// required String walletName,
// required Coin coin,
// required ElectrumX client,
// required CachedElectrumX cachedClient,
// required TransactionNotificationTracker tracker,
// required SecureStorageInterface secureStore,
// MainDB? mockableOverride,
// }) {
// txTracker = tracker;
// _walletId = walletId;
// _walletName = walletName;
// _coin = coin;
// _electrumXClient = client;
// _cachedElectrumXClient = cachedClient;
// _secureStore = secureStore;
// initCache(walletId, coin);
// initWalletDB(mockableOverride: mockableOverride);
// initCoinControlInterface(
// walletId: walletId,
// walletName: walletName,
// coin: coin,
// db: db,
// getChainHeight: () => chainHeight,
// refreshedBalanceCallback: (balance) async {
// _balance = balance;
// await updateCachedBalance(_balance!);
// },
// );
// initPaynymWalletInterface(
// walletId: walletId,
// walletName: walletName,
// network: _network,
// coin: coin,
// db: db,
// electrumXClient: electrumXClient,
// secureStorage: secureStore,
// getMnemonicString: () => mnemonicString,
// getMnemonicPassphrase: () => mnemonicPassphrase,
// getChainHeight: () => chainHeight,
// // getCurrentChangeAddress: () => currentChangeAddressP2PKH,
// getCurrentChangeAddress: () => currentChangeAddress,
// estimateTxFee: estimateTxFee,
// prepareSend: prepareSend,
// getTxCount: getTxCount,
// fetchBuildTxData: fetchBuildTxData,
// refresh: refresh,
// checkChangeAddressForTransactions: _checkChangeAddressForTransactions,
// // checkChangeAddressForTransactions:
// // _checkP2PKHChangeAddressForTransactions,
// dustLimitP2PKH: DUST_LIMIT_P2PKH.raw.toInt(),
// minConfirms: MINIMUM_CONFIRMATIONS,
// dustLimit: DUST_LIMIT.raw.toInt(),
// );
// }
//
// static const integrationTestFlag =
// bool.fromEnvironment("IS_INTEGRATION_TEST");
//
// final _prefs = Prefs.instance;
//
// Timer? timer;
// late final Coin _coin;
//
// late final TransactionNotificationTracker txTracker;
//
// NetworkType get _network {
// switch (coin) {
// case Coin.bitcoin:
// return bitcoin;
// case Coin.bitcoinTestNet:
// return testnet;
// default:
// throw Exception("Invalid network type!");
// }
// }
//
// @override
// set isFavorite(bool markFavorite) {
// _isFavorite = markFavorite;
// updateCachedIsFavorite(markFavorite);
// }
//
// @override
// bool get isFavorite => _isFavorite ??= getCachedIsFavorite();
//
// bool? _isFavorite;
//
// @override
// Coin get coin => _coin;
//
// @override
// Future<List<isar_models.UTXO>> get utxos => db.getUTXOs(walletId).findAll();
//
// @override
// Future<List<isar_models.Transaction>> get transactions => db
// .getTransactions(walletId)
// .filter()
// .not()
// .group((q) => q
// .subTypeEqualTo(isar_models.TransactionSubType.bip47Notification)
// .and()
// .typeEqualTo(isar_models.TransactionType.incoming))
// .sortByTimestampDesc()
// .findAll();
//
// @override
// Future<String> get currentReceivingAddress async =>
// (await _currentReceivingAddress).value;
//
// Future<isar_models.Address> get _currentReceivingAddress async =>
// (await db
// .getAddresses(walletId)
// .filter()
// .typeEqualTo(isar_models.AddressType.p2wpkh)
// .subTypeEqualTo(isar_models.AddressSubType.receiving)
// .sortByDerivationIndexDesc()
// .findFirst()) ??
// await _generateAddressForChain(0, 0, DerivePathTypeExt.primaryFor(coin));
//
// Future<String> get currentChangeAddress async =>
// (await _currentChangeAddress).value;
//
// Future<isar_models.Address> get _currentChangeAddress async =>
// (await db
// .getAddresses(walletId)
// .filter()
// .typeEqualTo(isar_models.AddressType.p2wpkh)
// .subTypeEqualTo(isar_models.AddressSubType.change)
// .sortByDerivationIndexDesc()
// .findFirst()) ??
// await _generateAddressForChain(1, 0, DerivePathTypeExt.primaryFor(coin));
//
// Future<String> get currentChangeAddressP2PKH async =>
// (await _currentChangeAddressP2PKH).value;
//
// Future<isar_models.Address> get _currentChangeAddressP2PKH async =>
// (await db
// .getAddresses(walletId)
// .filter()
// .typeEqualTo(isar_models.AddressType.p2pkh)
// .subTypeEqualTo(isar_models.AddressSubType.change)
// .sortByDerivationIndexDesc()
// .findFirst()) ??
// await _generateAddressForChain(1, 0, DerivePathType.bip44);
//
// @override
// Future<void> exit() async {
// _hasCalledExit = true;
// timer?.cancel();
// timer = null;
// stopNetworkAlivePinging();
// }
//
//
// bool longMutex = false;
//
// @override
// Future<void> recoverFromMnemonic({
// required String mnemonic,
// String? mnemonicPassphrase,
// required int maxUnusedAddressGap,
// required int maxNumberOfIndexesToCheck,
// required int height,
// }) async {
// longMutex = true;
// final start = DateTime.now();
// try {
// Logging.instance.log("IS_INTEGRATION_TEST: $integrationTestFlag",
// level: LogLevel.Info);
// if (!integrationTestFlag) {
// final features = await electrumXClient.getServerFeatures();
// Logging.instance.log("features: $features", level: LogLevel.Info);
// switch (coin) {
// case Coin.bitcoin:
// if (features['genesis_hash'] != GENESIS_HASH_MAINNET) {
// throw Exception("genesis hash does not match main net!");
// }
// break;
// case Coin.bitcoinTestNet:
// if (features['genesis_hash'] != GENESIS_HASH_TESTNET) {
// throw Exception("genesis hash does not match test net!");
// }
// break;
// default:
// throw Exception(
// "Attempted to generate a BitcoinWallet using a non bitcoin coin type: ${coin.name}");
// }
// }
// // check to make sure we aren't overwriting a mnemonic
// // this should never fail
// if ((await mnemonicString) != null ||
// (await this.mnemonicPassphrase) != null) {
// longMutex = false;
// throw Exception("Attempted to overwrite mnemonic on restore!");
// }
// await _secureStore.write(
// key: '${_walletId}_mnemonic', value: mnemonic.trim());
// await _secureStore.write(
// key: '${_walletId}_mnemonicPassphrase',
// value: mnemonicPassphrase ?? "",
// );
//
// await _recoverWalletFromBIP32SeedPhrase(
// mnemonic: mnemonic.trim(),
// mnemonicPassphrase: mnemonicPassphrase ?? "",
// maxUnusedAddressGap: maxUnusedAddressGap,
// maxNumberOfIndexesToCheck: maxNumberOfIndexesToCheck,
// );
// } catch (e, s) {
// Logging.instance.log(
// "Exception rethrown from recoverFromMnemonic(): $e\n$s",
// level: LogLevel.Error);
// longMutex = false;
// rethrow;
// }
// longMutex = false;
//
// final end = DateTime.now();
// Logging.instance.log(
// "$walletName recovery time: ${end.difference(start).inMilliseconds} millis",
// level: LogLevel.Info);
// }
//
// Future<Tuple3<List<isar_models.Address>, DerivePathType, int>> _checkGaps(
// int maxNumberOfIndexesToCheck,
// int maxUnusedAddressGap,
// int txCountBatchSize,
// bip32.BIP32 root,
// DerivePathType type,
// int chain,
// ) async {
// List<isar_models.Address> addressArray = [];
// int gapCounter = 0;
// int highestIndexWithHistory = 0;
//
// for (int index = 0;
// index < maxNumberOfIndexesToCheck && gapCounter < maxUnusedAddressGap;
// index += txCountBatchSize) {
// List<String> iterationsAddressArray = [];
// Logging.instance.log(
// "index: $index, \t GapCounter $chain ${type.name}: $gapCounter",
// level: LogLevel.Info);
//
// final _id = "k_$index";
// Map<String, String> txCountCallArgs = {};
//
// for (int j = 0; j < txCountBatchSize; j++) {
// final derivePath = constructDerivePath(
// derivePathType: type,
// networkWIF: root.network.wif,
// chain: chain,
// index: index + j,
// );
// final node = await Bip32Utils.getBip32NodeFromRoot(root, derivePath);
//
// String addressString;
// final data = PaymentData(pubkey: node.publicKey);
// isar_models.AddressType addrType;
// switch (type) {
// case DerivePathType.bip44:
// addressString = P2PKH(data: data, network: _network).data.address!;
// addrType = isar_models.AddressType.p2pkh;
// break;
// case DerivePathType.bip49:
// addressString = P2SH(
// data: PaymentData(
// redeem: P2WPKH(data: data, network: _network).data),
// network: _network)
// .data
// .address!;
// addrType = isar_models.AddressType.p2sh;
// break;
// case DerivePathType.bip84:
// addressString = P2WPKH(network: _network, data: data).data.address!;
// addrType = isar_models.AddressType.p2wpkh;
// break;
// default:
// throw Exception("DerivePathType $type not supported");
// }
//
// final address = isar_models.Address(
// walletId: walletId,
// value: addressString,
// publicKey: node.publicKey,
// type: addrType,
// derivationIndex: index + j,
// derivationPath: isar_models.DerivationPath()..value = derivePath,
// subType: chain == 0
// ? isar_models.AddressSubType.receiving
// : isar_models.AddressSubType.change,
// );
//
// addressArray.add(address);
//
// txCountCallArgs.addAll({
// "${_id}_$j": addressString,
// });
// }
//
// // get address tx counts
// final counts = await _getBatchTxCount(addresses: txCountCallArgs);
//
// // check and add appropriate addresses
// for (int k = 0; k < txCountBatchSize; k++) {
// int count = counts["${_id}_$k"]!;
// if (count > 0) {
// iterationsAddressArray.add(txCountCallArgs["${_id}_$k"]!);
//
// // update highest
// highestIndexWithHistory = index + k;
//
// // reset counter
// gapCounter = 0;
// }
//
// // increase counter when no tx history found
// if (count == 0) {
// gapCounter++;
// }
// }
// // cache all the transactions while waiting for the current function to finish.
// unawaited(getTransactionCacheEarly(iterationsAddressArray));
// }
// return Tuple3(addressArray, type, highestIndexWithHistory);
// }
//
// Future<void> getTransactionCacheEarly(List<String> allAddresses) async {
// try {
// final List<Map<String, dynamic>> allTxHashes =
// await _fetchHistory(allAddresses);
// for (final txHash in allTxHashes) {
// try {
// unawaited(cachedElectrumXClient.getTransaction(
// txHash: txHash["tx_hash"] as String,
// verbose: true,
// coin: coin,
// ));
// } catch (e) {
// continue;
// }
// }
// } catch (e) {
// //
// }
// }
//
// Future<void> _recoverWalletFromBIP32SeedPhrase({
// required String mnemonic,
// required String mnemonicPassphrase,
// int maxUnusedAddressGap = 20,
// int maxNumberOfIndexesToCheck = 1000,
// bool isRescan = false,
// }) async {
// longMutex = true;
//
// final root = await Bip32Utils.getBip32Root(
// mnemonic,
// mnemonicPassphrase,
// _network,
// );
//
// final deriveTypes = [
// DerivePathType.bip44,
// DerivePathType.bip49,
// DerivePathType.bip84,
// ];
//
// final List<Future<Tuple3<List<isar_models.Address>, DerivePathType, int>>>
// receiveFutures = [];
// final List<Future<Tuple3<List<isar_models.Address>, DerivePathType, int>>>
// changeFutures = [];
//
// const receiveChain = 0;
// const changeChain = 1;
// const indexZero = 0;
//
// // actual size is 36 due to p2pkh, p2sh, and p2wpkh so 12x3
// const txCountBatchSize = 12;
//
// try {
// // receiving addresses
// Logging.instance.log(
// "checking receiving addresses...",
// level: LogLevel.Info,
// );
//
// for (final type in deriveTypes) {
// receiveFutures.add(
// _checkGaps(
// maxNumberOfIndexesToCheck,
// maxUnusedAddressGap,
// txCountBatchSize,
// root,
// type,
// receiveChain,
// ),
// );
// }
//
// // change addresses
// Logging.instance.log(
// "checking change addresses...",
// level: LogLevel.Info,
// );
// for (final type in deriveTypes) {
// changeFutures.add(
// _checkGaps(
// maxNumberOfIndexesToCheck,
// maxUnusedAddressGap,
// txCountBatchSize,
// root,
// type,
// changeChain,
// ),
// );
// }
//
// // io limitations may require running these linearly instead
// final futuresResult = await Future.wait([
// Future.wait(receiveFutures),
// Future.wait(changeFutures),
// ]);
//
// final receiveResults = futuresResult[0];
// final changeResults = futuresResult[1];
//
// final List<isar_models.Address> addressesToStore = [];
//
// int highestReceivingIndexWithHistory = 0;
// // If restoring a wallet that never received any funds, then set receivingArray manually
// // If we didn't do this, it'd store an empty array
// for (final tuple in receiveResults) {
// if (tuple.item1.isEmpty) {
// final address = await _generateAddressForChain(
// receiveChain,
// indexZero,
// tuple.item2,
// );
// addressesToStore.add(address);
// } else {
// highestReceivingIndexWithHistory =
// max(tuple.item3, highestReceivingIndexWithHistory);
// addressesToStore.addAll(tuple.item1);
// }
// }
//
// int highestChangeIndexWithHistory = 0;
// // If restoring a wallet that never sent any funds with change, then set changeArray
// // manually. If we didn't do this, it'd store an empty array.
// for (final tuple in changeResults) {
// if (tuple.item1.isEmpty) {
// final address = await _generateAddressForChain(
// changeChain,
// indexZero,
// tuple.item2,
// );
// addressesToStore.add(address);
// } else {
// highestChangeIndexWithHistory =
// max(tuple.item3, highestChangeIndexWithHistory);
// addressesToStore.addAll(tuple.item1);
// }
// }
//
// // remove extra addresses to help minimize risk of creating a large gap
// addressesToStore.removeWhere((e) =>
// e.subType == isar_models.AddressSubType.change &&
// e.derivationIndex > highestChangeIndexWithHistory);
// addressesToStore.removeWhere((e) =>
// e.subType == isar_models.AddressSubType.receiving &&
// e.derivationIndex > highestReceivingIndexWithHistory);
//
// if (isRescan) {
// await db.updateOrPutAddresses(addressesToStore);
// } else {
// await db.putAddresses(addressesToStore);
// }
//
// // get own payment code
// // isSegwit does not matter here at all
// final myCode = await getPaymentCode(isSegwit: false);
//
// // refresh transactions to pick up any received notification transactions
// await _refreshNotificationAddressTransactions();
//
// try {
// final Set<String> codesToCheck = {};
// final nym = await PaynymIsApi().nym(myCode.toString());
// if (nym.value != null) {
// for (final follower in nym.value!.followers) {
// codesToCheck.add(follower.code);
// }
// for (final following in nym.value!.following) {
// codesToCheck.add(following.code);
// }
// }
//
// // restore paynym transactions
// await restoreAllHistory(
// maxUnusedAddressGap: maxUnusedAddressGap,
// maxNumberOfIndexesToCheck: maxNumberOfIndexesToCheck,
// paymentCodeStrings: codesToCheck,
// );
// } catch (e, s) {
// Logging.instance.log(
// "Failed to check paynym.is followers/following for history during "
// "bitcoin wallet ($walletId $walletName) "
// "_recoverWalletFromBIP32SeedPhrase: $e/n$s",
// level: LogLevel.Error,
// );
// }
//
// await Future.wait([
// _refreshTransactions(),
// _updateUTXOs(),
// ]);
//
// await Future.wait([
// updateCachedId(walletId),
// updateCachedIsFavorite(false),
// ]);
//
// longMutex = false;
// } catch (e, s) {
// Logging.instance.log(
// "Exception rethrown from _recoverWalletFromBIP32SeedPhrase(): $e\n$s",
// level: LogLevel.Error);
//
// longMutex = false;
// rethrow;
// }
// }
//
// Future<bool> refreshIfThereIsNewData() async {
// if (longMutex) return false;
// if (_hasCalledExit) return false;
// Logging.instance.log("refreshIfThereIsNewData", level: LogLevel.Info);
//
// try {
// bool needsRefresh = false;
// Set<String> txnsToCheck = {};
//
// for (final String txid in txTracker.pendings) {
// if (!txTracker.wasNotifiedConfirmed(txid)) {
// txnsToCheck.add(txid);
// }
// }
//
// for (String txid in txnsToCheck) {
// final txn = await electrumXClient.getTransaction(txHash: txid);
// int confirmations = txn["confirmations"] as int? ?? 0;
// bool isUnconfirmed = confirmations < MINIMUM_CONFIRMATIONS;
// if (!isUnconfirmed) {
// // unconfirmedTxs = {};
// needsRefresh = true;
// break;
// }
// }
// if (!needsRefresh) {
// final allOwnAddresses = await _fetchAllOwnAddresses();
// List<Map<String, dynamic>> allTxs = await _fetchHistory(
// allOwnAddresses.map((e) => e.value).toList(growable: false));
// for (Map<String, dynamic> transaction in allTxs) {
// final txid = transaction['tx_hash'] as String;
// if ((await db
// .getTransactions(walletId)
// .filter()
// .txidMatches(txid)
// .findFirst()) ==
// null) {
// Logging.instance.log(
// " txid not found in address history already ${transaction['tx_hash']}",
// level: LogLevel.Info);
// needsRefresh = true;
// break;
// }
// }
// }
// return needsRefresh;
// } on NoSuchTransactionException catch (e) {
// // TODO: move direct transactions elsewhere
// await db.isar.writeTxn(() async {
// await db.isar.transactions.deleteByTxidWalletId(e.txid, walletId);
// });
// await txTracker.deleteTransaction(e.txid);
// return true;
// } catch (e, s) {
// Logging.instance.log(
// "Exception caught in refreshIfThereIsNewData: $e\n$s",
// level: LogLevel.Error);
// rethrow;
// }
// }
//
// Future<void> getAllTxsToWatch() async {
// if (_hasCalledExit) return;
// List<isar_models.Transaction> unconfirmedTxnsToNotifyPending = [];
// List<isar_models.Transaction> unconfirmedTxnsToNotifyConfirmed = [];
//
// final currentChainHeight = await chainHeight;
//
// final txCount = await db.getTransactions(walletId).count();
//
// const paginateLimit = 50;
//
// for (int i = 0; i < txCount; i += paginateLimit) {
// final transactions = await db
// .getTransactions(walletId)
// .offset(i)
// .limit(paginateLimit)
// .findAll();
// for (final tx in transactions) {
// if (tx.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) {
// // get all transactions that were notified as pending but not as confirmed
// if (txTracker.wasNotifiedPending(tx.txid) &&
// !txTracker.wasNotifiedConfirmed(tx.txid)) {
// unconfirmedTxnsToNotifyConfirmed.add(tx);
// }
// } else {
// // get all transactions that were not notified as pending yet
// if (!txTracker.wasNotifiedPending(tx.txid)) {
// unconfirmedTxnsToNotifyPending.add(tx);
// }
// }
// }
// }
//
// // notify on unconfirmed transactions
// for (final tx in unconfirmedTxnsToNotifyPending) {
// final confirmations = tx.getConfirmations(currentChainHeight);
//
// if (tx.type == isar_models.TransactionType.incoming) {
// CryptoNotificationsEventBus.instance.fire(
// CryptoNotificationEvent(
// title: "Incoming transaction",
// walletId: walletId,
// walletName: walletName,
// date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000),
// shouldWatchForUpdates: confirmations < MINIMUM_CONFIRMATIONS,
// coin: coin,
// txid: tx.txid,
// confirmations: confirmations,
// requiredConfirmations: MINIMUM_CONFIRMATIONS,
// ),
// );
// await txTracker.addNotifiedPending(tx.txid);
// } else if (tx.type == isar_models.TransactionType.outgoing) {
// CryptoNotificationsEventBus.instance.fire(
// CryptoNotificationEvent(
// title: "Sending transaction",
// walletId: walletId,
// date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000),
// shouldWatchForUpdates: confirmations < MINIMUM_CONFIRMATIONS,
// txid: tx.txid,
// confirmations: confirmations,
// requiredConfirmations: MINIMUM_CONFIRMATIONS,
// walletName: walletName,
// coin: coin,
// ),
// );
//
// await txTracker.addNotifiedPending(tx.txid);
// }
// }
//
// // notify on confirmed
// for (final tx in unconfirmedTxnsToNotifyConfirmed) {
// if (tx.type == isar_models.TransactionType.incoming) {
// CryptoNotificationsEventBus.instance.fire(
// CryptoNotificationEvent(
// title: "Incoming transaction confirmed",
// walletId: walletId,
// date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000),
// shouldWatchForUpdates: false,
// txid: tx.txid,
// requiredConfirmations: MINIMUM_CONFIRMATIONS,
// walletName: walletName,
// coin: coin,
// ),
// );
//
// await txTracker.addNotifiedConfirmed(tx.txid);
// } else if (tx.type == isar_models.TransactionType.outgoing) {
// CryptoNotificationsEventBus.instance.fire(
// CryptoNotificationEvent(
// title: "Outgoing transaction confirmed",
// walletId: walletId,
// date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000),
// shouldWatchForUpdates: false,
// txid: tx.txid,
// requiredConfirmations: MINIMUM_CONFIRMATIONS,
// walletName: walletName,
// coin: coin,
// ),
// );
//
// await txTracker.addNotifiedConfirmed(tx.txid);
// }
// }
// }
//
//
// @override
// bool get isRefreshing => refreshMutex;
//
// bool refreshMutex = false;
//
// //TODO Show percentages properly/more consistently
// /// Refreshes display data for the wallet
// @override
// Future<void> refresh() async {
// if (refreshMutex) {
// Logging.instance.log("$walletId $walletName refreshMutex denied",
// level: LogLevel.Info);
// return;
// } else {
// refreshMutex = true;
// }
//
// try {
// GlobalEventBus.instance.fire(
// WalletSyncStatusChangedEvent(
// WalletSyncStatus.syncing,
// walletId,
// coin,
// ),
// );
//
// GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.0, walletId));
//
// GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.1, walletId));
// // isSegwit does not matter here at all
// final myCode = await getPaymentCode(isSegwit: false);
// final Set<String> codesToCheck = {};
// final nym = await PaynymIsApi().nym(myCode.toString());
// if (nym.value != null) {
// for (final follower in nym.value!.followers) {
// codesToCheck.add(follower.code);
// }
// for (final following in nym.value!.following) {
// codesToCheck.add(following.code);
// }
// }
//
// final currentHeight = await chainHeight;
// const storedHeight = 1; //await storedChainHeight;
//
// Logging.instance
// .log("chain height: $currentHeight", level: LogLevel.Info);
// // Logging.instance
// // .log("cached height: $storedHeight", level: LogLevel.Info);
//
// if (currentHeight != storedHeight) {
// GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId));
// await _checkChangeAddressForTransactions();
// await _checkP2PKHChangeAddressForTransactions();
//
// GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.3, walletId));
// await _checkCurrentReceivingAddressesForTransactions();
//
// GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.4, walletId));
// await checkAllCurrentReceivingPaynymAddressesForTransactions();
//
// final fetchFuture = _refreshTransactions();
// GlobalEventBus.instance
// .fire(RefreshPercentChangedEvent(0.50, walletId));
//
// final feeObj = _getFees();
// GlobalEventBus.instance
// .fire(RefreshPercentChangedEvent(0.60, walletId));
//
// GlobalEventBus.instance
// .fire(RefreshPercentChangedEvent(0.70, walletId));
// _feeObject = Future(() => feeObj);
//
// await fetchFuture;
// GlobalEventBus.instance
// .fire(RefreshPercentChangedEvent(0.80, walletId));
//
// await checkForNotificationTransactionsTo(codesToCheck);
// await _updateUTXOs();
// await getAllTxsToWatch();
// GlobalEventBus.instance
// .fire(RefreshPercentChangedEvent(0.90, walletId));
// }
//
// refreshMutex = false;
// GlobalEventBus.instance.fire(RefreshPercentChangedEvent(1.0, walletId));
// GlobalEventBus.instance.fire(
// WalletSyncStatusChangedEvent(
// WalletSyncStatus.synced,
// walletId,
// coin,
// ),
// );
//
// if (shouldAutoSync) {
// timer ??= Timer.periodic(const Duration(seconds: 30), (timer) async {
// Logging.instance.log(
// "Periodic refresh check for $walletId $walletName in object instance: $hashCode",
// level: LogLevel.Info);
// // chain height check currently broken
// // if ((await chainHeight) != (await storedChainHeight)) {
// if (await refreshIfThereIsNewData()) {
// await refresh();
// GlobalEventBus.instance.fire(UpdatedInBackgroundEvent(
// "New data found in $walletId $walletName in background!",
// walletId));
// }
// // }
// });
// }
// } catch (error, strace) {
// refreshMutex = false;
// GlobalEventBus.instance.fire(
// NodeConnectionStatusChangedEvent(
// NodeConnectionStatus.disconnected,
// walletId,
// coin,
// ),
// );
// GlobalEventBus.instance.fire(
// WalletSyncStatusChangedEvent(
// WalletSyncStatus.unableToSync,
// walletId,
// coin,
// ),
// );
// Logging.instance.log(
// "Caught exception in refreshWalletData(): $error\n$strace",
// level: LogLevel.Error);
// }
// }
//
//
// @override
// Future<void> initializeExisting() async {
// // this will add the notification address to the db if it isn't
// // already there for older wallets
// await getMyNotificationAddress();
//
// // await _checkCurrentChangeAddressesForTransactions();
// // await _checkCurrentReceivingAddressesForTransactions();
// }
//
// // hack to add tx to txData before refresh completes
// // required based on current app architecture where we don't properly store
// // transactions locally in a good way
// @override
// Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
// final transaction = isar_models.Transaction(
// walletId: walletId,
// txid: txData["txid"] as String,
// timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
// type: isar_models.TransactionType.outgoing,
// subType: isar_models.TransactionSubType.none,
// // precision may be lost here hence the following amountString
// amount: (txData["recipientAmt"] as Amount).raw.toInt(),
// amountString: (txData["recipientAmt"] as Amount).toJsonString(),
// fee: txData["fee"] as int,
// height: null,
// isCancelled: false,
// isLelantus: false,
// otherData: null,
// slateId: null,
// nonce: null,
// inputs: [],
// outputs: [],
// numberOfMessages: null,
// );
//
// final address = txData["address"] is String
// ? await db.getAddress(walletId, txData["address"] as String)
// : null;
//
// await db.addNewTransactionData(
// [
// Tuple2(transaction, address),
// ],
// walletId,
// );
// }
//
// Future<void> _refreshNotificationAddressTransactions() async {
// final address = await getMyNotificationAddress();
// final hashes = await _fetchHistory([address.value]);
//
// List<Map<String, dynamic>> allTransactions = [];
//
// final currentHeight = await chainHeight;
//
// for (final txHash in hashes) {
// final storedTx = await db
// .getTransactions(walletId)
// .filter()
// .txidEqualTo(txHash["tx_hash"] as String)
// .findFirst();
//
// // TODO: remove bip47Notification type check sometime after Q2 2023
// if (storedTx == null ||
// storedTx.subType ==
// isar_models.TransactionSubType.bip47Notification ||
// !storedTx.isConfirmed(currentHeight, MINIMUM_CONFIRMATIONS)) {
// final tx = await cachedElectrumXClient.getTransaction(
// txHash: txHash["tx_hash"] as String,
// verbose: true,
// coin: coin,
// );
//
// tx["address"] = await db
// .getAddresses(walletId)
// .filter()
// .valueEqualTo(txHash["address"] as String)
// .findFirst();
// tx["height"] = txHash["height"];
// allTransactions.add(tx);
// }
// }
//
// final List<Tuple2<isar_models.Transaction, isar_models.Address?>> txnsData =
// [];
//
// for (final txObject in allTransactions) {
// final data = await parseTransaction(
// txObject,
// cachedElectrumXClient,
// [address],
// coin,
// MINIMUM_CONFIRMATIONS,
// walletId,
// );
//
// txnsData.add(data);
// }
// await db.addNewTransactionData(txnsData, walletId);
// }
//
// Future<void> _refreshTransactions() async {
// final List<isar_models.Address> allAddresses =
// await _fetchAllOwnAddresses();
//
// final Set<Map<String, dynamic>> allTxHashes =
// (await _fetchHistory(allAddresses.map((e) => e.value).toList()))
// .toSet();
//
// // // prefetch/cache
// // Set<String> hashes = {};
// // for (var element in allReceivingTxHashes) {
// // hashes.add(element['tx_hash'] as String);
// // }
// // await fastFetch(hashes.toList());
//
// List<Map<String, dynamic>> allTransactions = [];
//
// final currentHeight = await chainHeight;
//
// for (final txHash in allTxHashes) {
// final storedTx = await db
// .getTransactions(walletId)
// .filter()
// .txidEqualTo(txHash["tx_hash"] as String)
// .findFirst();
//
// // TODO: remove bip47Notification type check sometime after Q2 2023
// if (storedTx == null ||
// storedTx.subType ==
// isar_models.TransactionSubType.bip47Notification ||
// !storedTx.isConfirmed(currentHeight, MINIMUM_CONFIRMATIONS)) {
// final tx = await cachedElectrumXClient.getTransaction(
// txHash: txHash["tx_hash"] as String,
// verbose: true,
// coin: coin,
// );
//
// if (!_duplicateTxCheck(allTransactions, tx["txid"] as String)) {
// tx["address"] = await db
// .getAddresses(walletId)
// .filter()
// .valueEqualTo(txHash["address"] as String)
// .findFirst();
// tx["height"] = txHash["height"];
// allTransactions.add(tx);
// }
// }
// }
//
// // // prefetch/cache
// // Set<String> vHashes = {};
// // for (final txObject in allTransactions) {
// // for (int i = 0; i < (txObject["vin"] as List).length; i++) {
// // final input = txObject["vin"]![i] as Map;
// // final prevTxid = input["txid"] as String;
// // vHashes.add(prevTxid);
// // }
// // }
// // await fastFetch(vHashes.toList());
//
// final List<Tuple2<isar_models.Transaction, isar_models.Address?>> txnsData =
// [];
//
// for (final txObject in allTransactions) {
// final data = await parseTransaction(
// txObject,
// cachedElectrumXClient,
// allAddresses,
// coin,
// MINIMUM_CONFIRMATIONS,
// walletId,
// );
//
// txnsData.add(data);
// }
// await db.addNewTransactionData(txnsData, walletId);
//
// // quick hack to notify manager to call notifyListeners if
// // transactions changed
// if (txnsData.isNotEmpty) {
// GlobalEventBus.instance.fire(
// UpdatedInBackgroundEvent(
// "Transactions updated/added for: $walletId $walletName ",
// walletId,
// ),
// );
// }
// }
//
// int estimateTxFee({required int vSize, required int feeRatePerKB}) {
// return vSize * (feeRatePerKB / 1000).ceil();
// }
//
// /// The coinselection algorithm decides whether or not the user is eligible to make the transaction
// /// with [satoshiAmountToSend] and [selectedTxFeeRate]. If so, it will call buildTrasaction() and return
// /// a map containing the tx hex along with other important information. If not, then it will return
// /// an integer (1 or 2)
// dynamic coinSelection({
// required int satoshiAmountToSend,
// required int selectedTxFeeRate,
// required String recipientAddress,
// required bool coinControl,
// required bool isSendAll,
// int? satsPerVByte,
// int additionalOutputs = 0,
// List<isar_models.UTXO>? utxos,
// }) async {
// Logging.instance
// .log("Starting coinSelection ----------", level: LogLevel.Info);
// final List<isar_models.UTXO> availableOutputs = utxos ?? await this.utxos;
// final currentChainHeight = await chainHeight;
// final List<isar_models.UTXO> spendableOutputs = [];
// int spendableSatoshiValue = 0;
//
// // Build list of spendable outputs and totaling their satoshi amount
// for (final utxo in availableOutputs) {
// if (utxo.isBlocked == false &&
// utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) &&
// utxo.used != true) {
// spendableOutputs.add(utxo);
// spendableSatoshiValue += utxo.value;
// }
// }
//
// if (coinControl) {
// if (spendableOutputs.length < availableOutputs.length) {
// throw ArgumentError("Attempted to use an unavailable utxo");
// }
// }
//
// // don't care about sorting if using all utxos
// if (!coinControl) {
// // sort spendable by age (oldest first)
// spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!));
// }
//
// Logging.instance.log("spendableOutputs.length: ${spendableOutputs.length}",
// level: LogLevel.Info);
// Logging.instance.log("availableOutputs.length: ${availableOutputs.length}",
// level: LogLevel.Info);
// Logging.instance
// .log("spendableOutputs: $spendableOutputs", level: LogLevel.Info);
// Logging.instance.log("spendableSatoshiValue: $spendableSatoshiValue",
// level: LogLevel.Info);
// Logging.instance
// .log("satoshiAmountToSend: $satoshiAmountToSend", level: LogLevel.Info);
// // If the amount the user is trying to send is smaller than the amount that they have spendable,
// // then return 1, which indicates that they have an insufficient balance.
// if (spendableSatoshiValue < satoshiAmountToSend) {
// return 1;
// // If the amount the user wants to send is exactly equal to the amount they can spend, then return
// // 2, which indicates that they are not leaving enough over to pay the transaction fee
// } else if (spendableSatoshiValue == satoshiAmountToSend && !isSendAll) {
// return 2;
// }
// // If neither of these statements pass, we assume that the user has a spendable balance greater
// // than the amount they're attempting to send. Note that this value still does not account for
// // the added transaction fee, which may require an extra input and will need to be checked for
// // later on.
//
// // Possible situation right here
// int satoshisBeingUsed = 0;
// int inputsBeingConsumed = 0;
// List<isar_models.UTXO> utxoObjectsToUse = [];
//
// if (!coinControl) {
// for (var i = 0;
// satoshisBeingUsed < satoshiAmountToSend &&
// i < spendableOutputs.length;
// i++) {
// utxoObjectsToUse.add(spendableOutputs[i]);
// satoshisBeingUsed += spendableOutputs[i].value;
// inputsBeingConsumed += 1;
// }
// for (int i = 0;
// i < additionalOutputs &&
// inputsBeingConsumed < spendableOutputs.length;
// i++) {
// utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]);
// satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value;
// inputsBeingConsumed += 1;
// }
// } else {
// satoshisBeingUsed = spendableSatoshiValue;
// utxoObjectsToUse = spendableOutputs;
// inputsBeingConsumed = spendableOutputs.length;
// }
//
// Logging.instance
// .log("satoshisBeingUsed: $satoshisBeingUsed", level: LogLevel.Info);
// Logging.instance
// .log("inputsBeingConsumed: $inputsBeingConsumed", level: LogLevel.Info);
// Logging.instance
// .log('utxoObjectsToUse: $utxoObjectsToUse', level: LogLevel.Info);
//
// // numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray
// List<String> recipientsArray = [recipientAddress];
// List<int> recipientsAmtArray = [satoshiAmountToSend];
//
// // gather required signing data
// final utxoSigningData = await fetchBuildTxData(utxoObjectsToUse);
//
// if (isSendAll) {
// Logging.instance
// .log("Attempting to send all $coin", level: LogLevel.Info);
//
// final int vSizeForOneOutput = (await buildTransaction(
// utxoSigningData: utxoSigningData,
// recipients: [recipientAddress],
// satoshiAmounts: [satoshisBeingUsed - 1],
// ))["vSize"] as int;
// int feeForOneOutput = satsPerVByte != null
// ? (satsPerVByte * vSizeForOneOutput)
// : estimateTxFee(
// vSize: vSizeForOneOutput,
// feeRatePerKB: selectedTxFeeRate,
// );
//
// if (satsPerVByte == null) {
// final int roughEstimate = roughFeeEstimate(
// spendableOutputs.length,
// 1,
// selectedTxFeeRate,
// ).raw.toInt();
// if (feeForOneOutput < roughEstimate) {
// feeForOneOutput = roughEstimate;
// }
// }
//
// final int amount = satoshiAmountToSend - feeForOneOutput;
// dynamic txn = await buildTransaction(
// utxoSigningData: utxoSigningData,
// recipients: recipientsArray,
// satoshiAmounts: [amount],
// );
// Map<String, dynamic> transactionObject = {
// "hex": txn["hex"],
// "recipient": recipientsArray[0],
// "recipientAmt": Amount(
// rawValue: BigInt.from(amount),
// fractionDigits: coin.decimals,
// ),
// "fee": feeForOneOutput,
// "vSize": txn["vSize"],
// "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(),
// };
// return transactionObject;
// }
//
// final int vSizeForOneOutput;
// try {
// vSizeForOneOutput = (await buildTransaction(
// utxoSigningData: utxoSigningData,
// recipients: [recipientAddress],
// satoshiAmounts: [satoshisBeingUsed - 1],
// ))["vSize"] as int;
// } catch (e) {
// Logging.instance.log("vSizeForOneOutput: $e", level: LogLevel.Error);
// rethrow;
// }
//
// final int vSizeForTwoOutPuts;
// try {
// vSizeForTwoOutPuts = (await buildTransaction(
// utxoSigningData: utxoSigningData,
// recipients: [
// recipientAddress,
// await currentChangeAddress,
// ],
// satoshiAmounts: [
// satoshiAmountToSend,
// max(0, satoshisBeingUsed - satoshiAmountToSend - 1),
// ],
// ))["vSize"] as int;
// } catch (e) {
// Logging.instance.log("vSizeForTwoOutPuts: $e", level: LogLevel.Error);
// rethrow;
// }
//
// // Assume 1 output, only for recipient and no change
// final feeForOneOutput = satsPerVByte != null
// ? (satsPerVByte * vSizeForOneOutput)
// : estimateTxFee(
// vSize: vSizeForOneOutput,
// feeRatePerKB: selectedTxFeeRate,
// );
// // Assume 2 outputs, one for recipient and one for change
// final feeForTwoOutputs = satsPerVByte != null
// ? (satsPerVByte * vSizeForTwoOutPuts)
// : estimateTxFee(
// vSize: vSizeForTwoOutPuts,
// feeRatePerKB: selectedTxFeeRate,
// );
//
// Logging.instance
// .log("feeForTwoOutputs: $feeForTwoOutputs", level: LogLevel.Info);
// Logging.instance
// .log("feeForOneOutput: $feeForOneOutput", level: LogLevel.Info);
//
// if (satoshisBeingUsed - satoshiAmountToSend > feeForOneOutput) {
// if (satoshisBeingUsed - satoshiAmountToSend >
// feeForOneOutput + DUST_LIMIT.raw.toInt()) {
// // Here, we know that theoretically, we may be able to include another output(change) but we first need to
// // factor in the value of this output in satoshis.
// int changeOutputSize =
// satoshisBeingUsed - satoshiAmountToSend - feeForTwoOutputs;
// // We check to see if the user can pay for the new transaction with 2 outputs instead of one. If they can and
// // the second output's size > DUST_LIMIT satoshis, we perform the mechanics required to properly generate and use a new
// // change address.
// if (changeOutputSize > DUST_LIMIT.raw.toInt() &&
// satoshisBeingUsed - satoshiAmountToSend - changeOutputSize ==
// feeForTwoOutputs) {
// // generate new change address if current change address has been used
// await _checkChangeAddressForTransactions();
// final String newChangeAddress = await currentChangeAddress;
//
// int feeBeingPaid =
// satoshisBeingUsed - satoshiAmountToSend - changeOutputSize;
//
// recipientsArray.add(newChangeAddress);
// recipientsAmtArray.add(changeOutputSize);
// // At this point, we have the outputs we're going to use, the amounts to send along with which addresses
// // we intend to send these amounts to. We have enough to send instructions to build the transaction.
// Logging.instance.log('2 outputs in tx', level: LogLevel.Info);
// Logging.instance
// .log('Input size: $satoshisBeingUsed', level: LogLevel.Info);
// Logging.instance.log('Recipient output size: $satoshiAmountToSend',
// level: LogLevel.Info);
// Logging.instance.log('Change Output Size: $changeOutputSize',
// level: LogLevel.Info);
// Logging.instance.log(
// 'Difference (fee being paid): $feeBeingPaid sats',
// level: LogLevel.Info);
// Logging.instance
// .log('Estimated fee: $feeForTwoOutputs', level: LogLevel.Info);
// dynamic txn = await buildTransaction(
// utxoSigningData: utxoSigningData,
// recipients: recipientsArray,
// satoshiAmounts: recipientsAmtArray,
// );
//
// // make sure minimum fee is accurate if that is being used
// if (txn["vSize"] - feeBeingPaid == 1) {
// int changeOutputSize =
// satoshisBeingUsed - satoshiAmountToSend - (txn["vSize"] as int);
// feeBeingPaid =
// satoshisBeingUsed - satoshiAmountToSend - changeOutputSize;
// recipientsAmtArray.removeLast();
// recipientsAmtArray.add(changeOutputSize);
// Logging.instance.log('Adjusted Input size: $satoshisBeingUsed',
// level: LogLevel.Info);
// Logging.instance.log(
// 'Adjusted Recipient output size: $satoshiAmountToSend',
// level: LogLevel.Info);
// Logging.instance.log(
// 'Adjusted Change Output Size: $changeOutputSize',
// level: LogLevel.Info);
// Logging.instance.log(
// 'Adjusted Difference (fee being paid): $feeBeingPaid sats',
// level: LogLevel.Info);
// Logging.instance.log('Adjusted Estimated fee: $feeForTwoOutputs',
// level: LogLevel.Info);
// txn = await buildTransaction(
// utxoSigningData: utxoSigningData,
// recipients: recipientsArray,
// satoshiAmounts: recipientsAmtArray,
// );
// }
//
// Map<String, dynamic> transactionObject = {
// "hex": txn["hex"],
// "recipient": recipientsArray[0],
// "recipientAmt": Amount(
// rawValue: BigInt.from(recipientsAmtArray[0]),
// fractionDigits: coin.decimals,
// ),
// "fee": feeBeingPaid,
// "vSize": txn["vSize"],
// "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(),
// };
// return transactionObject;
// } else {
// // Something went wrong here. It either overshot or undershot the estimated fee amount or the changeOutputSize
// // is smaller than or equal to DUST_LIMIT. Revert to single output transaction.
// Logging.instance.log('1 output in tx', level: LogLevel.Info);
// Logging.instance
// .log('Input size: $satoshisBeingUsed', level: LogLevel.Info);
// Logging.instance.log('Recipient output size: $satoshiAmountToSend',
// level: LogLevel.Info);
// Logging.instance.log(
// 'Difference (fee being paid): ${satoshisBeingUsed - satoshiAmountToSend} sats',
// level: LogLevel.Info);
// Logging.instance
// .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info);
// dynamic txn = await buildTransaction(
// utxoSigningData: utxoSigningData,
// recipients: recipientsArray,
// satoshiAmounts: recipientsAmtArray,
// );
// Map<String, dynamic> transactionObject = {
// "hex": txn["hex"],
// "recipient": recipientsArray[0],
// "recipientAmt": Amount(
// rawValue: BigInt.from(recipientsAmtArray[0]),
// fractionDigits: coin.decimals,
// ),
// "fee": satoshisBeingUsed - satoshiAmountToSend,
// "vSize": txn["vSize"],
// "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(),
// };
// return transactionObject;
// }
// } else {
// // No additional outputs needed since adding one would mean that it'd be smaller than DUST_LIMIT sats
// // which makes it uneconomical to add to the transaction. Here, we pass data directly to instruct
// // the wallet to begin crafting the transaction that the user requested.
// Logging.instance.log('1 output in tx', level: LogLevel.Info);
// Logging.instance
// .log('Input size: $satoshisBeingUsed', level: LogLevel.Info);
// Logging.instance.log('Recipient output size: $satoshiAmountToSend',
// level: LogLevel.Info);
// Logging.instance.log(
// 'Difference (fee being paid): ${satoshisBeingUsed - satoshiAmountToSend} sats',
// level: LogLevel.Info);
// Logging.instance
// .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info);
// dynamic txn = await buildTransaction(
// utxoSigningData: utxoSigningData,
// recipients: recipientsArray,
// satoshiAmounts: recipientsAmtArray,
// );
// Map<String, dynamic> transactionObject = {
// "hex": txn["hex"],
// "recipient": recipientsArray[0],
// "recipientAmt": Amount(
// rawValue: BigInt.from(recipientsAmtArray[0]),
// fractionDigits: coin.decimals,
// ),
// "fee": satoshisBeingUsed - satoshiAmountToSend,
// "vSize": txn["vSize"],
// "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(),
// };
// return transactionObject;
// }
// } else if (satoshisBeingUsed - satoshiAmountToSend == feeForOneOutput) {
// // In this scenario, no additional change output is needed since inputs - outputs equal exactly
// // what we need to pay for fees. Here, we pass data directly to instruct the wallet to begin
// // crafting the transaction that the user requested.
// Logging.instance.log('1 output in tx', level: LogLevel.Info);
// Logging.instance
// .log('Input size: $satoshisBeingUsed', level: LogLevel.Info);
// Logging.instance.log('Recipient output size: $satoshiAmountToSend',
// level: LogLevel.Info);
// Logging.instance.log(
// 'Fee being paid: ${satoshisBeingUsed - satoshiAmountToSend} sats',
// level: LogLevel.Info);
// Logging.instance
// .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info);
// dynamic txn = await buildTransaction(
// utxoSigningData: utxoSigningData,
// recipients: recipientsArray,
// satoshiAmounts: recipientsAmtArray,
// );
// Map<String, dynamic> transactionObject = {
// "hex": txn["hex"],
// "recipient": recipientsArray[0],
// "recipientAmt": Amount(
// rawValue: BigInt.from(recipientsAmtArray[0]),
// fractionDigits: coin.decimals,
// ),
// "fee": feeForOneOutput,
// "vSize": txn["vSize"],
// "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(),
// };
// return transactionObject;
// } else {
// // Remember that returning 2 indicates that the user does not have a sufficient balance to
// // pay for the transaction fee. Ideally, at this stage, we should check if the user has any
// // additional outputs they're able to spend and then recalculate fees.
// Logging.instance.log(
// 'Cannot pay tx fee - checking for more outputs and trying again',
// level: LogLevel.Warning);
// // try adding more outputs
// if (spendableOutputs.length > inputsBeingConsumed) {
// return coinSelection(
// satoshiAmountToSend: satoshiAmountToSend,
// selectedTxFeeRate: selectedTxFeeRate,
// satsPerVByte: satsPerVByte,
// recipientAddress: recipientAddress,
// isSendAll: isSendAll,
// additionalOutputs: additionalOutputs + 1,
// utxos: utxos,
// coinControl: coinControl,
// );
// }
// return 2;
// }
// }
//
// Future<List<SigningData>> fetchBuildTxData(
// List<isar_models.UTXO> utxosToUse,
// ) async {
// // return data
// List<SigningData> signingData = [];
//
// try {
// // Populating the addresses to check
// for (var i = 0; i < utxosToUse.length; i++) {
// if (utxosToUse[i].address == null) {
// final txid = utxosToUse[i].txid;
// final tx = await _cachedElectrumXClient.getTransaction(
// txHash: txid,
// coin: coin,
// );
// for (final output in tx["vout"] as List) {
// final n = output["n"];
// if (n != null && n == utxosToUse[i].vout) {
// utxosToUse[i] = utxosToUse[i].copyWith(
// address: output["scriptPubKey"]?["addresses"]?[0] as String? ??
// output["scriptPubKey"]["address"] as String,
// );
// }
// }
// }
//
// final derivePathType = addressType(address: utxosToUse[i].address!);
//
// signingData.add(
// SigningData(
// derivePathType: derivePathType,
// utxo: utxosToUse[i],
// ),
// );
// }
//
// Map<DerivePathType, Map<String, dynamic>> receiveDerivations = {};
// Map<DerivePathType, Map<String, dynamic>> changeDerivations = {};
//
// for (final sd in signingData) {
// String? pubKey;
// String? wif;
//
// final address = await db.getAddress(walletId, sd.utxo.address!);
// if (address?.derivationPath != null) {
// final bip32.BIP32 node;
// if (address!.subType == isar_models.AddressSubType.paynymReceive) {
// final code = await paymentCodeStringByKey(address.otherData!);
//
// final bip47base = await getBip47BaseNode();
//
// final privateKey = await getPrivateKeyForPaynymReceivingAddress(
// paymentCodeString: code!,
// index: address.derivationIndex,
// );
//
// node = bip32.BIP32.fromPrivateKey(
// privateKey,
// bip47base.chainCode,
// bip32.NetworkType(
// wif: _network.wif,
// bip32: bip32.Bip32Type(
// public: _network.bip32.public,
// private: _network.bip32.private,
// ),
// ),
// );
// } else {
// node = await Bip32Utils.getBip32Node(
// (await mnemonicString)!,
// (await mnemonicPassphrase)!,
// _network,
// address.derivationPath!.value,
// );
// }
//
// wif = node.toWIF();
// pubKey = Format.uint8listToString(node.publicKey);
// }
//
// if (wif == null || pubKey == null) {
// // fetch receiving derivations if null
// receiveDerivations[sd.derivePathType] ??= await _fetchDerivations(
// chain: 0,
// derivePathType: sd.derivePathType,
// );
// final receiveDerivation =
// receiveDerivations[sd.derivePathType]![sd.utxo.address!];
//
// if (receiveDerivation != null) {
// pubKey = receiveDerivation["pubKey"] as String;
// wif = receiveDerivation["wif"] as String;
// } else {
// // fetch change derivations if null
// changeDerivations[sd.derivePathType] ??= await _fetchDerivations(
// chain: 1,
// derivePathType: sd.derivePathType,
// );
// final changeDerivation =
// changeDerivations[sd.derivePathType]![sd.utxo.address!];
// if (changeDerivation != null) {
// pubKey = changeDerivation["pubKey"] as String;
// wif = changeDerivation["wif"] as String;
// }
// }
// }
//
// if (wif != null && pubKey != null) {
// final PaymentData data;
// final Uint8List? redeemScript;
//
// switch (sd.derivePathType) {
// case DerivePathType.bip44:
// data = P2PKH(
// data: PaymentData(
// pubkey: Format.stringToUint8List(pubKey),
// ),
// network: _network,
// ).data;
// redeemScript = null;
// break;
//
// case DerivePathType.bip49:
// final p2wpkh = P2WPKH(
// data: PaymentData(
// pubkey: Format.stringToUint8List(pubKey),
// ),
// network: _network,
// ).data;
// redeemScript = p2wpkh.output;
// data = P2SH(
// data: PaymentData(redeem: p2wpkh),
// network: _network,
// ).data;
// break;
//
// case DerivePathType.bip84:
// data = P2WPKH(
// data: PaymentData(
// pubkey: Format.stringToUint8List(pubKey),
// ),
// network: _network,
// ).data;
// redeemScript = null;
// break;
//
// default:
// throw Exception("DerivePathType unsupported");
// }
//
// final keyPair = ECPair.fromWIF(
// wif,
// network: _network,
// );
//
// sd.redeemScript = redeemScript;
// sd.output = data.output;
// sd.keyPair = keyPair;
// }
// }
//
// return signingData;
// } catch (e, s) {
// Logging.instance
// .log("fetchBuildTxData() threw: $e,\n$s", level: LogLevel.Error);
// rethrow;
// }
// }
//
// /// Builds and signs a transaction
// Future<Map<String, dynamic>> buildTransaction({
// required List<SigningData> utxoSigningData,
// required List<String> recipients,
// required List<int> satoshiAmounts,
// }) async {
// Logging.instance
// .log("Starting buildTransaction ----------", level: LogLevel.Info);
//
// final txb = TransactionBuilder(network: _network);
// txb.setVersion(1);
//
// // Add transaction inputs
// for (var i = 0; i < utxoSigningData.length; i++) {
// final txid = utxoSigningData[i].utxo.txid;
// txb.addInput(
// txid,
// utxoSigningData[i].utxo.vout,
// null,
// utxoSigningData[i].output!,
// );
// }
//
// // Add transaction output
// for (var i = 0; i < recipients.length; i++) {
// txb.addOutput(recipients[i], satoshiAmounts[i]);
// }
//
// try {
// // Sign the transaction accordingly
// for (var i = 0; i < utxoSigningData.length; i++) {
// txb.sign(
// vin: i,
// keyPair: utxoSigningData[i].keyPair!,
// witnessValue: utxoSigningData[i].utxo.value,
// redeemScript: utxoSigningData[i].redeemScript,
// );
// }
// } catch (e, s) {
// Logging.instance.log("Caught exception while signing transaction: $e\n$s",
// level: LogLevel.Error);
// rethrow;
// }
//
// final builtTx = txb.build();
// final vSize = builtTx.virtualSize();
//
// return {"hex": builtTx.toHex(), "vSize": vSize};
// }
//
// @override
// Future<void> fullRescan(
// int maxUnusedAddressGap,
// int maxNumberOfIndexesToCheck,
// ) async {
// Logging.instance.log("Starting full rescan!", level: LogLevel.Info);
// longMutex = true;
// GlobalEventBus.instance.fire(
// WalletSyncStatusChangedEvent(
// WalletSyncStatus.syncing,
// walletId,
// coin,
// ),
// );
//
// // clear cache
// await _cachedElectrumXClient.clearSharedTransactionCache(coin: coin);
//
// // back up data
// // await _rescanBackup();
//
// await db.deleteWalletBlockchainData(walletId);
// await _deleteDerivations();
//
// try {
// final _mnemonic = await mnemonicString;
// final _mnemonicPassphrase = await mnemonicPassphrase;
// if (_mnemonicPassphrase == null) {
// Logging.instance.log(
// "Exception in fullRescan: mnemonic passphrase null, possible migration issue; if using internal builds, delete wallet and restore from seed, if using a release build, please file bug report",
// level: LogLevel.Error);
// }
//
// await _recoverWalletFromBIP32SeedPhrase(
// mnemonic: _mnemonic!,
// mnemonicPassphrase: _mnemonicPassphrase!,
// maxUnusedAddressGap: maxUnusedAddressGap,
// maxNumberOfIndexesToCheck: maxNumberOfIndexesToCheck,
// isRescan: true,
// );
//
// longMutex = false;
// await refresh();
// Logging.instance.log("Full rescan complete!", level: LogLevel.Info);
// GlobalEventBus.instance.fire(
// WalletSyncStatusChangedEvent(
// WalletSyncStatus.synced,
// walletId,
// coin,
// ),
// );
// } catch (e, s) {
// GlobalEventBus.instance.fire(
// WalletSyncStatusChangedEvent(
// WalletSyncStatus.unableToSync,
// walletId,
// coin,
// ),
// );
//
// // restore from backup
// // await _rescanRestore();
//
// longMutex = false;
// Logging.instance.log("Exception rethrown from fullRescan(): $e\n$s",
// level: LogLevel.Error);
// rethrow;
// }
// }
//
// Future<void> _deleteDerivations() async {
// // P2PKH derivations
// await _secureStore.delete(key: "${walletId}_receiveDerivationsP2PKH");
// await _secureStore.delete(key: "${walletId}_changeDerivationsP2PKH");
//
// // P2SH derivations
// await _secureStore.delete(key: "${walletId}_receiveDerivationsP2SH");
// await _secureStore.delete(key: "${walletId}_changeDerivationsP2SH");
//
// // P2WPKH derivations
// await _secureStore.delete(key: "${walletId}_receiveDerivationsP2WPKH");
// await _secureStore.delete(key: "${walletId}_changeDerivationsP2WPKH");
// }
//
// bool isActive = false;
//
// @override
// void Function(bool)? get onIsActiveWalletChanged =>
// (isActive) => this.isActive = isActive;
//
// @override
// Future<Amount> estimateFeeFor(Amount amount, int feeRate) async {
// final available = balance.spendable;
//
// if (available == amount) {
// return amount - (await sweepAllEstimate(feeRate));
// } else if (amount <= Amount.zero || amount > available) {
// return roughFeeEstimate(1, 2, feeRate);
// }
//
// Amount runningBalance = Amount(
// rawValue: BigInt.zero,
// fractionDigits: coin.decimals,
// );
// int inputCount = 0;
// for (final output in (await utxos)) {
// if (!output.isBlocked) {
// runningBalance += Amount(
// rawValue: BigInt.from(output.value),
// fractionDigits: coin.decimals,
// );
// inputCount++;
// if (runningBalance > amount) {
// break;
// }
// }
// }
//
// final oneOutPutFee = roughFeeEstimate(inputCount, 1, feeRate);
// final twoOutPutFee = roughFeeEstimate(inputCount, 2, feeRate);
//
// if (runningBalance - amount > oneOutPutFee) {
// if (runningBalance - amount > oneOutPutFee + DUST_LIMIT) {
// final change = runningBalance - amount - twoOutPutFee;
// if (change > DUST_LIMIT &&
// runningBalance - amount - change == twoOutPutFee) {
// return runningBalance - amount - change;
// } else {
// return runningBalance - amount;
// }
// } else {
// return runningBalance - amount;
// }
// } else if (runningBalance - amount == oneOutPutFee) {
// return oneOutPutFee;
// } else {
// return twoOutPutFee;
// }
// }
//
// Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) {
// return Amount(
// rawValue: BigInt.from(
// ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() *
// (feeRatePerKB / 1000).ceil()),
// fractionDigits: coin.decimals,
// );
// }
//
// Future<Amount> sweepAllEstimate(int feeRate) async {
// int available = 0;
// int inputCount = 0;
// for (final output in (await utxos)) {
// if (!output.isBlocked &&
// output.isConfirmed(storedChainHeight, MINIMUM_CONFIRMATIONS)) {
// available += output.value;
// inputCount++;
// }
// }
//
// // transaction will only have 1 output minus the fee
// final estimatedFee = roughFeeEstimate(inputCount, 1, feeRate);
//
// return Amount(
// rawValue: BigInt.from(available),
// fractionDigits: coin.decimals,
// ) -
// estimatedFee;
// }
//
// @override
// Future<bool> generateNewAddress() async {
// try {
// final currentReceiving = await _currentReceivingAddress;
//
// final newReceivingIndex = currentReceiving.derivationIndex + 1;
//
// // Use new index to derive a new receiving address
// final newReceivingAddress = await _generateAddressForChain(
// 0, newReceivingIndex, DerivePathTypeExt.primaryFor(coin));
//
// // Add that new receiving address
// await db.putAddress(newReceivingAddress);
//
// return true;
// } catch (e, s) {
// Logging.instance.log(
// "Exception rethrown from generateNewAddress(): $e\n$s",
// level: LogLevel.Error);
// return false;
// }
// }
//
// @override
// Future<String> get xpub async {
// final node = await Bip32Utils.getBip32Root(
// (await mnemonic).join(" "),
// await mnemonicPassphrase ?? "",
// _network,
// );
//
// return node.neutered().toBase58();
// }
// }