/* 
 * This file is part of Stack Wallet.
 * 
 * Copyright (c) 2023 Cypher Stack
 * All Rights Reserved.
 * The code is distributed under GPLv3 license, see LICENSE file for details.
 * Generated by Cypher Stack on 2023-05-26
 *
 */

import 'dart:async';

import 'package:bip39/bip39.dart' as bip39;
import 'package:ethereum_addresses/ethereum_addresses.dart';
import 'package:http/http.dart';
import 'package:isar/isar.dart';
import 'package:stackwallet/db/hive/db.dart';
import 'package:stackwallet/db/isar/main_db.dart';
import 'package:stackwallet/models/balance.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/models/paymint/fee_object_model.dart';
import 'package:stackwallet/services/coins/coin_service.dart';
import 'package:stackwallet/services/ethereum/ethereum_api.dart';
import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/services/mixins/eth_token_cache.dart';
import 'package:stackwallet/services/mixins/wallet_cache.dart';
import 'package:stackwallet/services/mixins/wallet_db.dart';
import 'package:stackwallet/services/node_service.dart';
import 'package:stackwallet/services/transaction_notification_tracker.dart';
import 'package:stackwallet/utilities/amount/amount.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/eth_commons.dart';
import 'package:stackwallet/utilities/extensions/extensions.dart';
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:stackwallet/widgets/crypto_notifications.dart';
import 'package:tuple/tuple.dart';
import 'package:web3dart/web3dart.dart' as web3;

const int MINIMUM_CONFIRMATIONS = 3;

class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB {
  EthereumWallet({
    required String walletId,
    required String walletName,
    required Coin coin,
    required SecureStorageInterface secureStore,
    required TransactionNotificationTracker tracker,
    MainDB? mockableOverride,
  }) {
    txTracker = tracker;
    _walletId = walletId;
    _walletName = walletName;
    _coin = coin;
    _secureStore = secureStore;
    initCache(walletId, coin);
    initWalletDB(mockableOverride: mockableOverride);
  }

  NodeModel? _ethNode;

  final _gasLimit = 21000;

  Timer? timer;
  Timer? _networkAliveTimer;

  Future<void> updateTokenContracts(List<String> contractAddresses) async {
    // final set = getWalletTokenContractAddresses().toSet();
    // set.addAll(contractAddresses);
    await updateWalletTokenContractAddresses(contractAddresses);

    GlobalEventBus.instance.fire(
      UpdatedInBackgroundEvent(
        "$contractAddresses updated/added for: $walletId $walletName",
        walletId,
      ),
    );
  }

  Balance getCachedTokenBalance(EthContract contract) {
    final jsonString = DB.instance.get<dynamic>(
      boxName: _walletId,
      key: TokenCacheKeys.tokenBalance(contract.address),
    ) as String?;
    if (jsonString == null) {
      return Balance(
        total: Amount(
          rawValue: BigInt.zero,
          fractionDigits: contract.decimals,
        ),
        spendable: Amount(
          rawValue: BigInt.zero,
          fractionDigits: contract.decimals,
        ),
        blockedTotal: Amount(
          rawValue: BigInt.zero,
          fractionDigits: contract.decimals,
        ),
        pendingSpendable: Amount(
          rawValue: BigInt.zero,
          fractionDigits: contract.decimals,
        ),
      );
    }
    return Balance.fromJson(
      jsonString,
      contract.decimals,
    );
  }

  // Future<void> removeTokenContract(String contractAddress) async {
  //   final set = getWalletTokenContractAddresses().toSet();
  //   set.removeWhere((e) => e == contractAddress);
  //   await updateWalletTokenContractAddresses(set.toList());
  //
  //   GlobalEventBus.instance.fire(
  //     UpdatedInBackgroundEvent(
  //       "$contractAddress removed for: $walletId $walletName",
  //       walletId,
  //     ),
  //   );
  // }

  @override
  String get walletId => _walletId;
  late String _walletId;

  @override
  String get walletName => _walletName;
  late String _walletName;

  @override
  set walletName(String newName) => _walletName = newName;

  @override
  set isFavorite(bool markFavorite) {
    _isFavorite = markFavorite;
    updateCachedIsFavorite(markFavorite);
  }

  @override
  bool get isFavorite => _isFavorite ??= getCachedIsFavorite();
  bool? _isFavorite;

  @override
  Coin get coin => _coin;
  late Coin _coin;

  late SecureStorageInterface _secureStore;
  late final TransactionNotificationTracker txTracker;
  final _prefs = Prefs.instance;
  bool longMutex = false;

  NodeModel getCurrentNode() {
    return _ethNode ??
        NodeService(secureStorageInterface: _secureStore)
            .getPrimaryNodeFor(coin: coin) ??
        DefaultNodes.getNodeFor(coin);
  }

  web3.Web3Client getEthClient() {
    final node = getCurrentNode();
    return web3.Web3Client(node.host, Client());
  }

  late web3.EthPrivateKey _credentials;

  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();
      }
    }
  }

  @override
  Future<List<UTXO>> get utxos => db.getUTXOs(walletId).findAll();

  @override
  Future<List<Transaction>> get transactions => db
      .getTransactions(walletId)
      .filter()
      .otherDataEqualTo(
          null) // eth txns with other data where other data is the token contract address
      .sortByTimestampDesc()
      .findAll();

  @override
  Future<String> get currentReceivingAddress async {
    final address = await _currentReceivingAddress;
    return checksumEthereumAddress(
        address?.value ?? _credentials.address.hexEip55);
  }

  Future<Address?> get _currentReceivingAddress => db
      .getAddresses(walletId)
      .filter()
      .typeEqualTo(AddressType.ethereum)
      .subTypeEqualTo(AddressSubType.receiving)
      .sortByDerivationIndexDesc()
      .findFirst();

  @override
  Balance get balance => _balance ??= getCachedBalance();
  Balance? _balance;

  Future<void> updateBalance() async {
    web3.Web3Client client = getEthClient();
    web3.EtherAmount ethBalance = await client.getBalance(_credentials.address);
    _balance = Balance(
      total: Amount(
        rawValue: ethBalance.getInWei,
        fractionDigits: coin.decimals,
      ),
      spendable: Amount(
        rawValue: ethBalance.getInWei,
        fractionDigits: coin.decimals,
      ),
      blockedTotal: Amount(
        rawValue: BigInt.zero,
        fractionDigits: coin.decimals,
      ),
      pendingSpendable: Amount(
        rawValue: BigInt.zero,
        fractionDigits: coin.decimals,
      ),
    );
    await updateCachedBalance(_balance!);
  }

  @override
  Future<Amount> estimateFeeFor(Amount amount, int feeRate) async {
    return estimateFee(feeRate, _gasLimit, coin.decimals);
  }

  @override
  Future<void> exit() async {
    _hasCalledExit = true;
    timer?.cancel();
    timer = null;
    stopNetworkAlivePinging();
  }

  @override
  Future<FeeObject> get fees => EthereumAPI.getFees();

  @override
  Future<void> fullRescan(
    int maxUnusedAddressGap,
    int maxNumberOfIndexesToCheck,
  ) async {
    await db.deleteWalletBlockchainData(walletId);
    await _generateAndSaveAddress(
      (await mnemonicString)!,
      (await mnemonicPassphrase)!,
    );
    await updateBalance();
    await _refreshTransactions(isRescan: true);
  }

  @override
  Future<bool> generateNewAddress() {
    // not used for ETH
    throw UnimplementedError();
  }

  bool _hasCalledExit = false;

  @override
  bool get hasCalledExit => _hasCalledExit;

  @override
  Future<void> initializeExisting() async {
    Logging.instance.log(
      "initializeExisting() ${coin.prettyName} wallet",
      level: LogLevel.Info,
    );

    await _initCredentials(
      (await mnemonicString)!,
      (await mnemonicPassphrase)!,
    );

    if (getCachedId() == null) {
      throw Exception(
          "Attempted to initialize an existing wallet using an unknown wallet ID!");
    }
    await _prefs.init();
  }

  @override
  Future<void> initializeNew() async {
    Logging.instance.log(
      "Generating new ${coin.prettyName} wallet.",
      level: LogLevel.Info,
    );

    if (getCachedId() != 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([
      updateCachedId(walletId),
      updateCachedIsFavorite(false),
    ]);
  }

  Future<void> _generateNewWallet() async {
    // Logging.instance
    //     .log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info);
    // if (!integrationTestFlag) {
    //   try {
    //     final features = await electrumXClient.getServerFeatures();
    //     Logging.instance.log("features: $features", level: LogLevel.Info);
    //     switch (coin) {
    //       case Coin.namecoin:
    //         if (features['genesis_hash'] != GENESIS_HASH_MAINNET) {
    //           throw Exception("genesis hash does not match main net!");
    //         }
    //         break;
    //       default:
    //         throw Exception(
    //             "Attempted to generate a EthereumWallet using a non eth coin type: ${coin.name}");
    //     }
    //   } catch (e, s) {
    //     Logging.instance.log("$e/n$s", level: LogLevel.Info);
    //   }
    // }

    // this should never fail - sanity check
    if ((await mnemonicString) != null || (await mnemonicPassphrase) != null) {
      throw Exception(
          "Attempted to overwrite mnemonic on generate new wallet!");
    }

    final String mnemonic = bip39.generateMnemonic(strength: 128);
    await _secureStore.write(key: '${_walletId}_mnemonic', value: mnemonic);
    await _secureStore.write(
      key: '${_walletId}_mnemonicPassphrase',
      value: "",
    );

    await _generateAndSaveAddress(mnemonic, "");

    Logging.instance.log("_generateNewWalletFinished", level: LogLevel.Info);
  }

  Future<void> _initCredentials(
    String mnemonic,
    String mnemonicPassphrase,
  ) async {
    String privateKey = getPrivateKey(mnemonic, mnemonicPassphrase);
    _credentials = web3.EthPrivateKey.fromHex(privateKey);
  }

  Future<void> _generateAndSaveAddress(
    String mnemonic,
    String mnemonicPassphrase,
  ) async {
    await _initCredentials(mnemonic, mnemonicPassphrase);

    final address = Address(
      walletId: walletId,
      value: _credentials.address.hexEip55,
      publicKey: [], // maybe store address bytes here? seems a waste of space though
      derivationIndex: 0,
      derivationPath: DerivationPath()..value = "$hdPathEthereum/0",
      type: AddressType.ethereum,
      subType: AddressSubType.receiving,
    );

    await db.putAddress(address);
  }

  bool _isConnected = false;

  @override
  bool get isConnected => _isConnected;

  @override
  bool get isRefreshing => refreshMutex;

  bool refreshMutex = false;

  @override
  Future<int> get maxFee async {
    throw UnimplementedError("Not used for eth");
  }

  @override
  Future<List<String>> get mnemonic => _getMnemonicList();

  @override
  Future<String?> get mnemonicString =>
      _secureStore.read(key: '${_walletId}_mnemonic');

  @override
  Future<String?> get mnemonicPassphrase => _secureStore.read(
        key: '${_walletId}_mnemonicPassphrase',
      );

  Future<int> get chainHeight async {
    web3.Web3Client client = getEthClient();
    try {
      final height = await client.getBlockNumber();
      await updateCachedChainHeight(height);
      if (height > storedChainHeight) {
        GlobalEventBus.instance.fire(
          UpdatedInBackgroundEvent(
            "Updated current chain height in $walletId $walletName!",
            walletId,
          ),
        );
      }
      return height;
    } catch (e, s) {
      Logging.instance.log("Exception caught in chainHeight: $e\n$s",
          level: LogLevel.Error);
      return storedChainHeight;
    }
  }

  @override
  int get storedChainHeight => getCachedChainHeight();

  Future<List<String>> _getMnemonicList() async {
    final _mnemonicString = await mnemonicString;
    if (_mnemonicString == null) {
      return [];
    }
    final List<String> data = _mnemonicString.split(' ');
    return data;
  }

  @override
  Future<Map<String, dynamic>> prepareSend({
    required String address,
    required Amount amount,
    Map<String, dynamic>? args,
  }) async {
    final feeRateType = args?["feeRate"];
    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;
    }

    final feeEstimate = await estimateFeeFor(amount, fee);

    // bool isSendAll = false;
    // final availableBalance = balance.spendable;
    // if (satoshiAmount == availableBalance) {
    //   isSendAll = true;
    // }
    //
    // if (isSendAll) {
    //   //Subtract fee amount from send amount
    //   satoshiAmount -= feeEstimate;
    // }

    final client = getEthClient();

    final myAddress = await currentReceivingAddress;
    final myWeb3Address = web3.EthereumAddress.fromHex(myAddress);

    final est = await client.estimateGas(
      sender: myWeb3Address,
      to: web3.EthereumAddress.fromHex(address),
      gasPrice: web3.EtherAmount.fromUnitAndValue(
        web3.EtherUnit.wei,
        fee,
      ),
      amountOfGas: BigInt.from(_gasLimit),
      value: web3.EtherAmount.inWei(amount.raw),
    );

    final nonce = args?["nonce"] as int? ??
        await client.getTransactionCount(myWeb3Address,
            atBlock: const web3.BlockNum.pending());

    final nResponse = await EthereumAPI.getAddressNonce(address: myAddress);
    print("==============================================================");
    print("ETH client.estimateGas:  $est");
    print("ETH estimateFeeFor    :  $feeEstimate");
    print("ETH nonce custom response:  $nResponse");
    print("ETH actual nonce         :  $nonce");
    print("==============================================================");

    final tx = web3.Transaction(
      to: web3.EthereumAddress.fromHex(address),
      gasPrice: web3.EtherAmount.fromUnitAndValue(
        web3.EtherUnit.wei,
        fee,
      ),
      maxGas: _gasLimit,
      value: web3.EtherAmount.inWei(amount.raw),
      nonce: nonce,
    );

    Map<String, dynamic> txData = {
      "fee": feeEstimate,
      "feeInWei": fee,
      "address": address,
      "recipientAmt": amount,
      "ethTx": tx,
      "chainId": (await client.getChainId()).toInt(),
      "nonce": tx.nonce,
    };

    return txData;
  }

  @override
  Future<String> confirmSend({required Map<String, dynamic> txData}) async {
    web3.Web3Client client = getEthClient();

    final txid = await client.sendTransaction(
      _credentials,
      txData["ethTx"] as web3.Transaction,
      chainId: txData["chainId"] as int,
    );

    return txid;
  }

  @override
  Future<void> recoverFromMnemonic({
    required String mnemonic,
    String? mnemonicPassphrase,
    required int maxUnusedAddressGap,
    required int maxNumberOfIndexesToCheck,
    required int height,
  }) async {
    longMutex = true;
    final start = DateTime.now();

    try {
      // check to make sure we aren't overwriting a mnemonic
      // this should never fail
      if ((await mnemonicString) != null ||
          (await this.mnemonicPassphrase) != null) {
        longMutex = false;
        throw Exception("Attempted to overwrite mnemonic on restore!");
      }

      await _secureStore.write(
          key: '${_walletId}_mnemonic', value: mnemonic.trim());
      await _secureStore.write(
        key: '${_walletId}_mnemonicPassphrase',
        value: mnemonicPassphrase ?? "",
      );

      String privateKey =
          getPrivateKey(mnemonic.trim(), mnemonicPassphrase ?? "");
      _credentials = web3.EthPrivateKey.fromHex(privateKey);

      final address = Address(
        walletId: walletId,
        value: _credentials.address.hexEip55,
        publicKey: [], // maybe store address bytes here? seems a waste of space though
        derivationIndex: 0,
        derivationPath: DerivationPath()..value = "$hdPathEthereum/0",
        type: AddressType.ethereum,
        subType: AddressSubType.receiving,
      );

      await db.putAddress(address);

      await Future.wait([
        updateCachedId(walletId),
        updateCachedIsFavorite(false),
      ]);
    } 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<List<Address>> _fetchAllOwnAddresses() => db
      .getAddresses(walletId)
      .filter()
      .not()
      .typeEqualTo(AddressType.nonWallet)
      .and()
      .group((q) => q
          .subTypeEqualTo(AddressSubType.receiving)
          .or()
          .subTypeEqualTo(AddressSubType.change))
      .findAll();

  Future<bool> refreshIfThereIsNewData() async {
    if (longMutex) return false;
    if (_hasCalledExit) return false;
    final currentChainHeight = await chainHeight;

    try {
      bool needsRefresh = false;
      Set<String> txnsToCheck = {};

      for (final String txid in txTracker.pendings) {
        if (!txTracker.wasNotifiedConfirmed(txid)) {
          txnsToCheck.add(txid);
        }
      }

      for (String txid in txnsToCheck) {
        final response = await EthereumAPI.getEthTransactionByHash(txid);
        final txBlockNumber = response.value?.blockNumber;

        if (txBlockNumber != null) {
          final int txConfirmations = currentChainHeight - txBlockNumber;
          bool isUnconfirmed = txConfirmations < MINIMUM_CONFIRMATIONS;
          if (!isUnconfirmed) {
            needsRefresh = true;
            break;
          }
        }
      }
      if (!needsRefresh) {
        var allOwnAddresses = await _fetchAllOwnAddresses();
        final response = await EthereumAPI.getEthTransactions(
          address: allOwnAddresses.elementAt(0).value,
        );
        if (response.value != null) {
          final allTxs = response.value!;
          for (final element in allTxs) {
            final txid = element.hash;
            if ((await db
                    .getTransactions(walletId)
                    .filter()
                    .txidMatches(txid)
                    .findFirst()) ==
                null) {
              Logging.instance.log(
                  " txid not found in address history already $txid",
                  level: LogLevel.Info);
              needsRefresh = true;
              break;
            }
          }
        } else {
          Logging.instance.log(
            " refreshIfThereIsNewData get eth transactions failed: ${response.exception}",
            level: LogLevel.Error,
          );
        }
      }
      return needsRefresh;
    } catch (e, s) {
      Logging.instance.log(
          "Exception caught in refreshIfThereIsNewData: $e\n$s",
          level: LogLevel.Error);
      rethrow;
    }
  }

  Future<void> getAllTxsToWatch() async {
    if (_hasCalledExit) return;
    List<Transaction> unconfirmedTxnsToNotifyPending = [];
    List<Transaction> unconfirmedTxnsToNotifyConfirmed = [];

    final currentChainHeight = await chainHeight;

    final txCount = await db.getTransactions(walletId).count();

    const paginateLimit = 50;

    for (int i = 0; i < txCount; i += paginateLimit) {
      final transactions = await db
          .getTransactions(walletId)
          .offset(i)
          .limit(paginateLimit)
          .findAll();
      for (final tx in transactions) {
        if (tx.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) {
          // get all transactions that were notified as pending but not as confirmed
          if (txTracker.wasNotifiedPending(tx.txid) &&
              !txTracker.wasNotifiedConfirmed(tx.txid)) {
            unconfirmedTxnsToNotifyConfirmed.add(tx);
          }
        } else {
          // get all transactions that were not notified as pending yet
          if (!txTracker.wasNotifiedPending(tx.txid)) {
            unconfirmedTxnsToNotifyPending.add(tx);
          }
        }
      }
    }

    // notify on unconfirmed transactions
    for (final tx in unconfirmedTxnsToNotifyPending) {
      final confirmations = tx.getConfirmations(currentChainHeight);

      if (tx.type == TransactionType.incoming) {
        CryptoNotificationsEventBus.instance.fire(
          CryptoNotificationEvent(
            title: "Incoming transaction",
            walletId: walletId,
            date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000),
            shouldWatchForUpdates: confirmations < MINIMUM_CONFIRMATIONS,
            txid: tx.txid,
            confirmations: confirmations,
            requiredConfirmations: MINIMUM_CONFIRMATIONS,
            walletName: walletName,
            coin: coin,
          ),
        );

        await txTracker.addNotifiedPending(tx.txid);
      } else if (tx.type == TransactionType.outgoing) {
        CryptoNotificationsEventBus.instance.fire(
          CryptoNotificationEvent(
            title: "Sending transaction",
            walletId: walletId,
            date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000),
            shouldWatchForUpdates: confirmations < MINIMUM_CONFIRMATIONS,
            txid: tx.txid,
            confirmations: confirmations,
            requiredConfirmations: MINIMUM_CONFIRMATIONS,
            walletName: walletName,
            coin: coin,
          ),
        );

        await txTracker.addNotifiedPending(tx.txid);
      }
    }

    // notify on confirmed
    for (final tx in unconfirmedTxnsToNotifyConfirmed) {
      if (tx.type == TransactionType.incoming) {
        CryptoNotificationsEventBus.instance.fire(
          CryptoNotificationEvent(
            title: "Incoming transaction confirmed",
            walletId: walletId,
            date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000),
            shouldWatchForUpdates: false,
            txid: tx.txid,
            requiredConfirmations: MINIMUM_CONFIRMATIONS,
            walletName: walletName,
            coin: coin,
          ),
        );

        await txTracker.addNotifiedConfirmed(tx.txid);
      } else if (tx.type == TransactionType.outgoing) {
        CryptoNotificationsEventBus.instance.fire(
          CryptoNotificationEvent(
            title: "Outgoing transaction confirmed",
            walletId: walletId,
            date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000),
            shouldWatchForUpdates: false,
            txid: tx.txid,
            requiredConfirmations: MINIMUM_CONFIRMATIONS,
            walletName: walletName,
            coin: coin,
          ),
        );

        await txTracker.addNotifiedConfirmed(tx.txid);
      }
    }
  }

  @override
  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) {
        GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId));

        final newTxDataFuture = _refreshTransactions();
        GlobalEventBus.instance
            .fire(RefreshPercentChangedEvent(0.50, walletId));

        // final feeObj = _getFees();
        GlobalEventBus.instance
            .fire(RefreshPercentChangedEvent(0.60, walletId));

        GlobalEventBus.instance
            .fire(RefreshPercentChangedEvent(0.70, walletId));
        // _feeObject = Future(() => feeObj);
        GlobalEventBus.instance
            .fire(RefreshPercentChangedEvent(0.80, walletId));

        final allTxsToWatch = getAllTxsToWatch();
        await Future.wait([
          updateBalance(),
          newTxDataFuture,
          // feeObj,
          allTxsToWatch,
        ]);
        GlobalEventBus.instance
            .fire(RefreshPercentChangedEvent(0.90, walletId));
      }
      refreshMutex = false;
      GlobalEventBus.instance.fire(RefreshPercentChangedEvent(1.0, walletId));
      GlobalEventBus.instance.fire(
        WalletSyncStatusChangedEvent(
          WalletSyncStatus.synced,
          walletId,
          coin,
        ),
      );

      if (shouldAutoSync) {
        timer ??= Timer.periodic(const Duration(seconds: 30), (timer) async {
          Logging.instance.log(
              "Periodic refresh check for $walletId $walletName in object instance: $hashCode",
              level: LogLevel.Info);
          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 $walletName $walletId refresh(): $error\n$strace",
        level: LogLevel.Warning,
      );
    }
  }

  @override
  Future<bool> testNetworkConnection() async {
    web3.Web3Client client = getEthClient();
    try {
      await client.getBlockNumber();
      return true;
    } catch (_) {
      return false;
    }
  }

  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));
    }
  }

  @override
  Future<void> updateNode(bool shouldRefresh) async {
    _ethNode = NodeService(secureStorageInterface: _secureStore)
            .getPrimaryNodeFor(coin: coin) ??
        DefaultNodes.getNodeFor(coin);

    if (shouldRefresh) {
      unawaited(refresh());
    }
  }

  @override
  Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
    final txid = txData["txid"] as String;
    final addressString = checksumEthereumAddress(txData["address"] as String);
    final response = await EthereumAPI.getEthTransactionByHash(txid);

    final transaction = Transaction(
      walletId: walletId,
      txid: txid,
      timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
      type: TransactionType.outgoing,
      subType: TransactionSubType.none,
      // precision may be lost here hence the following amountString
      amount: (txData["recipientAmt"] as Amount).raw.toInt(),
      amountString: (txData["recipientAmt"] as Amount).toJsonString(),
      fee: (txData["fee"] as Amount).raw.toInt(),
      height: null,
      isCancelled: false,
      isLelantus: false,
      otherData: null,
      slateId: null,
      nonce: (txData["nonce"] as int?) ??
          response.value?.nonce.toBigIntFromHex.toInt(),
      inputs: [],
      outputs: [],
      numberOfMessages: null,
    );

    Address? address = await db.getAddress(
      walletId,
      addressString,
    );

    address ??= Address(
      walletId: walletId,
      value: addressString,
      publicKey: [],
      derivationIndex: -1,
      derivationPath: null,
      type: AddressType.ethereum,
      subType: AddressSubType.nonWallet,
    );

    await db.addNewTransactionData(
      [
        Tuple2(transaction, address),
      ],
      walletId,
    );
  }

  @override
  bool validateAddress(String address) {
    return isValidEthereumAddress(address);
  }

  Future<void> _refreshTransactions({bool isRescan = false}) async {
    String thisAddress = await currentReceivingAddress;

    int firstBlock = 0;

    if (!isRescan) {
      firstBlock =
          await db.getTransactions(walletId).heightProperty().max() ?? 0;

      if (firstBlock > 10) {
        // add some buffer
        firstBlock -= 10;
      }
    }

    final response = await EthereumAPI.getEthTransactions(
      address: thisAddress,
      firstBlock: isRescan ? 0 : firstBlock,
      includeTokens: true,
    );

    if (response.value == null) {
      Logging.instance.log(
        "Failed to refresh transactions for ${coin.prettyName} $walletName "
        "$walletId: ${response.exception}",
        level: LogLevel.Warning,
      );
      return;
    }

    if (response.value!.isEmpty) {
      // no new transactions found
      return;
    }

    final txsResponse =
        await EthereumAPI.getEthTransactionNonces(response.value!);

    if (txsResponse.value != null) {
      final allTxs = txsResponse.value!;
      final List<Tuple2<Transaction, Address?>> txnsData = [];
      for (final tuple in allTxs) {
        final element = tuple.item1;

        Amount transactionAmount = element.value;

        bool isIncoming;
        bool txFailed = false;
        if (checksumEthereumAddress(element.from) == thisAddress) {
          if (element.isError != 0) {
            txFailed = true;
          }
          isIncoming = false;
        } else if (checksumEthereumAddress(element.to) == thisAddress) {
          isIncoming = true;
        } else {
          continue;
        }

        //Calculate fees (GasLimit * gasPrice)
        // int txFee = element.gasPrice * element.gasUsed;
        Amount txFee = element.gasCost;

        final String addressString = checksumEthereumAddress(element.to);
        final int height = element.blockNumber;

        final txn = Transaction(
          walletId: walletId,
          txid: element.hash,
          timestamp: element.timestamp,
          type:
              isIncoming ? TransactionType.incoming : TransactionType.outgoing,
          subType: TransactionSubType.none,
          amount: transactionAmount.raw.toInt(),
          amountString: transactionAmount.toJsonString(),
          fee: txFee.raw.toInt(),
          height: height,
          isCancelled: txFailed,
          isLelantus: false,
          slateId: null,
          otherData: null,
          nonce: tuple.item2,
          inputs: [],
          outputs: [],
          numberOfMessages: null,
        );

        Address? transactionAddress = await db
            .getAddresses(walletId)
            .filter()
            .valueEqualTo(addressString)
            .findFirst();

        if (transactionAddress == null) {
          if (isIncoming) {
            transactionAddress = Address(
              walletId: walletId,
              value: addressString,
              publicKey: [],
              derivationIndex: 0,
              derivationPath: DerivationPath()..value = "$hdPathEthereum/0",
              type: AddressType.ethereum,
              subType: AddressSubType.receiving,
            );
          } else {
            final myRcvAddr = await currentReceivingAddress;
            final isSentToSelf = myRcvAddr == addressString;

            transactionAddress = Address(
              walletId: walletId,
              value: addressString,
              publicKey: [],
              derivationIndex: isSentToSelf ? 0 : -1,
              derivationPath: isSentToSelf
                  ? (DerivationPath()..value = "$hdPathEthereum/0")
                  : null,
              type: AddressType.ethereum,
              subType: isSentToSelf
                  ? AddressSubType.receiving
                  : AddressSubType.nonWallet,
            );
          }
        }

        txnsData.add(Tuple2(txn, transactionAddress));
      }
      await db.addNewTransactionData(txnsData, walletId);

      // quick hack to notify manager to call notifyListeners if
      // transactions changed
      if (txnsData.isNotEmpty) {
        GlobalEventBus.instance.fire(
          UpdatedInBackgroundEvent(
            "Transactions updated/added for: $walletId $walletName  ",
            walletId,
          ),
        );
      }
    } else {
      Logging.instance.log(
        "Failed to refresh transactions with nonces for ${coin.prettyName} "
        "$walletName $walletId: ${txsResponse.exception}",
        level: LogLevel.Warning,
      );
    }
  }

  void stopNetworkAlivePinging() {
    _networkAliveTimer?.cancel();
    _networkAliveTimer = null;
  }

  void startNetworkAlivePinging() {
    // call once on start right away
    _periodicPingCheck();

    // then periodically check
    _networkAliveTimer = Timer.periodic(
      Constants.networkAliveTimerDuration,
      (_) async {
        _periodicPingCheck();
      },
    );
  }
}