From d5b82f26f73835fa4f47f4efb84e6bc698c4577c Mon Sep 17 00:00:00 2001
From: Likho <likhojiba@gmail.com>
Date: Tue, 6 Sep 2022 16:17:45 +0200
Subject: [PATCH] WIP: Add bitcoincash

---
 .../add_edit_node_view.dart                   |    2 +
 .../coins/bitcoincash/bitcoincash_wallet.dart | 3013 +++++++++++++++++
 lib/services/coins/coin_service.dart          |   11 +
 lib/utilities/address_utils.dart              |    3 +
 lib/utilities/assets.dart                     |    6 +
 lib/utilities/block_explorers.dart            |    2 +
 lib/utilities/cfcolors.dart                   |    3 +
 lib/utilities/constants.dart                  |    3 +
 lib/utilities/default_nodes.dart              |   16 +
 lib/utilities/enums/coin_enum.dart            |   18 +
 10 files changed, 3077 insertions(+)
 create mode 100644 lib/services/coins/bitcoincash/bitcoincash_wallet.dart

diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart
index 0d1ce7ad8..5a4d03c57 100644
--- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart
+++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart
@@ -115,6 +115,7 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
       case Coin.bitcoin:
       case Coin.dogecoin:
       case Coin.firo:
+      case Coin.bitcoincash:
       case Coin.bitcoinTestNet:
       case Coin.firoTestNet:
       case Coin.dogecoinTestNet:
@@ -527,6 +528,7 @@ class _NodeFormState extends ConsumerState<NodeForm> {
       case Coin.bitcoin:
       case Coin.dogecoin:
       case Coin.firo:
+      case Coin.bitcoincash:
       case Coin.bitcoinTestNet:
       case Coin.firoTestNet:
       case Coin.dogecoinTestNet:
diff --git a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart
new file mode 100644
index 000000000..da45bd84e
--- /dev/null
+++ b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart
@@ -0,0 +1,3013 @@
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+import 'dart:typed_data';
+
+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:crypto/crypto.dart';
+import 'package:decimal/decimal.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter_secure_storage/flutter_secure_storage.dart';
+import 'package:http/http.dart';
+import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart';
+import 'package:stackwallet/electrumx_rpc/electrumx.dart';
+import 'package:stackwallet/hive/db.dart';
+import 'package:stackwallet/models/models.dart' as models;
+import 'package:stackwallet/models/paymint/fee_object_model.dart';
+import 'package:stackwallet/models/paymint/transactions_model.dart';
+import 'package:stackwallet/models/paymint/utxo_model.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/node_service.dart';
+import 'package:stackwallet/services/notifications_api.dart';
+import 'package:stackwallet/services/price.dart';
+import 'package:stackwallet/services/transaction_notification_tracker.dart';
+import 'package:stackwallet/utilities/assets.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/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/prefs.dart';
+import 'package:tuple/tuple.dart';
+import 'package:uuid/uuid.dart';
+
+const int MINIMUM_CONFIRMATIONS = 3;
+const int DUST_LIMIT = 1000000;
+
+const String GENESIS_HASH_MAINNET =
+    "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f";
+const String GENESIS_HASH_TESTNET =
+    "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943";
+
+enum DerivePathType { bip44 }
+
+bip32.BIP32 getBip32Node(int chain, int index, String mnemonic,
+    NetworkType network, DerivePathType derivePathType) {
+  final root = getBip32Root(mnemonic, network);
+
+  final node = getBip32NodeFromRoot(chain, index, root, derivePathType);
+  return node;
+}
+
+/// wrapper for compute()
+bip32.BIP32 getBip32NodeWrapper(
+  Tuple5<int, int, String, NetworkType, DerivePathType> args,
+) {
+  return getBip32Node(
+    args.item1,
+    args.item2,
+    args.item3,
+    args.item4,
+    args.item5,
+  );
+}
+
+bip32.BIP32 getBip32NodeFromRoot(
+    int chain, int index, bip32.BIP32 root, DerivePathType derivePathType) {
+  String coinType;
+  switch (root.network.wif) {
+    case 0x9e: // bch mainnet wif
+      coinType = "145"; // bch mainnet
+      break;
+    default:
+      throw Exception("Invalid Bitcoincash network type used!");
+  }
+  switch (derivePathType) {
+    case DerivePathType.bip44:
+      return root.derivePath("m/44'/$coinType'/0'/$chain/$index");
+    default:
+      throw Exception("DerivePathType must not be null.");
+  }
+}
+
+/// wrapper for compute()
+bip32.BIP32 getBip32NodeFromRootWrapper(
+  Tuple4<int, int, bip32.BIP32, DerivePathType> args,
+) {
+  return getBip32NodeFromRoot(
+    args.item1,
+    args.item2,
+    args.item3,
+    args.item4,
+  );
+}
+
+bip32.BIP32 getBip32Root(String mnemonic, NetworkType network) {
+  final seed = bip39.mnemonicToSeed(mnemonic);
+  final networkType = bip32.NetworkType(
+    wif: network.wif,
+    bip32: bip32.Bip32Type(
+      public: network.bip32.public,
+      private: network.bip32.private,
+    ),
+  );
+
+  final root = bip32.BIP32.fromSeed(seed, networkType);
+  return root;
+}
+
+/// wrapper for compute()
+bip32.BIP32 getBip32RootWrapper(Tuple2<String, NetworkType> args) {
+  return getBip32Root(args.item1, args.item2);
+}
+
+class BitcoinCashWallet extends CoinServiceAPI {
+  static const integrationTestFlag =
+      bool.fromEnvironment("IS_INTEGRATION_TEST");
+  final _prefs = Prefs.instance;
+
+  Timer? timer;
+  late Coin _coin;
+
+  late final TransactionNotificationTracker txTracker;
+
+  NetworkType get _network {
+    switch (coin) {
+      case Coin.bitcoincash:
+        return bitcoincash;
+      default:
+        throw Exception("Bitcoincash network type not set!");
+    }
+  }
+
+  List<UtxoObject> outputsList = [];
+
+  @override
+  Coin get coin => _coin;
+
+  @override
+  Future<List<String>> get allOwnAddresses =>
+      _allOwnAddresses ??= _fetchAllOwnAddresses();
+  Future<List<String>>? _allOwnAddresses;
+
+  Future<UtxoData>? _utxoData;
+  Future<UtxoData> get utxoData => _utxoData ??= _fetchUtxoData();
+
+  @override
+  Future<List<UtxoObject>> get unspentOutputs async =>
+      (await utxoData).unspentOutputArray;
+
+  @override
+  Future<Decimal> get availableBalance async {
+    final data = await utxoData;
+    return Format.satoshisToAmount(
+        data.satoshiBalance - data.satoshiBalanceUnconfirmed);
+  }
+
+  @override
+  Future<Decimal> get pendingBalance async {
+    final data = await utxoData;
+    return Format.satoshisToAmount(data.satoshiBalanceUnconfirmed);
+  }
+
+  @override
+  Future<Decimal> get balanceMinusMaxFee async =>
+      (await availableBalance) -
+      (Decimal.fromInt((await maxFee)) / Decimal.fromInt(Constants.satsPerCoin))
+          .toDecimal();
+
+  @override
+  Future<Decimal> get totalBalance async {
+    if (!isActive) {
+      final totalBalance = DB.instance
+          .get<dynamic>(boxName: walletId, key: 'totalBalance') as int?;
+      if (totalBalance == null) {
+        final data = await utxoData;
+        return Format.satoshisToAmount(data.satoshiBalance);
+      } else {
+        return Format.satoshisToAmount(totalBalance);
+      }
+    }
+    final data = await utxoData;
+    return Format.satoshisToAmount(data.satoshiBalance);
+  }
+
+  @override
+  Future<String> get currentReceivingAddress =>
+      _currentReceivingAddressP2PKH ??=
+          _getCurrentAddressForChain(0, DerivePathType.bip44);
+
+  Future<String>? _currentReceivingAddressP2PKH;
+
+  @override
+  Future<void> exit() async {
+    _hasCalledExit = true;
+    timer?.cancel();
+    timer = null;
+    stopNetworkAlivePinging();
+  }
+
+  bool _hasCalledExit = false;
+
+  @override
+  bool get hasCalledExit => _hasCalledExit;
+
+  @override
+  Future<FeeObject> get fees => _feeObject ??= _getFees();
+  Future<FeeObject>? _feeObject;
+
+  @override
+  Future<int> get maxFee async {
+    final fee = (await fees).fast;
+    final satsFee =
+        Format.satoshisToAmount(fee) * Decimal.fromInt(Constants.satsPerCoin);
+    return satsFee.floor().toBigInt().toInt();
+  }
+
+  @override
+  Future<List<String>> get mnemonic => _getMnemonicList();
+
+  Future<int> get chainHeight async {
+    try {
+      final result = await _electrumXClient.getBlockHeadTip();
+      return result["height"] as int;
+    } catch (e, s) {
+      Logging.instance.log("Exception caught in chainHeight: $e\n$s",
+          level: LogLevel.Error);
+      return -1;
+    }
+  }
+
+  Future<int> get storedChainHeight async {
+    final storedHeight = DB.instance
+        .get<dynamic>(boxName: walletId, key: "storedChainHeight") as int?;
+    return storedHeight ?? 0;
+  }
+
+  Future<void> updateStoredChainHeight({required int newHeight}) async {
+    DB.instance.put<dynamic>(
+        boxName: walletId, key: "storedChainHeight", value: newHeight);
+  }
+
+  DerivePathType addressType({required String address}) {
+    Uint8List? decodeBase58;
+    Segwit? decodeBech32;
+    try {
+      decodeBase58 = bs58check.decode(address);
+    } catch (err) {
+      // Base58check decode fail
+    }
+    if (decodeBase58 != null) {
+      if (decodeBase58[0] == _network.pubKeyHash) {
+        // P2PKH
+        return DerivePathType.bip44;
+      }
+      throw ArgumentError('Invalid version or Network mismatch');
+    } else {
+      try {
+        decodeBech32 = segwit.decode(address);
+      } catch (err) {
+        // Bech32 decode fail
+      }
+      if (_network.bech32 != decodeBech32!.hrp) {
+        throw ArgumentError('Invalid prefix or Network mismatch');
+      }
+      if (decodeBech32.version != 0) {
+        throw ArgumentError('Invalid address version');
+      }
+    }
+    throw ArgumentError('$address has no matching Script');
+  }
+
+  bool longMutex = false;
+
+  @override
+  Future<void> recoverFromMnemonic({
+    required String mnemonic,
+    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.dogecoin:
+            if (features['genesis_hash'] != GENESIS_HASH_MAINNET) {
+              throw Exception("genesis hash does not match main net!");
+            }
+            break;
+          case Coin.dogecoinTestNet:
+            if (features['genesis_hash'] != GENESIS_HASH_TESTNET) {
+              throw Exception("genesis hash does not match test net!");
+            }
+            break;
+          default:
+            throw Exception(
+                "Attempted to generate a BitcoinCashWallet using a non dogecoin coin type: ${coin.name}");
+        }
+      }
+      // check to make sure we aren't overwriting a mnemonic
+      // this should never fail
+      if ((await _secureStore.read(key: '${_walletId}_mnemonic')) != null) {
+        longMutex = false;
+        throw Exception("Attempted to overwrite mnemonic on restore!");
+      }
+      await _secureStore.write(
+          key: '${_walletId}_mnemonic', value: mnemonic.trim());
+      await _recoverWalletFromBIP32SeedPhrase(
+        mnemonic: mnemonic.trim(),
+        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<void> _recoverWalletFromBIP32SeedPhrase({
+    required String mnemonic,
+    int maxUnusedAddressGap = 20,
+    int maxNumberOfIndexesToCheck = 1000,
+  }) async {
+    longMutex = true;
+
+    Map<String, Map<String, String>> p2pkhReceiveDerivations = {};
+    Map<String, Map<String, String>> p2pkhChangeDerivations = {};
+
+    final root = await compute(getBip32RootWrapper, Tuple2(mnemonic, _network));
+
+    List<String> p2pkhReceiveAddressArray = [];
+    int p2pkhReceiveIndex = -1;
+
+    List<String> p2pkhChangeAddressArray = [];
+    int p2pkhChangeIndex = -1;
+
+    // The gap limit will be capped at [maxUnusedAddressGap]
+    int receivingGapCounter = 0;
+    int changeGapCounter = 0;
+
+    // actual size is 12 due to p2pkh so 12x1
+    const txCountBatchSize = 12;
+
+    try {
+      // receiving addresses
+      Logging.instance
+          .log("checking receiving addresses...", level: LogLevel.Info);
+      for (int index = 0;
+          index < maxNumberOfIndexesToCheck &&
+              receivingGapCounter < maxUnusedAddressGap;
+          index += txCountBatchSize) {
+        Logging.instance.log(
+            "index: $index, \t receivingGapCounter: $receivingGapCounter",
+            level: LogLevel.Info);
+
+        final receivingP2pkhID = "k_$index";
+        Map<String, String> txCountCallArgs = {};
+        final Map<String, dynamic> receivingNodes = {};
+
+        for (int j = 0; j < txCountBatchSize; j++) {
+          // bip44 / P2PKH
+          final node44 = await compute(
+            getBip32NodeFromRootWrapper,
+            Tuple4(
+              0,
+              index + j,
+              root,
+              DerivePathType.bip44,
+            ),
+          );
+          final p2pkhReceiveAddress = P2PKH(
+                  data: PaymentData(pubkey: node44.publicKey),
+                  network: _network)
+              .data
+              .address!;
+          receivingNodes.addAll({
+            "${receivingP2pkhID}_$j": {
+              "node": node44,
+              "address": p2pkhReceiveAddress,
+            }
+          });
+          txCountCallArgs.addAll({
+            "${receivingP2pkhID}_$j": p2pkhReceiveAddress,
+          });
+        }
+
+        // get address tx counts
+        final counts = await _getBatchTxCount(addresses: txCountCallArgs);
+
+        // check and add appropriate addresses
+        for (int k = 0; k < txCountBatchSize; k++) {
+          int p2pkhTxCount = counts["${receivingP2pkhID}_$k"]!;
+          if (p2pkhTxCount > 0) {
+            final node = receivingNodes["${receivingP2pkhID}_$k"];
+            // add address to array
+            p2pkhReceiveAddressArray.add(node["address"] as String);
+            // set current index
+            p2pkhReceiveIndex = index + k;
+            // reset counter
+            receivingGapCounter = 0;
+            // add info to derivations
+            p2pkhReceiveDerivations[node["address"] as String] = {
+              "pubKey": Format.uint8listToString(
+                  (node["node"] as bip32.BIP32).publicKey),
+              "wif": (node["node"] as bip32.BIP32).toWIF(),
+            };
+          }
+
+          // increase counter when no tx history found
+          if (p2pkhTxCount == 0) {
+            receivingGapCounter++;
+          }
+        }
+      }
+
+      Logging.instance
+          .log("checking change addresses...", level: LogLevel.Info);
+      // change addresses
+      for (int index = 0;
+          index < maxNumberOfIndexesToCheck &&
+              changeGapCounter < maxUnusedAddressGap;
+          index += txCountBatchSize) {
+        Logging.instance.log(
+            "index: $index, \t changeGapCounter: $changeGapCounter",
+            level: LogLevel.Info);
+        final changeP2pkhID = "k_$index";
+        Map<String, String> args = {};
+        final Map<String, dynamic> changeNodes = {};
+
+        for (int j = 0; j < txCountBatchSize; j++) {
+          // bip44 / P2PKH
+          final node44 = await compute(
+            getBip32NodeFromRootWrapper,
+            Tuple4(
+              1,
+              index + j,
+              root,
+              DerivePathType.bip44,
+            ),
+          );
+          final p2pkhChangeAddress = P2PKH(
+                  data: PaymentData(pubkey: node44.publicKey),
+                  network: _network)
+              .data
+              .address!;
+          changeNodes.addAll({
+            "${changeP2pkhID}_$j": {
+              "node": node44,
+              "address": p2pkhChangeAddress,
+            }
+          });
+          args.addAll({
+            "${changeP2pkhID}_$j": p2pkhChangeAddress,
+          });
+        }
+
+        // get address tx counts
+        final counts = await _getBatchTxCount(addresses: args);
+
+        // check and add appropriate addresses
+        for (int k = 0; k < txCountBatchSize; k++) {
+          int p2pkhTxCount = counts["${changeP2pkhID}_$k"]!;
+          if (p2pkhTxCount > 0) {
+            final node = changeNodes["${changeP2pkhID}_$k"];
+            // add address to array
+            p2pkhChangeAddressArray.add(node["address"] as String);
+            // set current index
+            p2pkhChangeIndex = index + k;
+            // reset counter
+            changeGapCounter = 0;
+            // add info to derivations
+            p2pkhChangeDerivations[node["address"] as String] = {
+              "pubKey": Format.uint8listToString(
+                  (node["node"] as bip32.BIP32).publicKey),
+              "wif": (node["node"] as bip32.BIP32).toWIF(),
+            };
+          }
+
+          // increase counter when no tx history found
+          if (p2pkhTxCount == 0) {
+            changeGapCounter++;
+          }
+        }
+      }
+
+      // save the derivations (if any)
+      if (p2pkhReceiveDerivations.isNotEmpty) {
+        await addDerivations(
+            chain: 0,
+            derivePathType: DerivePathType.bip44,
+            derivationsToAdd: p2pkhReceiveDerivations);
+      }
+      if (p2pkhChangeDerivations.isNotEmpty) {
+        await addDerivations(
+            chain: 1,
+            derivePathType: DerivePathType.bip44,
+            derivationsToAdd: p2pkhChangeDerivations);
+      }
+
+      // 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
+      if (p2pkhReceiveIndex == -1) {
+        final address =
+            await _generateAddressForChain(0, 0, DerivePathType.bip44);
+        p2pkhReceiveAddressArray.add(address);
+        p2pkhReceiveIndex = 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.
+      if (p2pkhChangeIndex == -1) {
+        final address =
+            await _generateAddressForChain(1, 0, DerivePathType.bip44);
+        p2pkhChangeAddressArray.add(address);
+        p2pkhChangeIndex = 0;
+      }
+
+      await DB.instance.put<dynamic>(
+          boxName: walletId,
+          key: 'receivingAddressesP2PKH',
+          value: p2pkhReceiveAddressArray);
+      await DB.instance.put<dynamic>(
+          boxName: walletId,
+          key: 'changeAddressesP2PKH',
+          value: p2pkhChangeAddressArray);
+      await DB.instance.put<dynamic>(
+          boxName: walletId,
+          key: 'receivingIndexP2PKH',
+          value: p2pkhReceiveIndex);
+      await DB.instance.put<dynamic>(
+          boxName: walletId, key: 'changeIndexP2PKH', value: p2pkhChangeIndex);
+      await DB.instance
+          .put<dynamic>(boxName: walletId, key: "id", value: _walletId);
+      await DB.instance
+          .put<dynamic>(boxName: walletId, key: "isFavorite", value: false);
+
+      longMutex = false;
+    } catch (e, s) {
+      Logging.instance.log(
+          "Exception rethrown from _recoverWalletFromBIP32SeedPhrase(): $e\n$s",
+          level: LogLevel.Info);
+
+      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;
+      Logging.instance.log(
+          "notified unconfirmed transactions: ${txTracker.pendings}",
+          level: LogLevel.Info);
+      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);
+        var confirmations = txn["confirmations"];
+        if (confirmations is! int) continue;
+        bool isUnconfirmed = confirmations < MINIMUM_CONFIRMATIONS;
+        if (!isUnconfirmed) {
+          // unconfirmedTxs = {};
+          needsRefresh = true;
+          break;
+        }
+      }
+      if (!needsRefresh) {
+        var allOwnAddresses = await _fetchAllOwnAddresses();
+        List<Map<String, dynamic>> allTxs =
+            await _fetchHistory(allOwnAddresses);
+        final txData = await transactionData;
+        for (Map<String, dynamic> transaction in allTxs) {
+          if (txData.findTransaction(transaction['tx_hash'] as String) ==
+              null) {
+            Logging.instance.log(
+                " txid not found in address history already ${transaction['tx_hash']}",
+                level: LogLevel.Info);
+            needsRefresh = true;
+            break;
+          }
+        }
+      }
+      return needsRefresh;
+    } catch (e, s) {
+      Logging.instance.log(
+          "Exception caught in refreshIfThereIsNewData: $e\n$s",
+          level: LogLevel.Info);
+      rethrow;
+    }
+  }
+
+  Future<void> getAllTxsToWatch(
+    TransactionData txData,
+  ) async {
+    if (_hasCalledExit) return;
+    List<models.Transaction> unconfirmedTxnsToNotifyPending = [];
+    List<models.Transaction> unconfirmedTxnsToNotifyConfirmed = [];
+
+    // Get all unconfirmed incoming transactions
+    for (final chunk in txData.txChunks) {
+      for (final tx in chunk.transactions) {
+        if (tx.confirmedStatus) {
+          if (txTracker.wasNotifiedPending(tx.txid) &&
+              !txTracker.wasNotifiedConfirmed(tx.txid)) {
+            unconfirmedTxnsToNotifyConfirmed.add(tx);
+          }
+        } else {
+          if (!txTracker.wasNotifiedPending(tx.txid)) {
+            unconfirmedTxnsToNotifyPending.add(tx);
+          }
+        }
+      }
+    }
+
+    // notify on new incoming transaction
+    for (final tx in unconfirmedTxnsToNotifyPending) {
+      if (tx.txType == "Received") {
+        NotificationApi.showNotification(
+          title: "Incoming transaction",
+          body: walletName,
+          walletId: walletId,
+          iconAssetName: Assets.svg.iconFor(coin: coin),
+          date: DateTime.now(),
+          shouldWatchForUpdates: tx.confirmations < MINIMUM_CONFIRMATIONS,
+          coinName: coin.name,
+          txid: tx.txid,
+          confirmations: tx.confirmations,
+          requiredConfirmations: MINIMUM_CONFIRMATIONS,
+        );
+        await txTracker.addNotifiedPending(tx.txid);
+      } else if (tx.txType == "Sent") {
+        NotificationApi.showNotification(
+          title: "Sending transaction",
+          body: walletName,
+          walletId: walletId,
+          iconAssetName: Assets.svg.iconFor(coin: coin),
+          date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000),
+          shouldWatchForUpdates: tx.confirmations < MINIMUM_CONFIRMATIONS,
+          coinName: coin.name,
+          txid: tx.txid,
+          confirmations: tx.confirmations,
+          requiredConfirmations: MINIMUM_CONFIRMATIONS,
+        );
+        await txTracker.addNotifiedPending(tx.txid);
+      }
+    }
+
+    // notify on confirmed
+    for (final tx in unconfirmedTxnsToNotifyConfirmed) {
+      if (tx.txType == "Received") {
+        NotificationApi.showNotification(
+          title: "Incoming transaction confirmed",
+          body: walletName,
+          walletId: walletId,
+          iconAssetName: Assets.svg.iconFor(coin: coin),
+          date: DateTime.now(),
+          shouldWatchForUpdates: false,
+          coinName: coin.name,
+        );
+
+        await txTracker.addNotifiedConfirmed(tx.txid);
+      } else if (tx.txType == "Sent") {
+        NotificationApi.showNotification(
+          title: "Outgoing transaction confirmed",
+          body: walletName,
+          walletId: walletId,
+          iconAssetName: Assets.svg.iconFor(coin: coin),
+          date: DateTime.now(),
+          shouldWatchForUpdates: false,
+          coinName: coin.name,
+        );
+        await txTracker.addNotifiedConfirmed(tx.txid);
+      }
+    }
+  }
+
+  bool refreshMutex = false;
+
+  bool _shouldAutoSync = false;
+
+  @override
+  bool get shouldAutoSync => _shouldAutoSync;
+
+  @override
+  set shouldAutoSync(bool shouldAutoSync) {
+    if (_shouldAutoSync != shouldAutoSync) {
+      _shouldAutoSync = shouldAutoSync;
+      if (!shouldAutoSync) {
+        timer?.cancel();
+        timer = null;
+        stopNetworkAlivePinging();
+      } else {
+        startNetworkAlivePinging();
+        refresh();
+      }
+    }
+  }
+
+  //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));
+
+      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) {
+        if (currentHeight != -1) {
+          // -1 failed to fetch current height
+          updateStoredChainHeight(newHeight: currentHeight);
+        }
+
+        GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId));
+        await _checkChangeAddressForTransactions(DerivePathType.bip44);
+
+        GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.3, walletId));
+        await _checkCurrentReceivingAddressesForTransactions();
+
+        final newTxData = _fetchTransactionData();
+        GlobalEventBus.instance
+            .fire(RefreshPercentChangedEvent(0.50, walletId));
+
+        final newUtxoData = _fetchUtxoData();
+        final feeObj = _getFees();
+        GlobalEventBus.instance
+            .fire(RefreshPercentChangedEvent(0.60, walletId));
+
+        _transactionData = Future(() => newTxData);
+
+        GlobalEventBus.instance
+            .fire(RefreshPercentChangedEvent(0.70, walletId));
+        _feeObject = Future(() => feeObj);
+        _utxoData = Future(() => newUtxoData);
+        GlobalEventBus.instance
+            .fire(RefreshPercentChangedEvent(0.80, walletId));
+
+        await getAllTxsToWatch(await newTxData);
+        GlobalEventBus.instance
+            .fire(RefreshPercentChangedEvent(0.90, walletId));
+      }
+
+      GlobalEventBus.instance.fire(RefreshPercentChangedEvent(1.0, walletId));
+      GlobalEventBus.instance.fire(
+        WalletSyncStatusChangedEvent(
+          WalletSyncStatus.synced,
+          walletId,
+          coin,
+        ),
+      );
+      refreshMutex = false;
+
+      if (shouldAutoSync) {
+        timer ??= Timer.periodic(const Duration(seconds: 150), (timer) async {
+          // 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<Map<String, dynamic>> prepareSend({
+    required String address,
+    required int satoshiAmount,
+    Map<String, dynamic>? args,
+  }) async {
+    try {
+      final feeRateType = args?["feeRate"];
+      final feeRateAmount = args?["feeRateAmount"];
+      if (feeRateType is FeeRateType || feeRateAmount is int) {
+        late final int rate;
+        if (feeRateType is FeeRateType) {
+          int fee = 0;
+          final feeObject = await fees;
+          switch (feeRateType) {
+            case FeeRateType.fast:
+              fee = feeObject.fast;
+              break;
+            case FeeRateType.average:
+              fee = feeObject.medium;
+              break;
+            case FeeRateType.slow:
+              fee = feeObject.slow;
+              break;
+          }
+          rate = fee;
+        } else {
+          rate = feeRateAmount as int;
+        }
+        // check for send all
+        bool isSendAll = false;
+        final balance = Format.decimalAmountToSatoshis(await availableBalance);
+        if (satoshiAmount == balance) {
+          isSendAll = true;
+        }
+
+        final result =
+            await coinSelection(satoshiAmount, rate, address, isSendAll);
+        Logging.instance.log("SEND RESULT: $result", level: LogLevel.Info);
+        if (result is int) {
+          switch (result) {
+            case 1:
+              throw Exception("Insufficient balance!");
+            case 2:
+              throw Exception("Insufficient funds to pay for transaction fee!");
+            default:
+              throw Exception("Transaction failed with error code $result");
+          }
+        } else {
+          final hex = result["hex"];
+          if (hex is String) {
+            final fee = result["fee"] as int;
+            final vSize = result["vSize"] as int;
+
+            Logging.instance.log("txHex: $hex", level: LogLevel.Info);
+            Logging.instance.log("fee: $fee", level: LogLevel.Info);
+            Logging.instance.log("vsize: $vSize", level: LogLevel.Info);
+            // fee should never be less than vSize sanity check
+            if (fee < vSize) {
+              throw Exception(
+                  "Error in fee calculation: Transaction fee cannot be less than vSize");
+            }
+            return result as Map<String, dynamic>;
+          } else {
+            throw Exception("sent hex is not a String!!!");
+          }
+        }
+      } else {
+        throw ArgumentError("Invalid fee rate argument provided!");
+      }
+    } catch (e, s) {
+      Logging.instance.log("Exception rethrown from prepareSend(): $e\n$s",
+          level: LogLevel.Error);
+      rethrow;
+    }
+  }
+
+  @override
+  Future<String> confirmSend({dynamic txData}) async {
+    try {
+      Logging.instance.log("confirmSend txData: $txData", level: LogLevel.Info);
+      final txHash = await _electrumXClient.broadcastTransaction(
+          rawTx: txData["hex"] as String);
+      Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info);
+      return txHash;
+    } catch (e, s) {
+      Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s",
+          level: LogLevel.Error);
+      rethrow;
+    }
+  }
+
+  @override
+  Future<String> send({
+    required String toAddress,
+    required int amount,
+    Map<String, String> args = const {},
+  }) async {
+    try {
+      final txData = await prepareSend(
+          address: toAddress, satoshiAmount: amount, args: args);
+      final txHash = await confirmSend(txData: txData);
+      return txHash;
+    } catch (e, s) {
+      Logging.instance
+          .log("Exception rethrown from send(): $e\n$s", level: LogLevel.Error);
+      rethrow;
+    }
+  }
+
+  @override
+  Future<bool> testNetworkConnection() async {
+    try {
+      final result = await _electrumXClient.ping();
+      return result;
+    } catch (_) {
+      return false;
+    }
+  }
+
+  Timer? _networkAliveTimer;
+
+  void startNetworkAlivePinging() {
+    // call once on start right away
+    _periodicPingCheck();
+
+    // then periodically check
+    _networkAliveTimer = Timer.periodic(
+      Constants.networkAliveTimerDuration,
+      (_) async {
+        _periodicPingCheck();
+      },
+    );
+  }
+
+  void _periodicPingCheck() async {
+    bool hasNetwork = await testNetworkConnection();
+    _isConnected = hasNetwork;
+    if (_isConnected != hasNetwork) {
+      NodeConnectionStatus status = hasNetwork
+          ? NodeConnectionStatus.connected
+          : NodeConnectionStatus.disconnected;
+      GlobalEventBus.instance
+          .fire(NodeConnectionStatusChangedEvent(status, walletId, coin));
+    }
+  }
+
+  void stopNetworkAlivePinging() {
+    _networkAliveTimer?.cancel();
+    _networkAliveTimer = null;
+  }
+
+  bool _isConnected = false;
+
+  @override
+  bool get isConnected => _isConnected;
+
+  @override
+  Future<void> initializeNew() async {
+    Logging.instance
+        .log("Generating new ${coin.prettyName} wallet.", level: LogLevel.Info);
+
+    if ((DB.instance.get<dynamic>(boxName: walletId, key: "id")) != null) {
+      throw Exception(
+          "Attempted to initialize a new wallet using an existing wallet ID!");
+    }
+    await _prefs.init();
+    try {
+      await _generateNewWallet();
+    } catch (e, s) {
+      Logging.instance.log("Exception rethrown from initializeNew(): $e\n$s",
+          level: LogLevel.Fatal);
+      rethrow;
+    }
+    await Future.wait([
+      DB.instance.put<dynamic>(boxName: walletId, key: "id", value: _walletId),
+      DB.instance
+          .put<dynamic>(boxName: walletId, key: "isFavorite", value: false),
+    ]);
+  }
+
+  @override
+  Future<void> initializeExisting() async {
+    Logging.instance.log("Opening existing ${coin.prettyName} wallet.",
+        level: LogLevel.Info);
+
+    if ((DB.instance.get<dynamic>(boxName: walletId, key: "id")) == null) {
+      throw Exception(
+          "Attempted to initialize an existing wallet using an unknown wallet ID!");
+    }
+    await _prefs.init();
+    final data =
+        DB.instance.get<dynamic>(boxName: walletId, key: "latest_tx_model")
+            as TransactionData?;
+    if (data != null) {
+      _transactionData = Future(() => data);
+    }
+  }
+
+  @override
+  Future<TransactionData> get transactionData =>
+      _transactionData ??= _fetchTransactionData();
+  Future<TransactionData>? _transactionData;
+
+  @override
+  bool validateAddress(String address) {
+    return Address.validateAddress(address, _network);
+  }
+
+  @override
+  String get walletId => _walletId;
+  late String _walletId;
+
+  @override
+  String get walletName => _walletName;
+  late String _walletName;
+
+  // setter for updating on rename
+  @override
+  set walletName(String newName) => _walletName = newName;
+
+  late ElectrumX _electrumXClient;
+
+  ElectrumX get electrumXClient => _electrumXClient;
+
+  late CachedElectrumX _cachedElectrumXClient;
+
+  CachedElectrumX get cachedElectrumXClient => _cachedElectrumXClient;
+
+  late FlutterSecureStorageInterface _secureStore;
+
+  late PriceAPI _priceAPI;
+
+  BitcoinCashWallet({
+    required String walletId,
+    required String walletName,
+    required Coin coin,
+    required ElectrumX client,
+    required CachedElectrumX cachedClient,
+    required TransactionNotificationTracker tracker,
+    PriceAPI? priceAPI,
+    FlutterSecureStorageInterface? secureStore,
+  }) {
+    txTracker = tracker;
+    _walletId = walletId;
+    _walletName = walletName;
+    _coin = coin;
+    _electrumXClient = client;
+    _cachedElectrumXClient = cachedClient;
+
+    _priceAPI = priceAPI ?? PriceAPI(Client());
+    _secureStore =
+        secureStore ?? const SecureStorageWrapper(FlutterSecureStorage());
+  }
+
+  @override
+  Future<void> updateNode(bool shouldRefresh) async {
+    final failovers = NodeService()
+        .failoverNodesFor(coin: coin)
+        .map((e) => ElectrumXNode(
+              address: e.host,
+              port: e.port,
+              name: e.name,
+              id: e.id,
+              useSSL: e.useSSL,
+            ))
+        .toList();
+    final newNode = await getCurrentNode();
+    _cachedElectrumXClient = CachedElectrumX.from(
+      node: newNode,
+      prefs: _prefs,
+      failovers: failovers,
+    );
+    _electrumXClient = ElectrumX.from(
+      node: newNode,
+      prefs: _prefs,
+      failovers: failovers,
+    );
+
+    if (shouldRefresh) {
+      refresh();
+    }
+  }
+
+  Future<List<String>> _getMnemonicList() async {
+    final mnemonicString =
+        await _secureStore.read(key: '${_walletId}_mnemonic');
+    if (mnemonicString == null) {
+      return [];
+    }
+    final List<String> data = mnemonicString.split(' ');
+    return data;
+  }
+
+  Future<ElectrumXNode> getCurrentNode() async {
+    final node = NodeService().getPrimaryNodeFor(coin: coin) ??
+        DefaultNodes.getNodeFor(coin);
+
+    return ElectrumXNode(
+      address: node.host,
+      port: node.port,
+      name: node.name,
+      useSSL: node.useSSL,
+      id: node.id,
+    );
+  }
+
+  Future<List<String>> _fetchAllOwnAddresses() async {
+    final List<String> allAddresses = [];
+
+    final receivingAddressesP2PKH = DB.instance.get<dynamic>(
+        boxName: walletId, key: 'receivingAddressesP2PKH') as List<dynamic>;
+    final changeAddressesP2PKH =
+        DB.instance.get<dynamic>(boxName: walletId, key: 'changeAddressesP2PKH')
+            as List<dynamic>;
+
+    // for (var i = 0; i < receivingAddresses.length; i++) {
+    //   if (!allAddresses.contains(receivingAddresses[i])) {
+    //     allAddresses.add(receivingAddresses[i]);
+    //   }
+    // }
+    // for (var i = 0; i < changeAddresses.length; i++) {
+    //   if (!allAddresses.contains(changeAddresses[i])) {
+    //     allAddresses.add(changeAddresses[i]);
+    //   }
+    // }
+    for (var i = 0; i < receivingAddressesP2PKH.length; i++) {
+      if (!allAddresses.contains(receivingAddressesP2PKH[i])) {
+        allAddresses.add(receivingAddressesP2PKH[i] as String);
+      }
+    }
+    for (var i = 0; i < changeAddressesP2PKH.length; i++) {
+      if (!allAddresses.contains(changeAddressesP2PKH[i])) {
+        allAddresses.add(changeAddressesP2PKH[i] as String);
+      }
+    }
+    return allAddresses;
+  }
+
+  Future<FeeObject> _getFees() async {
+    try {
+      //TODO adjust numbers for different speeds?
+      const int f = 1, m = 5, s = 20;
+
+      final fast = await electrumXClient.estimateFee(blocks: f);
+      final medium = await electrumXClient.estimateFee(blocks: m);
+      final slow = await electrumXClient.estimateFee(blocks: s);
+
+      final feeObject = FeeObject(
+        numberOfBlocksFast: f,
+        numberOfBlocksAverage: m,
+        numberOfBlocksSlow: s,
+        fast: Format.decimalAmountToSatoshis(fast),
+        medium: Format.decimalAmountToSatoshis(medium),
+        slow: Format.decimalAmountToSatoshis(slow),
+      );
+
+      Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info);
+      return feeObject;
+    } catch (e) {
+      Logging.instance
+          .log("Exception rethrown from _getFees(): $e", level: LogLevel.Error);
+      rethrow;
+    }
+  }
+
+  Future<void> _generateNewWallet() async {
+    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.bitcoincash:
+          if (features['genesis_hash'] != GENESIS_HASH_MAINNET) {
+            throw Exception("genesis hash does not match main net!");
+          }
+          break;
+        default:
+          throw Exception(
+              "Attempted to generate a BitcoinWallet using a non bitcoin coin type: ${coin.name}");
+      }
+    }
+
+    // this should never fail
+    if ((await _secureStore.read(key: '${_walletId}_mnemonic')) != null) {
+      throw Exception(
+          "Attempted to overwrite mnemonic on generate new wallet!");
+    }
+    await _secureStore.write(
+        key: '${_walletId}_mnemonic',
+        value: bip39.generateMnemonic(strength: 256));
+
+    // Set relevant indexes
+    await DB.instance
+        .put<dynamic>(boxName: walletId, key: "receivingIndexP2PKH", value: 0);
+    await DB.instance
+        .put<dynamic>(boxName: walletId, key: "changeIndexP2PKH", value: 0);
+    await DB.instance.put<dynamic>(
+      boxName: walletId,
+      key: 'blocked_tx_hashes',
+      value: ["0xdefault"],
+    ); // A list of transaction hashes to represent frozen utxos in wallet
+    // initialize address book entries
+    await DB.instance.put<dynamic>(
+        boxName: walletId,
+        key: 'addressBookEntries',
+        value: <String, String>{});
+
+    // Generate and add addresses to relevant arrays
+    // final initialReceivingAddress =
+    //     await _generateAddressForChain(0, 0, DerivePathType.bip44);
+    // final initialChangeAddress =
+    //     await _generateAddressForChain(1, 0, DerivePathType.bip44);
+    final initialReceivingAddressP2PKH =
+        await _generateAddressForChain(0, 0, DerivePathType.bip44);
+    final initialChangeAddressP2PKH =
+        await _generateAddressForChain(1, 0, DerivePathType.bip44);
+
+    // await _addToAddressesArrayForChain(
+    //     initialReceivingAddress, 0, DerivePathType.bip44);
+    // await _addToAddressesArrayForChain(
+    //     initialChangeAddress, 1, DerivePathType.bip44);
+    await _addToAddressesArrayForChain(
+        initialReceivingAddressP2PKH, 0, DerivePathType.bip44);
+    await _addToAddressesArrayForChain(
+        initialChangeAddressP2PKH, 1, DerivePathType.bip44);
+
+    // this._currentReceivingAddress = Future(() => initialReceivingAddress);
+    _currentReceivingAddressP2PKH = Future(() => initialReceivingAddressP2PKH);
+
+    Logging.instance.log("_generateNewWalletFinished", level: LogLevel.Info);
+  }
+
+  /// Generates a new internal or external chain address for the wallet using a BIP44 derivation path.
+  /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value!
+  /// [index] - This can be any integer >= 0
+  Future<String> _generateAddressForChain(
+    int chain,
+    int index,
+    DerivePathType derivePathType,
+  ) async {
+    final mnemonic = await _secureStore.read(key: '${_walletId}_mnemonic');
+    final node = await compute(
+      getBip32NodeWrapper,
+      Tuple5(
+        chain,
+        index,
+        mnemonic!,
+        _network,
+        derivePathType,
+      ),
+    );
+    final data = PaymentData(pubkey: node.publicKey);
+    String address;
+
+    switch (derivePathType) {
+      case DerivePathType.bip44:
+        address = P2PKH(data: data, network: _network).data.address!;
+        break;
+      // default:
+      //   // should never hit this due to all enum cases handled
+      //   return null;
+    }
+
+    // add generated address & info to derivations
+    await addDerivation(
+      chain: chain,
+      address: address,
+      pubKey: Format.uint8listToString(node.publicKey),
+      wif: node.toWIF(),
+      derivePathType: derivePathType,
+    );
+
+    return address;
+  }
+
+  /// Increases the index for either the internal or external chain, depending on [chain].
+  /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value!
+  Future<void> _incrementAddressIndexForChain(
+      int chain, DerivePathType derivePathType) async {
+    // Here we assume chain == 1 if it isn't 0
+    String indexKey = chain == 0 ? "receivingIndex" : "changeIndex";
+    switch (derivePathType) {
+      case DerivePathType.bip44:
+        indexKey += "P2PKH";
+        break;
+    }
+
+    final newIndex =
+        (DB.instance.get<dynamic>(boxName: walletId, key: indexKey)) + 1;
+    await DB.instance
+        .put<dynamic>(boxName: walletId, key: indexKey, value: newIndex);
+  }
+
+  /// Adds [address] to the relevant chain's address array, which is determined by [chain].
+  /// [address] - Expects a standard native segwit address
+  /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value!
+  Future<void> _addToAddressesArrayForChain(
+      String address, int chain, DerivePathType derivePathType) async {
+    String chainArray = '';
+    if (chain == 0) {
+      chainArray = 'receivingAddresses';
+    } else {
+      chainArray = 'changeAddresses';
+    }
+    switch (derivePathType) {
+      case DerivePathType.bip44:
+        chainArray += "P2PKH";
+        break;
+    }
+
+    final addressArray =
+        DB.instance.get<dynamic>(boxName: walletId, key: chainArray);
+    if (addressArray == null) {
+      Logging.instance.log(
+          'Attempting to add the following to $chainArray array for chain $chain:${[
+            address
+          ]}',
+          level: LogLevel.Info);
+      await DB.instance
+          .put<dynamic>(boxName: walletId, key: chainArray, value: [address]);
+    } else {
+      // Make a deep copy of the existing list
+      final List<String> newArray = [];
+      addressArray
+          .forEach((dynamic _address) => newArray.add(_address as String));
+      newArray.add(address); // Add the address passed into the method
+      await DB.instance
+          .put<dynamic>(boxName: walletId, key: chainArray, value: newArray);
+    }
+  }
+
+  /// Returns the latest receiving/change (external/internal) address for the wallet depending on [chain]
+  /// and
+  /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value!
+  Future<String> _getCurrentAddressForChain(
+      int chain, DerivePathType derivePathType) async {
+    // Here, we assume that chain == 1 if it isn't 0
+    String arrayKey = chain == 0 ? "receivingAddresses" : "changeAddresses";
+    switch (derivePathType) {
+      case DerivePathType.bip44:
+        arrayKey += "P2PKH";
+        break;
+    }
+    final internalChainArray =
+        DB.instance.get<dynamic>(boxName: walletId, key: arrayKey);
+    return internalChainArray.last as String;
+  }
+
+  String _buildDerivationStorageKey(
+      {required int chain, required DerivePathType derivePathType}) {
+    String key;
+    String chainId = chain == 0 ? "receive" : "change";
+    switch (derivePathType) {
+      case DerivePathType.bip44:
+        key = "${walletId}_${chainId}DerivationsP2PKH";
+        break;
+    }
+    return key;
+  }
+
+  Future<Map<String, dynamic>> _fetchDerivations(
+      {required int chain, required DerivePathType derivePathType}) async {
+    // build lookup key
+    final key = _buildDerivationStorageKey(
+        chain: chain, derivePathType: derivePathType);
+
+    // fetch current derivations
+    final derivationsString = await _secureStore.read(key: key);
+    return Map<String, dynamic>.from(
+        jsonDecode(derivationsString ?? "{}") as Map);
+  }
+
+  /// Add a single derivation to the local secure storage for [chain] and
+  /// [derivePathType] where [chain] must either be 1 for change or 0 for receive.
+  /// This will overwrite a previous entry where the address of the new derivation
+  /// matches a derivation currently stored.
+  Future<void> addDerivation({
+    required int chain,
+    required String address,
+    required String pubKey,
+    required String wif,
+    required DerivePathType derivePathType,
+  }) async {
+    // build lookup key
+    final key = _buildDerivationStorageKey(
+        chain: chain, derivePathType: derivePathType);
+
+    // fetch current derivations
+    final derivationsString = await _secureStore.read(key: key);
+    final derivations =
+        Map<String, dynamic>.from(jsonDecode(derivationsString ?? "{}") as Map);
+
+    // add derivation
+    derivations[address] = {
+      "pubKey": pubKey,
+      "wif": wif,
+    };
+
+    // save derivations
+    final newReceiveDerivationsString = jsonEncode(derivations);
+    await _secureStore.write(key: key, value: newReceiveDerivationsString);
+  }
+
+  /// Add multiple derivations to the local secure storage for [chain] and
+  /// [derivePathType] where [chain] must either be 1 for change or 0 for receive.
+  /// This will overwrite any previous entries where the address of the new derivation
+  /// matches a derivation currently stored.
+  /// The [derivationsToAdd] must be in the format of:
+  /// {
+  ///   addressA : {
+  ///     "pubKey": <the pubKey string>,
+  ///     "wif": <the wif string>,
+  ///   },
+  ///   addressB : {
+  ///     "pubKey": <the pubKey string>,
+  ///     "wif": <the wif string>,
+  ///   },
+  /// }
+  Future<void> addDerivations({
+    required int chain,
+    required DerivePathType derivePathType,
+    required Map<String, dynamic> derivationsToAdd,
+  }) async {
+    // build lookup key
+    final key = _buildDerivationStorageKey(
+        chain: chain, derivePathType: derivePathType);
+
+    // fetch current derivations
+    final derivationsString = await _secureStore.read(key: key);
+    final derivations =
+        Map<String, dynamic>.from(jsonDecode(derivationsString ?? "{}") as Map);
+
+    // add derivation
+    derivations.addAll(derivationsToAdd);
+
+    // save derivations
+    final newReceiveDerivationsString = jsonEncode(derivations);
+    await _secureStore.write(key: key, value: newReceiveDerivationsString);
+  }
+
+  Future<UtxoData> _fetchUtxoData() async {
+    final List<String> allAddresses = await _fetchAllOwnAddresses();
+
+    try {
+      final fetchedUtxoList = <List<Map<String, dynamic>>>[];
+
+      final Map<int, Map<String, List<dynamic>>> batches = {};
+      const batchSizeMax = 10;
+      int batchNumber = 0;
+      for (int i = 0; i < allAddresses.length; i++) {
+        if (batches[batchNumber] == null) {
+          batches[batchNumber] = {};
+        }
+        final scripthash = _convertToScriptHash(allAddresses[i], _network);
+        batches[batchNumber]!.addAll({
+          scripthash: [scripthash]
+        });
+        if (i % batchSizeMax == batchSizeMax - 1) {
+          batchNumber++;
+        }
+      }
+
+      for (int i = 0; i < batches.length; i++) {
+        final response =
+            await _electrumXClient.getBatchUTXOs(args: batches[i]!);
+        for (final entry in response.entries) {
+          if (entry.value.isNotEmpty) {
+            fetchedUtxoList.add(entry.value);
+          }
+        }
+      }
+
+      final priceData =
+          await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency);
+      Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero;
+      final List<Map<String, dynamic>> outputArray = [];
+      int satoshiBalance = 0;
+      int satoshiBalancePending = 0;
+
+      for (int i = 0; i < fetchedUtxoList.length; i++) {
+        for (int j = 0; j < fetchedUtxoList[i].length; j++) {
+          int value = fetchedUtxoList[i][j]["value"] as int;
+          satoshiBalance += value;
+
+          final txn = await cachedElectrumXClient.getTransaction(
+            txHash: fetchedUtxoList[i][j]["tx_hash"] as String,
+            verbose: true,
+            coin: coin,
+          );
+
+          final Map<String, dynamic> utxo = {};
+          final int confirmations = txn["confirmations"] as int? ?? 0;
+          final bool confirmed = txn["confirmations"] == null
+              ? false
+              : txn["confirmations"] as int >= MINIMUM_CONFIRMATIONS;
+          if (!confirmed) {
+            satoshiBalancePending += value;
+          }
+
+          utxo["txid"] = txn["txid"];
+          utxo["vout"] = fetchedUtxoList[i][j]["tx_pos"];
+          utxo["value"] = value;
+
+          utxo["status"] = <String, dynamic>{};
+          utxo["status"]["confirmed"] = confirmed;
+          utxo["status"]["confirmations"] = confirmations;
+          utxo["status"]["block_height"] = fetchedUtxoList[i][j]["height"];
+          utxo["status"]["block_hash"] = txn["blockhash"];
+          utxo["status"]["block_time"] = txn["blocktime"];
+
+          final fiatValue = ((Decimal.fromInt(value) * currentPrice) /
+                  Decimal.fromInt(Constants.satsPerCoin))
+              .toDecimal(scaleOnInfinitePrecision: 2);
+          utxo["rawWorth"] = fiatValue;
+          utxo["fiatWorth"] = fiatValue.toString();
+          outputArray.add(utxo);
+        }
+      }
+
+      Decimal currencyBalanceRaw =
+          ((Decimal.fromInt(satoshiBalance) * currentPrice) /
+                  Decimal.fromInt(Constants.satsPerCoin))
+              .toDecimal(scaleOnInfinitePrecision: 2);
+
+      final Map<String, dynamic> result = {
+        "total_user_currency": currencyBalanceRaw.toString(),
+        "total_sats": satoshiBalance,
+        "total_btc": (Decimal.fromInt(satoshiBalance) /
+                Decimal.fromInt(Constants.satsPerCoin))
+            .toDecimal(scaleOnInfinitePrecision: Constants.decimalPlaces)
+            .toString(),
+        "outputArray": outputArray,
+        "unconfirmed": satoshiBalancePending,
+      };
+
+      final dataModel = UtxoData.fromJson(result);
+
+      final List<UtxoObject> allOutputs = dataModel.unspentOutputArray;
+      Logging.instance
+          .log('Outputs fetched: $allOutputs', level: LogLevel.Info);
+      await _sortOutputs(allOutputs);
+      await DB.instance.put<dynamic>(
+          boxName: walletId, key: 'latest_utxo_model', value: dataModel);
+      await DB.instance.put<dynamic>(
+          boxName: walletId,
+          key: 'totalBalance',
+          value: dataModel.satoshiBalance);
+      return dataModel;
+    } catch (e, s) {
+      Logging.instance
+          .log("Output fetch unsuccessful: $e\n$s", level: LogLevel.Error);
+      final latestTxModel =
+          DB.instance.get<dynamic>(boxName: walletId, key: 'latest_utxo_model');
+
+      if (latestTxModel == null) {
+        final emptyModel = {
+          "total_user_currency": "0.00",
+          "total_sats": 0,
+          "total_btc": "0",
+          "outputArray": <dynamic>[]
+        };
+        return UtxoData.fromJson(emptyModel);
+      } else {
+        Logging.instance
+            .log("Old output model located", level: LogLevel.Warning);
+        return latestTxModel as models.UtxoData;
+      }
+    }
+  }
+
+  /// Takes in a list of UtxoObjects and adds a name (dependent on object index within list)
+  /// and checks for the txid associated with the utxo being blocked and marks it accordingly.
+  /// Now also checks for output labeling.
+  Future<void> _sortOutputs(List<UtxoObject> utxos) async {
+    final blockedHashArray =
+        DB.instance.get<dynamic>(boxName: walletId, key: 'blocked_tx_hashes')
+            as List<dynamic>?;
+    final List<String> lst = [];
+    if (blockedHashArray != null) {
+      for (var hash in blockedHashArray) {
+        lst.add(hash as String);
+      }
+    }
+    final labels =
+        DB.instance.get<dynamic>(boxName: walletId, key: 'labels') as Map? ??
+            {};
+
+    outputsList = [];
+
+    for (var i = 0; i < utxos.length; i++) {
+      if (labels[utxos[i].txid] != null) {
+        utxos[i].txName = labels[utxos[i].txid] as String? ?? "";
+      } else {
+        utxos[i].txName = 'Output #$i';
+      }
+
+      if (utxos[i].status.confirmed == false) {
+        outputsList.add(utxos[i]);
+      } else {
+        if (lst.contains(utxos[i].txid)) {
+          utxos[i].blocked = true;
+          outputsList.add(utxos[i]);
+        } else if (!lst.contains(utxos[i].txid)) {
+          outputsList.add(utxos[i]);
+        }
+      }
+    }
+  }
+
+  Future<int> getTxCount({required String address}) async {
+    String? scripthash;
+    try {
+      scripthash = _convertToScriptHash(address, _network);
+      final transactions =
+          await electrumXClient.getHistory(scripthash: scripthash);
+      return transactions.length;
+    } catch (e) {
+      Logging.instance.log(
+          "Exception rethrown in _getTxCount(address: $address, scripthash: $scripthash): $e",
+          level: LogLevel.Error);
+      rethrow;
+    }
+  }
+
+  Future<Map<String, int>> _getBatchTxCount({
+    required Map<String, String> addresses,
+  }) async {
+    try {
+      final Map<String, List<dynamic>> args = {};
+      for (final entry in addresses.entries) {
+        args[entry.key] = [_convertToScriptHash(entry.value, _network)];
+      }
+      final response = await electrumXClient.getBatchHistory(args: args);
+
+      final Map<String, int> result = {};
+      for (final entry in response.entries) {
+        result[entry.key] = entry.value.length;
+      }
+      return result;
+    } catch (e, s) {
+      Logging.instance.log(
+          "Exception rethrown in _getBatchTxCount(address: $addresses: $e\n$s",
+          level: LogLevel.Error);
+      rethrow;
+    }
+  }
+
+  Future<void> _checkReceivingAddressForTransactions(
+      DerivePathType derivePathType) async {
+    try {
+      final String currentExternalAddr =
+          await _getCurrentAddressForChain(0, derivePathType);
+      final int txCount = await getTxCount(address: currentExternalAddr);
+      Logging.instance.log(
+          'Number of txs for current receiving address $currentExternalAddr: $txCount',
+          level: LogLevel.Info);
+
+      if (txCount >= 1) {
+        // First increment the receiving index
+        await _incrementAddressIndexForChain(0, derivePathType);
+
+        // Check the new receiving index
+        String indexKey = "receivingIndex";
+        switch (derivePathType) {
+          case DerivePathType.bip44:
+            indexKey += "P2PKH";
+            break;
+        }
+        final newReceivingIndex =
+            DB.instance.get<dynamic>(boxName: walletId, key: indexKey) as int;
+
+        // Use new index to derive a new receiving address
+        final newReceivingAddress = await _generateAddressForChain(
+            0, newReceivingIndex, derivePathType);
+
+        // Add that new receiving address to the array of receiving addresses
+        await _addToAddressesArrayForChain(
+            newReceivingAddress, 0, derivePathType);
+
+        // Set the new receiving address that the service
+
+        switch (derivePathType) {
+          case DerivePathType.bip44:
+            _currentReceivingAddressP2PKH = Future(() => newReceivingAddress);
+            break;
+        }
+      }
+    } on SocketException catch (se, s) {
+      Logging.instance.log(
+          "SocketException caught in _checkReceivingAddressForTransactions($derivePathType): $se\n$s",
+          level: LogLevel.Error);
+      return;
+    } catch (e, s) {
+      Logging.instance.log(
+          "Exception rethrown from _checkReceivingAddressForTransactions($derivePathType): $e\n$s",
+          level: LogLevel.Error);
+      rethrow;
+    }
+  }
+
+  Future<void> _checkChangeAddressForTransactions(
+      DerivePathType derivePathType) async {
+    try {
+      final String currentExternalAddr =
+          await _getCurrentAddressForChain(1, derivePathType);
+      final int txCount = await getTxCount(address: currentExternalAddr);
+      Logging.instance.log(
+          'Number of txs for current change address $currentExternalAddr: $txCount',
+          level: LogLevel.Info);
+
+      if (txCount >= 1) {
+        // First increment the change index
+        await _incrementAddressIndexForChain(1, derivePathType);
+
+        // Check the new change index
+        String indexKey = "changeIndex";
+        switch (derivePathType) {
+          case DerivePathType.bip44:
+            indexKey += "P2PKH";
+            break;
+        }
+        final newChangeIndex =
+            DB.instance.get<dynamic>(boxName: walletId, key: indexKey) as int;
+
+        // Use new index to derive a new change address
+        final newChangeAddress =
+            await _generateAddressForChain(1, newChangeIndex, derivePathType);
+
+        // Add that new receiving address to the array of change addresses
+        await _addToAddressesArrayForChain(newChangeAddress, 1, derivePathType);
+      }
+    } catch (e, s) {
+      Logging.instance.log(
+          "Exception rethrown from _checkChangeAddressForTransactions($derivePathType): $e\n$s",
+          level: LogLevel.Error);
+      rethrow;
+    }
+  }
+
+  Future<void> _checkCurrentReceivingAddressesForTransactions() async {
+    try {
+      for (final type in DerivePathType.values) {
+        await _checkReceivingAddressForTransactions(type);
+      }
+    } catch (e, s) {
+      Logging.instance.log(
+          "Exception rethrown from _checkCurrentReceivingAddressesForTransactions(): $e\n$s",
+          level: LogLevel.Info);
+      rethrow;
+    }
+  }
+
+  /// public wrapper because dart can't test private...
+  Future<void> checkCurrentReceivingAddressesForTransactions() async {
+    if (Platform.environment["FLUTTER_TEST"] == "true") {
+      try {
+        return _checkCurrentReceivingAddressesForTransactions();
+      } catch (_) {
+        rethrow;
+      }
+    }
+  }
+
+  Future<void> _checkCurrentChangeAddressesForTransactions() async {
+    try {
+      for (final type in DerivePathType.values) {
+        await _checkChangeAddressForTransactions(type);
+      }
+    } catch (e, s) {
+      Logging.instance.log(
+          "Exception rethrown from _checkCurrentChangeAddressesForTransactions(): $e\n$s",
+          level: LogLevel.Error);
+      rethrow;
+    }
+  }
+
+  /// public wrapper because dart can't test private...
+  Future<void> checkCurrentChangeAddressesForTransactions() async {
+    if (Platform.environment["FLUTTER_TEST"] == "true") {
+      try {
+        return _checkCurrentChangeAddressesForTransactions();
+      } catch (_) {
+        rethrow;
+      }
+    }
+  }
+
+  /// attempts to convert a string to a valid scripthash
+  ///
+  /// Returns the scripthash or throws an exception on invalid bch address
+  String _convertToScriptHash(String bchAddress, NetworkType network) {
+    try {
+      final output = Address.addressToOutputScript(bchAddress, network);
+      final hash = sha256.convert(output.toList(growable: false)).toString();
+
+      final chars = hash.split("");
+      final reversedPairs = <String>[];
+      var i = chars.length - 1;
+      while (i > 0) {
+        reversedPairs.add(chars[i - 1]);
+        reversedPairs.add(chars[i]);
+        i -= 2;
+      }
+      return reversedPairs.join("");
+    } catch (e) {
+      rethrow;
+    }
+  }
+
+  Future<List<Map<String, dynamic>>> _fetchHistory(
+      List<String> allAddresses) async {
+    try {
+      List<Map<String, dynamic>> allTxHashes = [];
+
+      final Map<int, Map<String, List<dynamic>>> batches = {};
+      final Map<String, String> requestIdToAddressMap = {};
+      const batchSizeMax = 10;
+      int batchNumber = 0;
+      for (int i = 0; i < allAddresses.length; i++) {
+        if (batches[batchNumber] == null) {
+          batches[batchNumber] = {};
+        }
+        final scripthash = _convertToScriptHash(allAddresses[i], _network);
+        final id = const Uuid().v1();
+        requestIdToAddressMap[id] = allAddresses[i];
+        batches[batchNumber]!.addAll({
+          id: [scripthash]
+        });
+        if (i % batchSizeMax == batchSizeMax - 1) {
+          batchNumber++;
+        }
+      }
+
+      for (int i = 0; i < batches.length; i++) {
+        final response =
+            await _electrumXClient.getBatchHistory(args: batches[i]!);
+        for (final entry in response.entries) {
+          for (int j = 0; j < entry.value.length; j++) {
+            entry.value[j]["address"] = requestIdToAddressMap[entry.key];
+            if (!allTxHashes.contains(entry.value[j])) {
+              allTxHashes.add(entry.value[j]);
+            }
+          }
+        }
+      }
+
+      return allTxHashes;
+    } catch (e, s) {
+      Logging.instance.log("_fetchHistory: $e\n$s", level: LogLevel.Error);
+      rethrow;
+    }
+  }
+
+  bool _duplicateTxCheck(
+      List<Map<String, dynamic>> allTransactions, String txid) {
+    for (int i = 0; i < allTransactions.length; i++) {
+      if (allTransactions[i]["txid"] == txid) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  Future<TransactionData> _fetchTransactionData() async {
+    final List<String> allAddresses = await _fetchAllOwnAddresses();
+
+    final changeAddressesP2PKH =
+        DB.instance.get<dynamic>(boxName: walletId, key: 'changeAddressesP2PKH')
+            as List<dynamic>;
+
+    final List<Map<String, dynamic>> allTxHashes =
+        await _fetchHistory(allAddresses);
+
+    final cachedTransactions =
+        DB.instance.get<dynamic>(boxName: walletId, key: 'latest_tx_model')
+            as TransactionData?;
+    int latestTxnBlockHeight =
+        DB.instance.get<dynamic>(boxName: walletId, key: "storedTxnDataHeight")
+                as int? ??
+            0;
+
+    final unconfirmedCachedTransactions =
+        cachedTransactions?.getAllTransactions() ?? {};
+    unconfirmedCachedTransactions
+        .removeWhere((key, value) => value.confirmedStatus);
+
+    if (cachedTransactions != null) {
+      for (final tx in allTxHashes.toList(growable: false)) {
+        final txHeight = tx["height"] as int;
+        if (txHeight > 0 &&
+            txHeight < latestTxnBlockHeight - MINIMUM_CONFIRMATIONS) {
+          if (unconfirmedCachedTransactions[tx["tx_hash"] as String] == null) {
+            allTxHashes.remove(tx);
+          }
+        }
+      }
+    }
+
+    List<Map<String, dynamic>> allTransactions = [];
+
+    for (final txHash in allTxHashes) {
+      final tx = await cachedElectrumXClient.getTransaction(
+        txHash: txHash["tx_hash"] as String,
+        verbose: true,
+        coin: coin,
+      );
+
+      // Logging.instance.log("TRANSACTION: ${jsonEncode(tx)}");
+      // TODO fix this for sent to self transactions?
+      if (!_duplicateTxCheck(allTransactions, tx["txid"] as String)) {
+        tx["address"] = txHash["address"];
+        tx["height"] = txHash["height"];
+        allTransactions.add(tx);
+      }
+    }
+
+    Logging.instance.log("addAddresses: $allAddresses", level: LogLevel.Info);
+    Logging.instance.log("allTxHashes: $allTxHashes", level: LogLevel.Info);
+
+    Logging.instance.log("allTransactions length: ${allTransactions.length}",
+        level: LogLevel.Info);
+
+    final priceData =
+        await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency);
+    Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero;
+    final List<Map<String, dynamic>> midSortedArray = [];
+
+    for (final txObject in allTransactions) {
+      List<String> sendersArray = [];
+      List<String> recipientsArray = [];
+
+      // Usually only has value when txType = 'Send'
+      int inputAmtSentFromWallet = 0;
+      // Usually has value regardless of txType due to change addresses
+      int outputAmtAddressedToWallet = 0;
+      int fee = 0;
+
+      Map<String, dynamic> midSortedTx = {};
+
+      for (int i = 0; i < (txObject["vin"] as List).length; i++) {
+        final input = txObject["vin"][i] as Map;
+        final prevTxid = input["txid"] as String;
+        final prevOut = input["vout"] as int;
+
+        final tx = await _cachedElectrumXClient.getTransaction(
+            txHash: prevTxid, coin: coin);
+
+        for (final out in tx["vout"] as List) {
+          if (prevOut == out["n"]) {
+            final address = out["scriptPubKey"]["addresses"][0] as String?;
+            if (address != null) {
+              sendersArray.add(address);
+            }
+          }
+        }
+      }
+
+      Logging.instance.log("sendersArray: $sendersArray", level: LogLevel.Info);
+
+      for (final output in txObject["vout"] as List) {
+        final address = output["scriptPubKey"]["addresses"][0] as String?;
+        if (address != null) {
+          recipientsArray.add(address);
+        }
+      }
+
+      Logging.instance
+          .log("recipientsArray: $recipientsArray", level: LogLevel.Info);
+
+      final foundInSenders =
+          allAddresses.any((element) => sendersArray.contains(element));
+      Logging.instance
+          .log("foundInSenders: $foundInSenders", level: LogLevel.Info);
+
+      // If txType = Sent, then calculate inputAmtSentFromWallet
+      if (foundInSenders) {
+        int totalInput = 0;
+        for (int i = 0; i < (txObject["vin"] as List).length; i++) {
+          final input = txObject["vin"][i] as Map;
+          final prevTxid = input["txid"] as String;
+          final prevOut = input["vout"] as int;
+          final tx = await _cachedElectrumXClient.getTransaction(
+            txHash: prevTxid,
+            coin: coin,
+          );
+
+          for (final out in tx["vout"] as List) {
+            if (prevOut == out["n"]) {
+              inputAmtSentFromWallet +=
+                  (Decimal.parse(out["value"].toString()) *
+                          Decimal.fromInt(Constants.satsPerCoin))
+                      .toBigInt()
+                      .toInt();
+            }
+          }
+        }
+        totalInput = inputAmtSentFromWallet;
+        int totalOutput = 0;
+
+        for (final output in txObject["vout"] as List) {
+          final address = output["scriptPubKey"]["addresses"][0];
+          final value = output["value"];
+          final _value = (Decimal.parse(value.toString()) *
+                  Decimal.fromInt(Constants.satsPerCoin))
+              .toBigInt()
+              .toInt();
+          totalOutput += _value;
+          if (changeAddressesP2PKH.contains(address)) {
+            inputAmtSentFromWallet -= _value;
+          } else {
+            // change address from 'sent from' to the 'sent to' address
+            txObject["address"] = address;
+          }
+        }
+        // calculate transaction fee
+        fee = totalInput - totalOutput;
+        // subtract fee from sent to calculate correct value of sent tx
+        inputAmtSentFromWallet -= fee;
+      } else {
+        // counters for fee calculation
+        int totalOut = 0;
+        int totalIn = 0;
+
+        // add up received tx value
+        for (final output in txObject["vout"] as List) {
+          final address = output["scriptPubKey"]["addresses"][0];
+          if (address != null) {
+            final value = (Decimal.parse(output["value"].toString()) *
+                    Decimal.fromInt(Constants.satsPerCoin))
+                .toBigInt()
+                .toInt();
+            totalOut += value;
+            if (allAddresses.contains(address)) {
+              outputAmtAddressedToWallet += value;
+            }
+          }
+        }
+
+        // calculate fee for received tx
+        for (int i = 0; i < (txObject["vin"] as List).length; i++) {
+          final input = txObject["vin"][i] as Map;
+          final prevTxid = input["txid"] as String;
+          final prevOut = input["vout"] as int;
+          final tx = await _cachedElectrumXClient.getTransaction(
+            txHash: prevTxid,
+            coin: coin,
+          );
+
+          for (final out in tx["vout"] as List) {
+            if (prevOut == out["n"]) {
+              totalIn += (Decimal.parse(out["value"].toString()) *
+                      Decimal.fromInt(Constants.satsPerCoin))
+                  .toBigInt()
+                  .toInt();
+            }
+          }
+        }
+        fee = totalIn - totalOut;
+      }
+
+      // create final tx map
+      midSortedTx["txid"] = txObject["txid"];
+      midSortedTx["confirmed_status"] = (txObject["confirmations"] != null) &&
+          (txObject["confirmations"] as int >= MINIMUM_CONFIRMATIONS);
+      midSortedTx["confirmations"] = txObject["confirmations"] ?? 0;
+      midSortedTx["timestamp"] = txObject["blocktime"] ??
+          (DateTime.now().millisecondsSinceEpoch ~/ 1000);
+
+      if (foundInSenders) {
+        midSortedTx["txType"] = "Sent";
+        midSortedTx["amount"] = inputAmtSentFromWallet;
+        final String worthNow =
+            ((currentPrice * Decimal.fromInt(inputAmtSentFromWallet)) /
+                    Decimal.fromInt(Constants.satsPerCoin))
+                .toDecimal(scaleOnInfinitePrecision: 2)
+                .toStringAsFixed(2);
+        midSortedTx["worthNow"] = worthNow;
+        midSortedTx["worthAtBlockTimestamp"] = worthNow;
+      } else {
+        midSortedTx["txType"] = "Received";
+        midSortedTx["amount"] = outputAmtAddressedToWallet;
+        final worthNow =
+            ((currentPrice * Decimal.fromInt(outputAmtAddressedToWallet)) /
+                    Decimal.fromInt(Constants.satsPerCoin))
+                .toDecimal(scaleOnInfinitePrecision: 2)
+                .toStringAsFixed(2);
+        midSortedTx["worthNow"] = worthNow;
+      }
+      midSortedTx["aliens"] = <dynamic>[];
+      midSortedTx["fees"] = fee;
+      midSortedTx["address"] = txObject["address"];
+      midSortedTx["inputSize"] = txObject["vin"].length;
+      midSortedTx["outputSize"] = txObject["vout"].length;
+      midSortedTx["inputs"] = txObject["vin"];
+      midSortedTx["outputs"] = txObject["vout"];
+
+      final int height = txObject["height"] as int;
+      midSortedTx["height"] = height;
+
+      if (height >= latestTxnBlockHeight) {
+        latestTxnBlockHeight = height;
+      }
+
+      midSortedArray.add(midSortedTx);
+    }
+
+    // sort by date  ----  //TODO not sure if needed
+    // shouldn't be any issues with a null timestamp but I got one at some point?
+    midSortedArray
+        .sort((a, b) => (b["timestamp"] as int) - (a["timestamp"] as int));
+    // {
+    //   final aT = a["timestamp"];
+    //   final bT = b["timestamp"];
+    //
+    //   if (aT == null && bT == null) {
+    //     return 0;
+    //   } else if (aT == null) {
+    //     return -1;
+    //   } else if (bT == null) {
+    //     return 1;
+    //   } else {
+    //     return bT - aT;
+    //   }
+    // });
+
+    // buildDateTimeChunks
+    final Map<String, dynamic> result = {"dateTimeChunks": <dynamic>[]};
+    final dateArray = <dynamic>[];
+
+    for (int i = 0; i < midSortedArray.length; i++) {
+      final txObject = midSortedArray[i];
+      final date = extractDateFromTimestamp(txObject["timestamp"] as int);
+      final txTimeArray = [txObject["timestamp"], date];
+
+      if (dateArray.contains(txTimeArray[1])) {
+        result["dateTimeChunks"].forEach((dynamic chunk) {
+          if (extractDateFromTimestamp(chunk["timestamp"] as int) ==
+              txTimeArray[1]) {
+            if (chunk["transactions"] == null) {
+              chunk["transactions"] = <Map<String, dynamic>>[];
+            }
+            chunk["transactions"].add(txObject);
+          }
+        });
+      } else {
+        dateArray.add(txTimeArray[1]);
+        final chunk = {
+          "timestamp": txTimeArray[0],
+          "transactions": [txObject],
+        };
+        result["dateTimeChunks"].add(chunk);
+      }
+    }
+
+    final transactionsMap = cachedTransactions?.getAllTransactions() ?? {};
+    transactionsMap
+        .addAll(TransactionData.fromJson(result).getAllTransactions());
+
+    final txModel = TransactionData.fromMap(transactionsMap);
+
+    await DB.instance.put<dynamic>(
+        boxName: walletId,
+        key: 'storedTxnDataHeight',
+        value: latestTxnBlockHeight);
+    await DB.instance.put<dynamic>(
+        boxName: walletId, key: 'latest_tx_model', value: txModel);
+
+    return txModel;
+  }
+
+  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(int satoshiAmountToSend, int selectedTxFeeRate,
+      String _recipientAddress, bool isSendAll,
+      {int additionalOutputs = 0, List<UtxoObject>? utxos}) async {
+    Logging.instance
+        .log("Starting coinSelection ----------", level: LogLevel.Info);
+    final List<UtxoObject> availableOutputs = utxos ?? outputsList;
+    final List<UtxoObject> spendableOutputs = [];
+    int spendableSatoshiValue = 0;
+
+    // Build list of spendable outputs and totaling their satoshi amount
+    for (var i = 0; i < availableOutputs.length; i++) {
+      if (availableOutputs[i].blocked == false &&
+          availableOutputs[i].status.confirmed == true) {
+        spendableOutputs.add(availableOutputs[i]);
+        spendableSatoshiValue += availableOutputs[i].value;
+      }
+    }
+
+    // sort spendable by age (oldest first)
+    spendableOutputs.sort(
+        (a, b) => b.status.confirmations.compareTo(a.status.confirmations));
+
+    Logging.instance.log("spendableOutputs.length: ${spendableOutputs.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<UtxoObject> utxoObjectsToUse = [];
+
+    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;
+    }
+
+    Logging.instance
+        .log("satoshisBeingUsed: $satoshisBeingUsed", level: LogLevel.Info);
+    Logging.instance
+        .log("inputsBeingConsumed: $inputsBeingConsumed", level: LogLevel.Info);
+    Logging.instance
+        .log('utxoObjectsToUse: $utxoObjectsToUse', level: LogLevel.Info);
+    Logging.instance
+        .log('satoshiAmountToSend $satoshiAmountToSend', 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(
+        utxosToUse: utxoObjectsToUse,
+        utxoSigningData: utxoSigningData,
+        recipients: [_recipientAddress],
+        satoshiAmounts: [satoshisBeingUsed - 1],
+      ))["vSize"] as int;
+      int feeForOneOutput = estimateTxFee(
+        vSize: vSizeForOneOutput,
+        feeRatePerKB: selectedTxFeeRate,
+      );
+      if (feeForOneOutput < (vSizeForOneOutput + 1) * 1000) {
+        feeForOneOutput = (vSizeForOneOutput + 1) * 1000;
+      }
+
+      final int amount = satoshiAmountToSend - feeForOneOutput;
+      dynamic txn = await buildTransaction(
+        utxosToUse: utxoObjectsToUse,
+        utxoSigningData: utxoSigningData,
+        recipients: recipientsArray,
+        satoshiAmounts: [amount],
+      );
+      Map<String, dynamic> transactionObject = {
+        "hex": txn["hex"],
+        "recipient": recipientsArray[0],
+        "recipientAmt": amount,
+        "fee": feeForOneOutput,
+        "vSize": txn["vSize"],
+      };
+      return transactionObject;
+    }
+
+    final int vSizeForOneOutput = (await buildTransaction(
+      utxosToUse: utxoObjectsToUse,
+      utxoSigningData: utxoSigningData,
+      recipients: [_recipientAddress],
+      satoshiAmounts: [satoshisBeingUsed - 1],
+    ))["vSize"] as int;
+    final int vSizeForTwoOutPuts = (await buildTransaction(
+      utxosToUse: utxoObjectsToUse,
+      utxoSigningData: utxoSigningData,
+      recipients: [
+        _recipientAddress,
+        await _getCurrentAddressForChain(1, DerivePathType.bip44),
+      ],
+      satoshiAmounts: [
+        satoshiAmountToSend,
+        satoshisBeingUsed - satoshiAmountToSend - 1,
+      ], // dust limit is the minimum amount a change output should be
+    ))["vSize"] as int;
+    debugPrint("vSizeForOneOutput $vSizeForOneOutput");
+    debugPrint("vSizeForTwoOutPuts $vSizeForTwoOutPuts");
+
+    // Assume 1 output, only for recipient and no change
+    var feeForOneOutput = estimateTxFee(
+      vSize: vSizeForOneOutput,
+      feeRatePerKB: selectedTxFeeRate,
+    );
+    // Assume 2 outputs, one for recipient and one for change
+    var feeForTwoOutputs = estimateTxFee(
+      vSize: vSizeForTwoOutPuts,
+      feeRatePerKB: selectedTxFeeRate,
+    );
+
+    Logging.instance
+        .log("feeForTwoOutputs: $feeForTwoOutputs", level: LogLevel.Info);
+    Logging.instance
+        .log("feeForOneOutput: $feeForOneOutput", level: LogLevel.Info);
+    if (feeForOneOutput < (vSizeForOneOutput + 1) * 1000) {
+      feeForOneOutput = (vSizeForOneOutput + 1) * 1000;
+    }
+    if (feeForTwoOutputs < ((vSizeForTwoOutPuts + 1) * 1000)) {
+      feeForTwoOutputs = ((vSizeForTwoOutPuts + 1) * 1000);
+    }
+
+    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) {
+        // 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 > 546 satoshis, we perform the mechanics required to properly generate and use a new
+        // change address.
+        if (changeOutputSize > DUST_LIMIT &&
+            satoshisBeingUsed - satoshiAmountToSend - changeOutputSize ==
+                feeForTwoOutputs) {
+          // generate new change address if current change address has been used
+          await _checkChangeAddressForTransactions(DerivePathType.bip44);
+          final String newChangeAddress =
+              await _getCurrentAddressForChain(1, DerivePathType.bip44);
+
+          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(
+            utxosToUse: utxoObjectsToUse,
+            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(
+              utxosToUse: utxoObjectsToUse,
+              utxoSigningData: utxoSigningData,
+              recipients: recipientsArray,
+              satoshiAmounts: recipientsAmtArray,
+            );
+          }
+
+          Map<String, dynamic> transactionObject = {
+            "hex": txn["hex"],
+            "recipient": recipientsArray[0],
+            "recipientAmt": recipientsAmtArray[0],
+            "fee": feeBeingPaid,
+            "vSize": txn["vSize"],
+          };
+          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(
+            utxosToUse: utxoObjectsToUse,
+            utxoSigningData: utxoSigningData,
+            recipients: recipientsArray,
+            satoshiAmounts: recipientsAmtArray,
+          );
+          Map<String, dynamic> transactionObject = {
+            "hex": txn["hex"],
+            "recipient": recipientsArray[0],
+            "recipientAmt": recipientsAmtArray[0],
+            "fee": satoshisBeingUsed - satoshiAmountToSend,
+            "vSize": txn["vSize"],
+          };
+          return transactionObject;
+        }
+      } else {
+        // No additional outputs needed since adding one would mean that it'd be smaller than 546 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(
+          utxosToUse: utxoObjectsToUse,
+          utxoSigningData: utxoSigningData,
+          recipients: recipientsArray,
+          satoshiAmounts: recipientsAmtArray,
+        );
+        Map<String, dynamic> transactionObject = {
+          "hex": txn["hex"],
+          "recipient": recipientsArray[0],
+          "recipientAmt": recipientsAmtArray[0],
+          "fee": satoshisBeingUsed - satoshiAmountToSend,
+          "vSize": txn["vSize"],
+        };
+        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(
+        utxosToUse: utxoObjectsToUse,
+        utxoSigningData: utxoSigningData,
+        recipients: recipientsArray,
+        satoshiAmounts: recipientsAmtArray,
+      );
+      Map<String, dynamic> transactionObject = {
+        "hex": txn["hex"],
+        "recipient": recipientsArray[0],
+        "recipientAmt": recipientsAmtArray[0],
+        "fee": feeForOneOutput,
+        "vSize": txn["vSize"],
+      };
+      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, selectedTxFeeRate,
+            _recipientAddress, isSendAll,
+            additionalOutputs: additionalOutputs + 1, utxos: utxos);
+      }
+      return 2;
+    }
+  }
+
+  Future<Map<String, dynamic>> fetchBuildTxData(
+    List<UtxoObject> utxosToUse,
+  ) async {
+    // return data
+    Map<String, dynamic> results = {};
+    Map<String, List<String>> addressTxid = {};
+
+    // addresses to check
+    List<String> addressesP2PKH = [];
+
+    try {
+      // Populating the addresses to check
+      for (var i = 0; i < utxosToUse.length; i++) {
+        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) {
+            final address = output["scriptPubKey"]["addresses"][0] as String;
+            if (!addressTxid.containsKey(address)) {
+              addressTxid[address] = <String>[];
+            }
+            (addressTxid[address] as List).add(txid);
+            switch (addressType(address: address)) {
+              case DerivePathType.bip44:
+                addressesP2PKH.add(address);
+                break;
+            }
+          }
+        }
+      }
+
+      // p2pkh / bip44
+      final p2pkhLength = addressesP2PKH.length;
+      if (p2pkhLength > 0) {
+        final receiveDerivations = await _fetchDerivations(
+          chain: 0,
+          derivePathType: DerivePathType.bip44,
+        );
+        final changeDerivations = await _fetchDerivations(
+          chain: 1,
+          derivePathType: DerivePathType.bip44,
+        );
+        for (int i = 0; i < p2pkhLength; i++) {
+          // receives
+          final receiveDerivation = receiveDerivations[addressesP2PKH[i]];
+          // if a match exists it will not be null
+          if (receiveDerivation != null) {
+            final data = P2PKH(
+              data: PaymentData(
+                  pubkey: Format.stringToUint8List(
+                      receiveDerivation["pubKey"] as String)),
+              network: _network,
+            ).data;
+
+            for (String tx in addressTxid[addressesP2PKH[i]]!) {
+              results[tx] = {
+                "output": data.output,
+                "keyPair": ECPair.fromWIF(
+                  receiveDerivation["wif"] as String,
+                  network: _network,
+                ),
+              };
+            }
+          } else {
+            // if its not a receive, check change
+            final changeDerivation = changeDerivations[addressesP2PKH[i]];
+            // if a match exists it will not be null
+            if (changeDerivation != null) {
+              final data = P2PKH(
+                data: PaymentData(
+                    pubkey: Format.stringToUint8List(
+                        changeDerivation["pubKey"] as String)),
+                network: _network,
+              ).data;
+
+              for (String tx in addressTxid[addressesP2PKH[i]]!) {
+                results[tx] = {
+                  "output": data.output,
+                  "keyPair": ECPair.fromWIF(
+                    changeDerivation["wif"] as String,
+                    network: _network,
+                  ),
+                };
+              }
+            }
+          }
+        }
+      }
+
+      return results;
+    } 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<UtxoObject> utxosToUse,
+    required Map<String, dynamic> 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 < utxosToUse.length; i++) {
+      final txid = utxosToUse[i].txid;
+      txb.addInput(txid, utxosToUse[i].vout, null,
+          utxoSigningData[txid]["output"] as Uint8List);
+    }
+
+    // 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 < utxosToUse.length; i++) {
+        final txid = utxosToUse[i].txid;
+        txb.sign(
+          vin: i,
+          keyPair: utxoSigningData[txid]["keyPair"] as ECPair,
+          witnessValue: utxosToUse[i].value,
+          redeemScript: utxoSigningData[txid]["redeemScript"] as Uint8List?,
+        );
+      }
+    } 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
+    _cachedElectrumXClient.clearSharedTransactionCache(coin: coin);
+
+    // back up data
+    await _rescanBackup();
+
+    try {
+      final mnemonic = await _secureStore.read(key: '${_walletId}_mnemonic');
+      await _recoverWalletFromBIP32SeedPhrase(
+        mnemonic: mnemonic!,
+        maxUnusedAddressGap: maxUnusedAddressGap,
+        maxNumberOfIndexesToCheck: maxNumberOfIndexesToCheck,
+      );
+
+      longMutex = false;
+      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> _rescanRestore() async {
+    Logging.instance.log("starting rescan restore", level: LogLevel.Info);
+
+    // restore from backup
+    // p2pkh
+    final tempReceivingAddressesP2PKH = DB.instance
+        .get<dynamic>(boxName: walletId, key: 'receivingAddressesP2PKH_BACKUP');
+    final tempChangeAddressesP2PKH = DB.instance
+        .get<dynamic>(boxName: walletId, key: 'changeAddressesP2PKH_BACKUP');
+    final tempReceivingIndexP2PKH = DB.instance
+        .get<dynamic>(boxName: walletId, key: 'receivingIndexP2PKH_BACKUP');
+    final tempChangeIndexP2PKH = DB.instance
+        .get<dynamic>(boxName: walletId, key: 'changeIndexP2PKH_BACKUP');
+    await DB.instance.put<dynamic>(
+        boxName: walletId,
+        key: 'receivingAddressesP2PKH',
+        value: tempReceivingAddressesP2PKH);
+    await DB.instance.put<dynamic>(
+        boxName: walletId,
+        key: 'changeAddressesP2PKH',
+        value: tempChangeAddressesP2PKH);
+    await DB.instance.put<dynamic>(
+        boxName: walletId,
+        key: 'receivingIndexP2PKH',
+        value: tempReceivingIndexP2PKH);
+    await DB.instance.put<dynamic>(
+        boxName: walletId,
+        key: 'changeIndexP2PKH',
+        value: tempChangeIndexP2PKH);
+    await DB.instance.delete<dynamic>(
+        key: 'receivingAddressesP2PKH_BACKUP', boxName: walletId);
+    await DB.instance
+        .delete<dynamic>(key: 'changeAddressesP2PKH_BACKUP', boxName: walletId);
+    await DB.instance
+        .delete<dynamic>(key: 'receivingIndexP2PKH_BACKUP', boxName: walletId);
+    await DB.instance
+        .delete<dynamic>(key: 'changeIndexP2PKH_BACKUP', boxName: walletId);
+
+    // P2PKH derivations
+    final p2pkhReceiveDerivationsString = await _secureStore.read(
+        key: "${walletId}_receiveDerivationsP2PKH_BACKUP");
+    final p2pkhChangeDerivationsString = await _secureStore.read(
+        key: "${walletId}_changeDerivationsP2PKH_BACKUP");
+
+    await _secureStore.write(
+        key: "${walletId}_receiveDerivationsP2PKH",
+        value: p2pkhReceiveDerivationsString);
+    await _secureStore.write(
+        key: "${walletId}_changeDerivationsP2PKH",
+        value: p2pkhChangeDerivationsString);
+
+    await _secureStore.delete(
+        key: "${walletId}_receiveDerivationsP2PKH_BACKUP");
+    await _secureStore.delete(key: "${walletId}_changeDerivationsP2PKH_BACKUP");
+
+    // UTXOs
+    final utxoData = DB.instance
+        .get<dynamic>(boxName: walletId, key: 'latest_utxo_model_BACKUP');
+    await DB.instance.put<dynamic>(
+        boxName: walletId, key: 'latest_utxo_model', value: utxoData);
+    await DB.instance
+        .delete<dynamic>(key: 'latest_utxo_model_BACKUP', boxName: walletId);
+
+    Logging.instance.log("rescan restore  complete", level: LogLevel.Info);
+  }
+
+  Future<void> _rescanBackup() async {
+    Logging.instance.log("starting rescan backup", level: LogLevel.Info);
+
+    // backup current and clear data
+    // p2pkh
+    final tempReceivingAddressesP2PKH = DB.instance
+        .get<dynamic>(boxName: walletId, key: 'receivingAddressesP2PKH');
+    await DB.instance.put<dynamic>(
+        boxName: walletId,
+        key: 'receivingAddressesP2PKH_BACKUP',
+        value: tempReceivingAddressesP2PKH);
+    await DB.instance
+        .delete<dynamic>(key: 'receivingAddressesP2PKH', boxName: walletId);
+
+    final tempChangeAddressesP2PKH = DB.instance
+        .get<dynamic>(boxName: walletId, key: 'changeAddressesP2PKH');
+    await DB.instance.put<dynamic>(
+        boxName: walletId,
+        key: 'changeAddressesP2PKH_BACKUP',
+        value: tempChangeAddressesP2PKH);
+    await DB.instance
+        .delete<dynamic>(key: 'changeAddressesP2PKH', boxName: walletId);
+
+    final tempReceivingIndexP2PKH =
+        DB.instance.get<dynamic>(boxName: walletId, key: 'receivingIndexP2PKH');
+    await DB.instance.put<dynamic>(
+        boxName: walletId,
+        key: 'receivingIndexP2PKH_BACKUP',
+        value: tempReceivingIndexP2PKH);
+    await DB.instance
+        .delete<dynamic>(key: 'receivingIndexP2PKH', boxName: walletId);
+
+    final tempChangeIndexP2PKH =
+        DB.instance.get<dynamic>(boxName: walletId, key: 'changeIndexP2PKH');
+    await DB.instance.put<dynamic>(
+        boxName: walletId,
+        key: 'changeIndexP2PKH_BACKUP',
+        value: tempChangeIndexP2PKH);
+    await DB.instance
+        .delete<dynamic>(key: 'changeIndexP2PKH', boxName: walletId);
+
+    // P2PKH derivations
+    final p2pkhReceiveDerivationsString =
+        await _secureStore.read(key: "${walletId}_receiveDerivationsP2PKH");
+    final p2pkhChangeDerivationsString =
+        await _secureStore.read(key: "${walletId}_changeDerivationsP2PKH");
+
+    await _secureStore.write(
+        key: "${walletId}_receiveDerivationsP2PKH_BACKUP",
+        value: p2pkhReceiveDerivationsString);
+    await _secureStore.write(
+        key: "${walletId}_changeDerivationsP2PKH_BACKUP",
+        value: p2pkhChangeDerivationsString);
+
+    await _secureStore.delete(key: "${walletId}_receiveDerivationsP2PKH");
+    await _secureStore.delete(key: "${walletId}_changeDerivationsP2PKH");
+
+    // UTXOs
+    final utxoData =
+        DB.instance.get<dynamic>(boxName: walletId, key: 'latest_utxo_model');
+    await DB.instance.put<dynamic>(
+        boxName: walletId, key: 'latest_utxo_model_BACKUP', value: utxoData);
+    await DB.instance
+        .delete<dynamic>(key: 'latest_utxo_model', boxName: walletId);
+
+    Logging.instance.log("rescan backup complete", level: LogLevel.Info);
+  }
+
+  @override
+  set isFavorite(bool markFavorite) {
+    DB.instance.put<dynamic>(
+        boxName: walletId, key: "isFavorite", value: markFavorite);
+  }
+
+  @override
+  bool get isFavorite {
+    try {
+      return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
+          as bool;
+    } catch (e, s) {
+      Logging.instance
+          .log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error);
+      rethrow;
+    }
+  }
+
+  @override
+  bool get isRefreshing => refreshMutex;
+
+  bool isActive = false;
+
+  @override
+  void Function(bool)? get onIsActiveWalletChanged =>
+      (isActive) => this.isActive = isActive;
+
+  @override
+  Future<int> estimateFeeFor(int satoshiAmount, int feeRate) async {
+    final available = Format.decimalAmountToSatoshis(await availableBalance);
+
+    if (available == satoshiAmount) {
+      return satoshiAmount - sweepAllEstimate(feeRate);
+    } else if (satoshiAmount <= 0 || satoshiAmount > available) {
+      return roughFeeEstimate(1, 2, feeRate);
+    }
+
+    int runningBalance = 0;
+    int inputCount = 0;
+    for (final output in outputsList) {
+      runningBalance += output.value;
+      inputCount++;
+      if (runningBalance > satoshiAmount) {
+        break;
+      }
+    }
+
+    final oneOutPutFee = roughFeeEstimate(inputCount, 1, feeRate);
+    final twoOutPutFee = roughFeeEstimate(inputCount, 2, feeRate);
+
+    if (runningBalance - satoshiAmount > oneOutPutFee) {
+      if (runningBalance - satoshiAmount > oneOutPutFee + DUST_LIMIT) {
+        final change = runningBalance - satoshiAmount - twoOutPutFee;
+        if (change > DUST_LIMIT &&
+            runningBalance - satoshiAmount - change == twoOutPutFee) {
+          return runningBalance - satoshiAmount - change;
+        } else {
+          return runningBalance - satoshiAmount;
+        }
+      } else {
+        return runningBalance - satoshiAmount;
+      }
+    } else if (runningBalance - satoshiAmount == oneOutPutFee) {
+      return oneOutPutFee;
+    } else {
+      return twoOutPutFee;
+    }
+  }
+
+  // TODO: correct formula for doge?
+  int roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) {
+    return ((181 * inputCount) + (34 * outputCount) + 10) *
+        (feeRatePerKB / 1000).ceil();
+  }
+
+  int sweepAllEstimate(int feeRate) {
+    int available = 0;
+    int inputCount = 0;
+    for (final output in outputsList) {
+      if (output.status.confirmed) {
+        available += output.value;
+        inputCount++;
+      }
+    }
+
+    // transaction will only have 1 output minus the fee
+    final estimatedFee = roughFeeEstimate(inputCount, 1, feeRate);
+
+    return available - estimatedFee;
+  }
+}
+
+// Bitcoincash Network
+final bitcoincash = NetworkType(
+    messagePrefix: '\x18Bitcoin Signed Message:\n',
+    bech32: 'bc',
+    bip32: Bip32Type(public: 0x0488b21e, private: 0x0488ade4),
+    pubKeyHash: 0x00,
+    scriptHash: 0x05,
+    wif: 0x80);
diff --git a/lib/services/coins/coin_service.dart b/lib/services/coins/coin_service.dart
index 3d8b1bd10..4099c34c1 100644
--- a/lib/services/coins/coin_service.dart
+++ b/lib/services/coins/coin_service.dart
@@ -8,6 +8,7 @@ import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart';
 import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart';
 import 'package:stackwallet/services/coins/firo/firo_wallet.dart';
 import 'package:stackwallet/services/coins/monero/monero_wallet.dart';
+import 'package:stackwallet/services/coins/bitcoincash/bitcoincash_wallet.dart';
 import 'package:stackwallet/services/transaction_notification_tracker.dart';
 import 'package:stackwallet/utilities/enums/coin_enum.dart';
 import 'package:stackwallet/utilities/prefs.dart';
@@ -132,6 +133,16 @@ abstract class CoinServiceAPI {
           cachedClient: cachedClient,
           tracker: tracker,
         );
+
+      case Coin.bitcoincash:
+        return BitcoinCashWallet(
+          walletId: walletId,
+          walletName: walletName,
+          coin: coin,
+          client: client,
+          cachedClient: cachedClient,
+          tracker: tracker,
+        );
     }
   }
 
diff --git a/lib/utilities/address_utils.dart b/lib/utilities/address_utils.dart
index 805dc64db..f251f7523 100644
--- a/lib/utilities/address_utils.dart
+++ b/lib/utilities/address_utils.dart
@@ -5,6 +5,7 @@ import 'package:crypto/crypto.dart';
 import 'package:flutter_libepiccash/epic_cash.dart';
 import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart';
 import 'package:stackwallet/services/coins/firo/firo_wallet.dart';
+import 'package:stackwallet/services/coins/bitcoincash/bitcoincash_wallet.dart';
 import 'package:stackwallet/utilities/enums/coin_enum.dart';
 import 'package:stackwallet/utilities/logger.dart';
 
@@ -49,6 +50,8 @@ class AddressUtils {
       case Coin.monero:
         return RegExp("[a-zA-Z0-9]{95}").hasMatch(address) ||
             RegExp("[a-zA-Z0-9]{106}").hasMatch(address);
+      case Coin.bitcoincash:
+        return Address.validateAddress(address, bitcoincash);
       case Coin.bitcoinTestNet:
         return Address.validateAddress(address, testnet);
       case Coin.firoTestNet:
diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart
index 9adc50e59..80e718883 100644
--- a/lib/utilities/assets.dart
+++ b/lib/utilities/assets.dart
@@ -110,6 +110,7 @@ class _SVG {
   String get bitcoinTestnet => "assets/svg/coin_icons/Bitcoin.svg";
   String get firoTestnet => "assets/svg/coin_icons/Firo.svg";
   String get dogecoinTestnet => "assets/svg/coin_icons/Dogecoin.svg";
+  String get bitcoincash => "assets/svg/coin_icons/Bitcoin.svg";
 
   String iconFor({required Coin coin}) {
     switch (coin) {
@@ -123,6 +124,8 @@ class _SVG {
         return firo;
       case Coin.monero:
         return monero;
+      case Coin.bitcoincash:
+        return bitcoincash;
       case Coin.bitcoinTestNet:
         return bitcoinTestnet;
       case Coin.firoTestNet:
@@ -144,6 +147,7 @@ class _PNG {
   String get dogecoin => "assets/images/doge.png";
   String get bitcoin => "assets/images/bitcoin.png";
   String get epicCash => "assets/images/epic-cash.png";
+  String get bitcoincash => "assets/images/bitcoin.png";
 
   String imageFor({required Coin coin}) {
     switch (coin) {
@@ -156,6 +160,8 @@ class _PNG {
       case Coin.epicCash:
         return epicCash;
       case Coin.firo:
+      case Coin.bitcoincash:
+        return bitcoincash;
       case Coin.firoTestNet:
         return firo;
       case Coin.monero:
diff --git a/lib/utilities/block_explorers.dart b/lib/utilities/block_explorers.dart
index a9b3d5db5..9040c956e 100644
--- a/lib/utilities/block_explorers.dart
+++ b/lib/utilities/block_explorers.dart
@@ -22,5 +22,7 @@ Uri getBlockExplorerTransactionUrlFor({
       return Uri.parse("https://explorer.firo.org/tx/$txid");
     case Coin.firoTestNet:
       return Uri.parse("https://testexplorer.firo.org/tx/$txid");
+    case Coin.bitcoincash:
+      return Uri.parse("https://www.blockchain.com/bch/tx/$txid");
   }
 }
diff --git a/lib/utilities/cfcolors.dart b/lib/utilities/cfcolors.dart
index ce6097b7d..d50a2c7ea 100644
--- a/lib/utilities/cfcolors.dart
+++ b/lib/utilities/cfcolors.dart
@@ -10,6 +10,7 @@ class _CoinThemeColor {
   Color get dogecoin => const Color(0xFFFFE079);
   Color get epicCash => const Color(0xFFC1C1FF);
   Color get monero => const Color(0xFFB1C5FF);
+  Color get bitcoincash => const Color(0xFFFCC17B);
 
   Color forCoin(Coin coin) {
     switch (coin) {
@@ -26,6 +27,8 @@ class _CoinThemeColor {
         return firo;
       case Coin.monero:
         return monero;
+      case Coin.bitcoincash:
+        return bitcoincash;
     }
   }
 }
diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart
index b8e5b5bce..97b3d8896 100644
--- a/lib/utilities/constants.dart
+++ b/lib/utilities/constants.dart
@@ -45,6 +45,7 @@ abstract class Constants {
       case Coin.dogecoinTestNet:
       case Coin.firoTestNet:
       case Coin.epicCash:
+      case Coin.bitcoincash:
         values.addAll([24, 21, 18, 15, 12]);
         break;
 
@@ -75,6 +76,8 @@ abstract class Constants {
 
       case Coin.monero:
         return 120;
+      case Coin.bitcoincash:
+        return 600;
     }
   }
 
diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart
index d2e042439..09e373df0 100644
--- a/lib/utilities/default_nodes.dart
+++ b/lib/utilities/default_nodes.dart
@@ -13,6 +13,7 @@ abstract class DefaultNodes {
         firo,
         monero,
         epicCash,
+        bitcoincash,
         bitcoinTestnet,
         dogecoinTestnet,
         firoTestnet,
@@ -80,6 +81,18 @@ abstract class DefaultNodes {
         isDown: false,
       );
 
+  static NodeModel get bitcoincash => NodeModel(
+        host: "https://electrum1.cipig.net:20055",
+        port: 20055,
+        name: defaultName,
+        id: _nodeId(Coin.bitcoincash),
+        useSSL: true,
+        enabled: true,
+        coinName: Coin.bitcoincash.name,
+        isFailover: true,
+        isDown: false,
+      );
+
   static NodeModel get bitcoinTestnet => NodeModel(
         host: "electrumx-testnet.cypherstack.com",
         port: 51002,
@@ -133,6 +146,9 @@ abstract class DefaultNodes {
       case Coin.monero:
         return monero;
 
+      case Coin.bitcoincash:
+        return bitcoincash;
+
       case Coin.bitcoinTestNet:
         return bitcoinTestnet;
 
diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart
index 9e489dde4..fb7b83f5d 100644
--- a/lib/utilities/enums/coin_enum.dart
+++ b/lib/utilities/enums/coin_enum.dart
@@ -5,6 +5,8 @@ import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart'
     as epic;
 import 'package:stackwallet/services/coins/firo/firo_wallet.dart' as firo;
 import 'package:stackwallet/services/coins/monero/monero_wallet.dart' as xmr;
+import 'package:stackwallet/services/coins/bitcoincash/bitcoincash_wallet.dart'
+    as bch;
 
 enum Coin {
   bitcoin,
@@ -12,6 +14,7 @@ enum Coin {
   epicCash,
   firo,
   monero,
+  bitcoincash,
 
   ///
   ///
@@ -38,6 +41,8 @@ extension CoinExt on Coin {
         return "Firo";
       case Coin.monero:
         return "Monero";
+      case Coin.bitcoincash:
+        return "Bitcoincash";
       case Coin.bitcoinTestNet:
         return "tBitcoin";
       case Coin.firoTestNet:
@@ -59,6 +64,8 @@ extension CoinExt on Coin {
         return "FIRO";
       case Coin.monero:
         return "XMR";
+      case Coin.bitcoincash:
+        return "BCH";
       case Coin.bitcoinTestNet:
         return "tBTC";
       case Coin.firoTestNet:
@@ -81,6 +88,8 @@ extension CoinExt on Coin {
         return "firo";
       case Coin.monero:
         return "monero";
+      case Coin.bitcoincash:
+        return "bitcoincash";
       case Coin.bitcoinTestNet:
         return "bitcoin";
       case Coin.firoTestNet:
@@ -95,6 +104,7 @@ extension CoinExt on Coin {
       case Coin.bitcoin:
       case Coin.dogecoin:
       case Coin.firo:
+      case Coin.bitcoincash:
       case Coin.bitcoinTestNet:
       case Coin.firoTestNet:
       case Coin.dogecoinTestNet:
@@ -125,6 +135,9 @@ extension CoinExt on Coin {
 
       case Coin.monero:
         return xmr.MINIMUM_CONFIRMATIONS;
+
+      case Coin.bitcoincash:
+        return bch.MINIMUM_CONFIRMATIONS;
     }
   }
 }
@@ -146,6 +159,9 @@ Coin coinFromPrettyName(String name) {
     case "Monero":
     case "monero":
       return Coin.monero;
+    case "Bitcoincash":
+    case "bitcoincash":
+      return Coin.bitcoincash;
     case "Bitcoin Testnet":
     case "tBitcoin":
     case "bitcoinTestNet":
@@ -176,6 +192,8 @@ Coin coinFromTickerCaseInsensitive(String ticker) {
       return Coin.firo;
     case "xmr":
       return Coin.monero;
+    case "bch":
+      return Coin.bitcoincash;
     case "tbtc":
       return Coin.bitcoinTestNet;
     case "tfiro":